diff --git a/test/api/v3/integration/payments/GET-payments_amazon_subscribe_cancel.test.js b/test/api/v3/integration/payments/GET-payments_amazon_subscribe_cancel.test.js deleted file mode 100644 index 37588d1f18..0000000000 --- a/test/api/v3/integration/payments/GET-payments_amazon_subscribe_cancel.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -describe('payments : amazon #subscribeCancel', () => { - let endpoint = '/amazon/subscribe/cancel'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies subscription', async () => { - await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('missingSubscription'), - }); - }); -}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_checkout.test.js b/test/api/v3/integration/payments/POST-payments_amazon_checkout.test.js deleted file mode 100644 index 8745a74e85..0000000000 --- a/test/api/v3/integration/payments/POST-payments_amazon_checkout.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - generateUser, -} from '../../../../helpers/api-integration/v3'; - -describe('payments - amazon - #checkout', () => { - let endpoint = '/amazon/checkout'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Missing req.body.orderReferenceId', - }); - }); -}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_subscribe.test.js b/test/api/v3/integration/payments/POST-payments_amazon_subscribe.test.js deleted file mode 100644 index 5c3b98ad87..0000000000 --- a/test/api/v3/integration/payments/POST-payments_amazon_subscribe.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -describe('payments - amazon - #subscribe', () => { - let endpoint = '/amazon/subscribe'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies subscription code', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('missingSubscriptionCode'), - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js new file mode 100644 index 0000000000..2ddc847fa6 --- /dev/null +++ b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js @@ -0,0 +1,78 @@ +import { + generateUser, + generateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import amzLib from '../../../../../../website/server/libs/amazonPayments'; + +describe('payments : amazon #subscribeCancel', () => { + let endpoint = '/amazon/subscribe/cancel'; + let user, group, amazonSubscribeCancelStub; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('throws error when there users has no subscription', async () => { + await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('missingSubscription'), + }); + }); + + describe('success', () => { + beforeEach(async () => { + amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').returnsPromise().resolves({}); + }); + + afterEach(() => { + amzLib.cancelSubscription.restore(); + }); + + it('cancels a user subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + await user.get(endpoint); + + expect(amazonSubscribeCancelStub).to.be.calledOnce; + expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); + expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(undefined); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + + it('cancels a group subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + group = await generateGroup(user, { + name: 'test group', + type: 'guild', + privacy: 'public', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + }); + + await user.get(`${endpoint}?groupId=${group._id}`); + + expect(amazonSubscribeCancelStub).to.be.calledOnce; + expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); + expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(group._id); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js new file mode 100644 index 0000000000..7a431b4242 --- /dev/null +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js @@ -0,0 +1,63 @@ +import { + generateUser, +} from '../../../../../helpers/api-integration/v3'; +import amzLib from '../../../../../../website/server/libs/amazonPayments'; + +describe('payments - amazon - #checkout', () => { + let endpoint = '/amazon/checkout'; + let user, amazonCheckoutStub; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies credentials', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Missing req.body.orderReferenceId', + }); + }); + + describe('success', () => { + beforeEach(async () => { + amazonCheckoutStub = sinon.stub(amzLib, 'checkout').returnsPromise().resolves({}); + }); + + afterEach(() => { + amzLib.checkout.restore(); + }); + + it('makes a purcahse with amazon checkout', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + let gift = { + type: 'gems', + gems: { + amount: 16, + uuid: user._id, + }, + }; + + let orderReferenceId = 'orderReferenceId-example'; + + await user.post(endpoint, { + gift, + orderReferenceId, + }); + + expect(amazonCheckoutStub).to.be.calledOnce; + expect(amazonCheckoutStub.args[0][0].user._id).to.eql(user._id); + expect(amazonCheckoutStub.args[0][0].gift).to.eql(gift); + expect(amazonCheckoutStub.args[0][0].orderReferenceId).to.eql(orderReferenceId); + expect(amazonCheckoutStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonCheckoutStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_createOrderReferenceId.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js similarity index 89% rename from test/api/v3/integration/payments/POST-payments_amazon_createOrderReferenceId.test.js rename to test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js index 7838d4194a..ce9a361353 100644 --- a/test/api/v3/integration/payments/POST-payments_amazon_createOrderReferenceId.test.js +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js @@ -1,6 +1,6 @@ import { generateUser, -} from '../../../../helpers/api-integration/v3'; +} from '../../../../../helpers/api-integration/v3'; describe('payments - amazon - #createOrderReferenceId', () => { let endpoint = '/amazon/createOrderReferenceId'; diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js new file mode 100644 index 0000000000..03a0427df3 --- /dev/null +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js @@ -0,0 +1,96 @@ +import { + generateUser, + generateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import amzLib from '../../../../../../website/server/libs/amazonPayments'; + +describe('payments - amazon - #subscribe', () => { + let endpoint = '/amazon/subscribe'; + let user, group, subscribeWithAmazonStub; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies subscription code', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingSubscriptionCode'), + }); + }); + + describe('success', () => { + let billingAgreementId = 'billingAgreementId-example'; + let subscription = 'basic_3mo'; + let coupon; + + beforeEach(async () => { + subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').returnsPromise().resolves({}); + }); + + afterEach(() => { + amzLib.subscribe.restore(); + }); + + it('creates a user subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + await user.post(endpoint, { + billingAgreementId, + subscription, + coupon, + }); + + expect(subscribeWithAmazonStub).to.be.calledOnce; + expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); + expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; + expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); + expect(subscribeWithAmazonStub.args[0][0].groupId).not.exist; + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + + it('creates a group subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + group = await generateGroup(user, { + name: 'test group', + type: 'guild', + privacy: 'public', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + }); + + await user.post(endpoint, { + billingAgreementId, + subscription, + coupon, + groupId: group._id, + }); + + expect(subscribeWithAmazonStub).to.be.calledOnce; + expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); + expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; + expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); + expect(subscribeWithAmazonStub.args[0][0].user._id).to.eql(user._id); + expect(subscribeWithAmazonStub.args[0][0].groupId).to.eql(group._id); + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_verifyAccessToken.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js similarity index 88% rename from test/api/v3/integration/payments/POST-payments_amazon_verifyAccessToken.test.js rename to test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js index 51ccf8c41c..e086091d05 100644 --- a/test/api/v3/integration/payments/POST-payments_amazon_verifyAccessToken.test.js +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js @@ -1,6 +1,6 @@ import { generateUser, -} from '../../../../helpers/api-integration/v3'; +} from '../../../../../helpers/api-integration/v3'; describe('payments : amazon', () => { let endpoint = '/amazon/verifyAccessToken'; diff --git a/test/api/v3/unit/libs/amazonPayments.test.js b/test/api/v3/unit/libs/amazonPayments.test.js new file mode 100644 index 0000000000..0604d3755c --- /dev/null +++ b/test/api/v3/unit/libs/amazonPayments.test.js @@ -0,0 +1,562 @@ +import moment from 'moment'; +import cc from 'coupon-code'; + +import { + generateGroup, +} from '../../../../helpers/api-unit.helper.js'; +import { model as User } from '../../../../../website/server/models/user'; +import { model as Coupon } from '../../../../../website/server/models/coupon'; +import amzLib from '../../../../../website/server/libs/amazonPayments'; +import payments from '../../../../../website/server/libs/payments'; +import common from '../../../../../website/common'; + +const i18n = common.i18n; + +describe('Amazon Payments', () => { + let subKey = 'basic_3mo'; + + describe('checkout', () => { + let user, orderReferenceId, headers; + let setOrderReferenceDetailsSpy; + let confirmOrderReferenceSpy; + let authorizeSpy; + let closeOrderReferenceSpy; + + let paymentBuyGemsStub; + let paymentCreateSubscritionStub; + let amount = 5; + + function expectAmazonStubs () { + expect(setOrderReferenceDetailsSpy).to.be.calledOnce; + expect(setOrderReferenceDetailsSpy).to.be.calledWith({ + AmazonOrderReferenceId: orderReferenceId, + OrderReferenceAttributes: { + OrderTotal: { + CurrencyCode: amzLib.constants.CURRENCY_CODE, + Amount: amount, + }, + SellerNote: amzLib.constants.SELLER_NOTE, + SellerOrderAttributes: { + SellerOrderId: common.uuid(), + StoreName: amzLib.constants.STORE_NAME, + }, + }, + }); + + expect(confirmOrderReferenceSpy).to.be.calledOnce; + expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId }); + + expect(authorizeSpy).to.be.calledOnce; + expect(authorizeSpy).to.be.calledWith({ + AmazonOrderReferenceId: orderReferenceId, + AuthorizationReferenceId: common.uuid().substring(0, 32), + AuthorizationAmount: { + CurrencyCode: amzLib.constants.CURRENCY_CODE, + Amount: amount, + }, + SellerAuthorizationNote: amzLib.constants.SELLER_NOTE, + TransactionTimeout: 0, + CaptureNow: true, + }); + + expect(closeOrderReferenceSpy).to.be.calledOnce; + expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId }); + } + + beforeEach(function () { + user = new User(); + headers = {}; + orderReferenceId = 'orderReferenceId'; + + setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails'); + setOrderReferenceDetailsSpy.returnsPromise().resolves({}); + + confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference'); + confirmOrderReferenceSpy.returnsPromise().resolves({}); + + authorizeSpy = sinon.stub(amzLib, 'authorize'); + authorizeSpy.returnsPromise().resolves({}); + + closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference'); + closeOrderReferenceSpy.returnsPromise().resolves({}); + + paymentBuyGemsStub = sinon.stub(payments, 'buyGems'); + paymentBuyGemsStub.returnsPromise().resolves({}); + + paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription'); + paymentCreateSubscritionStub.returnsPromise().resolves({}); + + sinon.stub(common, 'uuid').returns('uuid-generated'); + }); + + afterEach(function () { + amzLib.setOrderReferenceDetails.restore(); + amzLib.confirmOrderReference.restore(); + amzLib.authorize.restore(); + amzLib.closeOrderReference.restore(); + payments.buyGems.restore(); + payments.createSubscription.restore(); + common.uuid.restore(); + }); + + it('should purchase gems', async () => { + await amzLib.checkout({user, orderReferenceId, headers}); + + expect(paymentBuyGemsStub).to.be.calledOnce; + expect(paymentBuyGemsStub).to.be.calledWith({ + user, + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON, + headers, + }); + expectAmazonStubs(); + }); + + it('should gift gems', async () => { + let receivingUser = new User(); + receivingUser.save(); + let gift = { + type: 'gems', + gems: { + amount: 16, + uuid: receivingUser._id, + }, + }; + amount = 16 / 4; + await amzLib.checkout({gift, user, orderReferenceId, headers}); + + gift.member = receivingUser; + expect(paymentBuyGemsStub).to.be.calledOnce; + expect(paymentBuyGemsStub).to.be.calledWith({ + user, + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT, + headers, + gift, + }); + expectAmazonStubs(); + }); + + it('should gift a subscription', async () => { + let receivingUser = new User(); + receivingUser.save(); + let gift = { + type: 'subscription', + subscription: { + key: subKey, + uuid: receivingUser._id, + }, + }; + amount = common.content.subscriptionBlocks[subKey].price; + + await amzLib.checkout({user, orderReferenceId, headers, gift}); + + gift.member = receivingUser; + expect(paymentCreateSubscritionStub).to.be.calledOnce; + expect(paymentCreateSubscritionStub).to.be.calledWith({ + user, + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT, + headers, + gift, + }); + expectAmazonStubs(); + }); + }); + + describe('subscribe', () => { + let user, group, amount, billingAgreementId, sub, coupon, groupId, headers; + let amazonSetBillingAgreementDetailsSpy; + let amazonConfirmBillingAgreementSpy; + let amazongAuthorizeOnBillingAgreementSpy; + let createSubSpy; + + beforeEach(async () => { + user = new User(); + user.profile.name = 'sender'; + user.purchased.plan.customerId = 'customer-id'; + user.purchased.plan.planId = subKey; + user.purchased.plan.lastBillingDate = new Date(); + + group = generateGroup({ + name: 'test group', + type: 'guild', + privacy: 'public', + leader: user._id, + }); + group.purchased.plan.customerId = 'customer-id'; + group.purchased.plan.planId = subKey; + await group.save(); + + amount = common.content.subscriptionBlocks[subKey].price; + billingAgreementId = 'billingAgreementId'; + sub = { + key: subKey, + price: amount, + }; + groupId = group._id; + headers = {}; + + amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails'); + amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({}); + + amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement'); + amazonConfirmBillingAgreementSpy.returnsPromise().resolves({}); + + amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); + amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({}); + + createSubSpy = sinon.stub(payments, 'createSubscription'); + createSubSpy.returnsPromise().resolves({}); + + sinon.stub(common, 'uuid').returns('uuid-generated'); + }); + + afterEach(function () { + amzLib.setBillingAgreementDetails.restore(); + amzLib.confirmBillingAgreement.restore(); + amzLib.authorizeOnBillingAgreement.restore(); + payments.createSubscription.restore(); + common.uuid.restore(); + }); + + it('should throw an error if we are missing a subscription', async () => { + await expect(amzLib.subscribe({ + billingAgreementId, + coupon, + user, + groupId, + headers, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + name: 'BadRequest', + message: i18n.t('missingSubscriptionCode'), + }); + }); + + it('should throw an error if we are missing a billingAgreementId', async () => { + await expect(amzLib.subscribe({ + sub, + coupon, + user, + groupId, + headers, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + name: 'BadRequest', + message: 'Missing req.body.billingAgreementId', + }); + }); + + it('should throw an error when coupon code is missing', async () => { + sub.discount = 40; + + await expect(amzLib.subscribe({ + billingAgreementId, + sub, + coupon, + user, + groupId, + headers, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + name: 'BadRequest', + message: i18n.t('couponCodeRequired'), + }); + }); + + it('should throw an error when coupon code is invalid', async () => { + sub.discount = 40; + sub.key = 'google_6mo'; + coupon = 'example-coupon'; + + let couponModel = new Coupon(); + couponModel.event = 'google_6mo'; + await couponModel.save(); + + sinon.stub(cc, 'validate').returns('invalid'); + + await expect(amzLib.subscribe({ + billingAgreementId, + sub, + coupon, + user, + groupId, + headers, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('invalidCoupon'), + }); + cc.validate.restore(); + }); + + it('subscribes with amazon with a coupon', async () => { + sub.discount = 40; + sub.key = 'google_6mo'; + coupon = 'example-coupon'; + + let couponModel = new Coupon(); + couponModel.event = 'google_6mo'; + let updatedCouponModel = await couponModel.save(); + + sinon.stub(cc, 'validate').returns(updatedCouponModel._id); + + await amzLib.subscribe({ + billingAgreementId, + sub, + coupon, + user, + groupId, + headers, + }); + + expect(createSubSpy).to.be.calledOnce; + expect(createSubSpy).to.be.calledWith({ + user, + customerId: billingAgreementId, + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON, + sub, + headers, + groupId, + }); + + cc.validate.restore(); + }); + + it('subscribes with amazon', async () => { + await amzLib.subscribe({ + billingAgreementId, + sub, + coupon, + user, + groupId, + headers, + }); + + expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce; + expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + BillingAgreementAttributes: { + SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION, + SellerBillingAgreementAttributes: { + SellerBillingAgreementId: common.uuid(), + StoreName: amzLib.constants.STORE_NAME, + CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION, + }, + }, + }); + + expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce; + expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + + expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce; + expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + AuthorizationReferenceId: common.uuid().substring(0, 32), + AuthorizationAmount: { + CurrencyCode: amzLib.constants.CURRENCY_CODE, + Amount: amount, + }, + SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, + TransactionTimeout: 0, + CaptureNow: true, + SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, + SellerOrderAttributes: { + SellerOrderId: common.uuid(), + StoreName: amzLib.constants.STORE_NAME, + }, + }); + + expect(createSubSpy).to.be.calledOnce; + expect(createSubSpy).to.be.calledWith({ + user, + customerId: billingAgreementId, + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON, + sub, + headers, + groupId, + }); + }); + }); + + describe('cancelSubscription', () => { + let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength; + let getBillingAgreementDetailsSpy; + let paymentCancelSubscriptionSpy; + + function expectAmazonStubs () { + expect(getBillingAgreementDetailsSpy).to.be.calledOnce; + expect(getBillingAgreementDetailsSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + } + + beforeEach(async () => { + user = new User(); + user.profile.name = 'sender'; + user.purchased.plan.customerId = 'customer-id'; + user.purchased.plan.planId = subKey; + user.purchased.plan.lastBillingDate = new Date(); + + group = generateGroup({ + name: 'test group', + type: 'guild', + privacy: 'public', + leader: user._id, + }); + group.purchased.plan.customerId = 'customer-id'; + group.purchased.plan.planId = subKey; + group.purchased.plan.lastBillingDate = new Date(); + await group.save(); + + subscriptionBlock = common.content.subscriptionBlocks[subKey]; + subscriptionLength = subscriptionBlock.months * 30; + + headers = {}; + + getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails'); + getBillingAgreementDetailsSpy.returnsPromise().resolves({ + BillingAgreementDetails: { + BillingAgreementStatus: {State: 'Closed'}, + }, + }); + + paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription'); + paymentCancelSubscriptionSpy.returnsPromise().resolves({}); + }); + + afterEach(function () { + amzLib.getBillingAgreementDetails.restore(); + payments.cancelSubscription.restore(); + }); + + it('should throw an error if we are missing a subscription', async () => { + user.purchased.plan.customerId = undefined; + + await expect(amzLib.cancelSubscription({user})) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('missingSubscription'), + }); + }); + + it('should cancel a user subscription', async () => { + billingAgreementId = user.purchased.plan.customerId; + + await amzLib.cancelSubscription({user, headers}); + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId: undefined, + nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON, + headers, + }); + expectAmazonStubs(); + }); + + it('should close a user subscription if amazon not closed', async () => { + amzLib.getBillingAgreementDetails.restore(); + getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') + .returnsPromise() + .resolves({ + BillingAgreementDetails: { + BillingAgreementStatus: {State: 'Open'}, + }, + }); + let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({}); + billingAgreementId = user.purchased.plan.customerId; + + await amzLib.cancelSubscription({user, headers}); + + expectAmazonStubs(); + expect(closeBillingAgreementSpy).to.be.calledOnce; + expect(closeBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId: undefined, + nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON, + headers, + }); + amzLib.closeBillingAgreement.restore(); + }); + + it('should throw an error if group is not found', async () => { + await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'})) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 404, + name: 'NotFound', + message: i18n.t('groupNotFound'), + }); + }); + + it('should throw an error if user is not group leader', async () => { + let nonLeader = new User(); + nonLeader.guilds.push(group._id); + await nonLeader.save(); + + await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id})) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('onlyGroupLeaderCanManageSubscription'), + }); + }); + + it('should cancel a group subscription', async () => { + billingAgreementId = group.purchased.plan.customerId; + + await amzLib.cancelSubscription({user, groupId: group._id, headers}); + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId: group._id, + nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON, + headers, + }); + expectAmazonStubs(); + }); + + it('should close a group subscription if amazon not closed', async () => { + amzLib.getBillingAgreementDetails.restore(); + getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') + .returnsPromise() + .resolves({ + BillingAgreementDetails: { + BillingAgreementStatus: {State: 'Open'}, + }, + }); + let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({}); + billingAgreementId = group.purchased.plan.customerId; + + await amzLib.cancelSubscription({user, groupId: group._id, headers}); + + expectAmazonStubs(); + expect(closeBillingAgreementSpy).to.be.calledOnce; + expect(closeBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId: group._id, + nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON, + headers, + }); + amzLib.closeBillingAgreement.restore(); + }); + }); +}); diff --git a/test/api/v3/unit/libs/payments.test.js b/test/api/v3/unit/libs/payments.test.js index bab1b6246d..6f021cb005 100644 --- a/test/api/v3/unit/libs/payments.test.js +++ b/test/api/v3/unit/libs/payments.test.js @@ -11,7 +11,6 @@ import { generateGroup, } from '../../../../helpers/api-unit.helper.js'; import i18n from '../../../../../website/common/script/i18n'; -import amzLib from '../../../../../website/server/libs/amazonPayments'; describe('payments/index', () => { let user, group, data, plan; @@ -873,54 +872,4 @@ describe('payments/index', () => { expect(createSubSpy.calledOnce).to.be.true; }); }); - - describe('subscribeWithAmazon', () => { - let amazonSetBillingAgreementDetailsSpy; - let amazonConfirmBillingAgreementSpy; - let amazongAuthorizeOnBillingAgreementSpy; - let createSubSpy; - - beforeEach(function () { - amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails'); - amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({}); - - amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement'); - amazonConfirmBillingAgreementSpy.returnsPromise().resolves({}); - - amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); - amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({}); - - createSubSpy = sinon.stub(api, 'createSubscription'); - createSubSpy.returnsPromise().resolves({}); - }); - - afterEach(function () { - amzLib.setBillingAgreementDetails.restore(); - amzLib.confirmBillingAgreement.restore(); - amzLib.authorizeOnBillingAgreement.restore(); - api.createSubscription.restore(); - }); - - it('subscribes with amazon', async () => { - let billingAgreementId = 'billingAgreementId'; - let sub = data.sub; - let coupon; - let groupId = group._id; - let headers = {}; - - await api.subscribeWithAmazon({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - }); - - expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true; - expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true; - expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true; - expect(createSubSpy.calledOnce).to.be.true; - }); - }); }); diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 8705a321d2..9f859b74f3 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -22,6 +22,7 @@ import { sendNotification as sendPushNotification } from '../../libs/pushNotific import pusher from '../../libs/pusher'; import common from '../../../common'; import payments from '../../libs/payments'; +import amzLib from '../../libs/amazonPayments'; import shared from '../../../common'; @@ -175,7 +176,7 @@ api.createGroupPlan = { let groupId = savedGroup._id; let headers = req.headers; - await payments.subscribeWithAmazon({ + await amzLib.subscribe({ billingAgreementId, sub, coupon, diff --git a/website/server/controllers/top-level/payments/amazon.js b/website/server/controllers/top-level/payments/amazon.js index 0b005835b6..649bca9f4f 100644 --- a/website/server/controllers/top-level/payments/amazon.js +++ b/website/server/controllers/top-level/payments/amazon.js @@ -1,7 +1,5 @@ import { BadRequest, - NotAuthorized, - NotFound, } from '../../../libs/errors'; import amzLib from '../../../libs/amazonPayments'; import { @@ -9,15 +7,6 @@ import { authWithUrl, } from '../../../middlewares/auth'; import shared from '../../../../common'; -import payments from '../../../libs/payments'; -import moment from 'moment'; -import { model as Coupon } from '../../../models/coupon'; -import { model as User } from '../../../models/user'; -import { - model as Group, - basicFields as basicGroupFields, -} from '../../../models/group'; -import cc from 'coupon-code'; let api = {}; @@ -89,67 +78,10 @@ api.checkout = { let gift = req.body.gift; let user = res.locals.user; let orderReferenceId = req.body.orderReferenceId; - let amount = 5; - - // @TODO: Make thise use payment.subscribeWithAmazon if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId'); - if (gift) { - if (gift.type === 'gems') { - amount = gift.gems.amount / 4; - } else if (gift.type === 'subscription') { - amount = shared.content.subscriptionBlocks[gift.subscription.key].price; - } - } - - await amzLib.setOrderReferenceDetails({ - AmazonOrderReferenceId: orderReferenceId, - OrderReferenceAttributes: { - OrderTotal: { - CurrencyCode: 'USD', - Amount: amount, - }, - SellerNote: 'Habitica Payment', - SellerOrderAttributes: { - SellerOrderId: shared.uuid(), - StoreName: 'Habitica', - }, - }, - }); - - await amzLib.confirmOrderReference({ AmazonOrderReferenceId: orderReferenceId }); - - await amzLib.authorize({ - AmazonOrderReferenceId: orderReferenceId, - AuthorizationReferenceId: shared.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: 'USD', - Amount: amount, - }, - SellerAuthorizationNote: 'Habitica Payment', - TransactionTimeout: 0, - CaptureNow: true, - }); - - await amzLib.closeOrderReference({ AmazonOrderReferenceId: orderReferenceId }); - - // execute payment - let method = 'buyGems'; - let data = { - user, - paymentMethod: 'Amazon Payments', - headers: req.headers, - }; - - if (gift) { - if (gift.type === 'subscription') method = 'createSubscription'; - gift.member = await User.findById(gift ? gift.uuid : undefined).exec(); - data.gift = gift; - data.paymentMethod = 'Amazon Payments (Gift)'; - } - - await payments[method](data); + await amzLib.checkout({gift, user, orderReferenceId, headers: req.headers}); res.respond(200); }, @@ -174,55 +106,13 @@ api.subscribe = { let user = res.locals.user; let groupId = req.body.groupId; - if (!sub) throw new BadRequest(res.t('missingSubscriptionCode')); - if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId'); - - if (sub.discount) { // apply discount - if (!coupon) throw new BadRequest(res.t('couponCodeRequired')); - let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); - if (!result) throw new NotAuthorized(res.t('invalidCoupon')); - } - - await amzLib.setBillingAgreementDetails({ - AmazonBillingAgreementId: billingAgreementId, - BillingAgreementAttributes: { - SellerNote: 'Habitica Subscription', - SellerBillingAgreementAttributes: { - SellerBillingAgreementId: shared.uuid(), - StoreName: 'Habitica', - CustomInformation: 'Habitica Subscription', - }, - }, - }); - - await amzLib.confirmBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - }); - - await amzLib.authorizeOnBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - AuthorizationReferenceId: shared.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: 'USD', - Amount: sub.price, - }, - SellerAuthorizationNote: 'Habitica Subscription Payment', - TransactionTimeout: 0, - CaptureNow: true, - SellerNote: 'Habitica Subscription Payment', - SellerOrderAttributes: { - SellerOrderId: shared.uuid(), - StoreName: 'Habitica', - }, - }); - - await payments.createSubscription({ - user, - customerId: billingAgreementId, - paymentMethod: 'Amazon Payments', + await amzLib.subscribe({ + billingAgreementId, sub, - headers: req.headers, + coupon, + user, groupId, + headers: req.headers, }); res.respond(200); @@ -243,53 +133,7 @@ api.subscribeCancel = { let user = res.locals.user; let groupId = req.query.groupId; - let billingAgreementId; - let planId; - let lastBillingDate; - - if (groupId) { - let groupFields = basicGroupFields.concat(' purchased'); - let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); - - if (!group) { - throw new NotFound(res.t('groupNotFound')); - } - - if (!group.leader === user._id) { - throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription')); - } - - billingAgreementId = group.purchased.plan.customerId; - planId = group.purchased.plan.planId; - lastBillingDate = group.purchased.plan.lastBillingDate; - } else { - billingAgreementId = user.purchased.plan.customerId; - planId = user.purchased.plan.planId; - lastBillingDate = user.purchased.plan.lastBillingDate; - } - - if (!billingAgreementId) throw new NotAuthorized(res.t('missingSubscription')); - - let details = await amzLib.getBillingAgreementDetails({ - AmazonBillingAgreementId: billingAgreementId, - }); - - if (details.BillingAgreementDetails.BillingAgreementStatus.State !== 'Closed') { - await amzLib.closeBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - }); - } - - let subscriptionBlock = shared.content.subscriptionBlocks[planId]; - let subscriptionLength = subscriptionBlock.months * 30; - - await payments.cancelSubscription({ - user, - groupId, - nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), - paymentMethod: 'Amazon Payments', - headers: req.headers, - }); + await amzLib.cancelSubscription({user, groupId, headers: req.headers}); if (req.query.noRedirect) { res.respond(200); diff --git a/website/server/libs/amazonPayments.js b/website/server/libs/amazonPayments.js index 40c4592e95..07762e62fc 100644 --- a/website/server/libs/amazonPayments.js +++ b/website/server/libs/amazonPayments.js @@ -1,10 +1,22 @@ import amazonPayments from 'amazon-payments'; import nconf from 'nconf'; -import common from '../../common'; import Bluebird from 'bluebird'; +import moment from 'moment'; +import cc from 'coupon-code'; + +import common from '../../common'; import { BadRequest, + NotAuthorized, + NotFound, } from './errors'; +import payments from './payments'; +import { model as User } from '../models/user'; +import { + model as Group, + basicFields as basicGroupFields, +} from '../models/group'; +import { model as Coupon } from '../models/coupon'; // TODO better handling of errors @@ -19,17 +31,35 @@ let amzPayment = amazonPayments.connect({ clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'), }); -let getTokenInfo = Bluebird.promisify(amzPayment.api.getTokenInfo, {context: amzPayment.api}); -let createOrderReferenceId = Bluebird.promisify(amzPayment.offAmazonPayments.createOrderReferenceForId, {context: amzPayment.offAmazonPayments}); -let setOrderReferenceDetails = Bluebird.promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails, {context: amzPayment.offAmazonPayments}); -let confirmOrderReference = Bluebird.promisify(amzPayment.offAmazonPayments.confirmOrderReference, {context: amzPayment.offAmazonPayments}); -let closeOrderReference = Bluebird.promisify(amzPayment.offAmazonPayments.closeOrderReference, {context: amzPayment.offAmazonPayments}); -let setBillingAgreementDetails = Bluebird.promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails, {context: amzPayment.offAmazonPayments}); -let getBillingAgreementDetails = Bluebird.promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails, {context: amzPayment.offAmazonPayments}); -let confirmBillingAgreement = Bluebird.promisify(amzPayment.offAmazonPayments.confirmBillingAgreement, {context: amzPayment.offAmazonPayments}); -let closeBillingAgreement = Bluebird.promisify(amzPayment.offAmazonPayments.closeBillingAgreement, {context: amzPayment.offAmazonPayments}); +let api = {}; -let authorizeOnBillingAgreement = (inputSet) => { +api.constants = { + CURRENCY_CODE: 'USD', + SELLER_NOTE: 'Habitica Payment', + SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', + SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', + STORE_NAME: 'Habitica', + + GIFT_TYPE_GEMS: 'gems', + GIFT_TYPE_SUBSCRIPTION: 'subscription', + + METHOD_BUY_GEMS: 'buyGems', + METHOD_CREATE_SUBSCRIPTION: 'createSubscription', + PAYMENT_METHOD_AMAZON: 'Amazon Payments', + PAYMENT_METHOD_AMAZON_GIFT: 'Amazon Payments (Gift)', +}; + +api.getTokenInfo = Bluebird.promisify(amzPayment.api.getTokenInfo, {context: amzPayment.api}); +api.createOrderReferenceId = Bluebird.promisify(amzPayment.offAmazonPayments.createOrderReferenceForId, {context: amzPayment.offAmazonPayments}); +api.setOrderReferenceDetails = Bluebird.promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails, {context: amzPayment.offAmazonPayments}); +api.confirmOrderReference = Bluebird.promisify(amzPayment.offAmazonPayments.confirmOrderReference, {context: amzPayment.offAmazonPayments}); +api.closeOrderReference = Bluebird.promisify(amzPayment.offAmazonPayments.closeOrderReference, {context: amzPayment.offAmazonPayments}); +api.setBillingAgreementDetails = Bluebird.promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails, {context: amzPayment.offAmazonPayments}); +api.getBillingAgreementDetails = Bluebird.promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails, {context: amzPayment.offAmazonPayments}); +api.confirmBillingAgreement = Bluebird.promisify(amzPayment.offAmazonPayments.confirmBillingAgreement, {context: amzPayment.offAmazonPayments}); +api.closeBillingAgreement = Bluebird.promisify(amzPayment.offAmazonPayments.closeBillingAgreement, {context: amzPayment.offAmazonPayments}); + +api.authorizeOnBillingAgreement = function authorizeOnBillingAgreement (inputSet) { return new Promise((resolve, reject) => { amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => { if (err) return reject(err); @@ -39,7 +69,7 @@ let authorizeOnBillingAgreement = (inputSet) => { }); }; -let authorize = (inputSet) => { +api.authorize = function authorize (inputSet) { return new Promise((resolve, reject) => { amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => { if (err) return reject(err); @@ -49,16 +79,213 @@ let authorize = (inputSet) => { }); }; -module.exports = { - getTokenInfo, - createOrderReferenceId, - setOrderReferenceDetails, - confirmOrderReference, - closeOrderReference, - confirmBillingAgreement, - getBillingAgreementDetails, - setBillingAgreementDetails, - closeBillingAgreement, - authorizeOnBillingAgreement, - authorize, +/** + * Makes a purchase using Amazon Payment Lib + * + * @param options + * @param options.user The user object who is purchasing + * @param options.gift The gift details if any + * @param options.orderReferenceId The amazon orderReferenceId generated on the front end + * @param options.headers The request headers + * + * @return undefined + */ +api.checkout = async function checkout (options = {}) { + let {gift, user, orderReferenceId, headers} = options; + let amount = 5; + + if (gift) { + if (gift.type === this.constants.GIFT_TYPE_GEMS) { + amount = gift.gems.amount / 4; + } else if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) { + amount = common.content.subscriptionBlocks[gift.subscription.key].price; + } + } + + await this.setOrderReferenceDetails({ + AmazonOrderReferenceId: orderReferenceId, + OrderReferenceAttributes: { + OrderTotal: { + CurrencyCode: this.constants.CURRENCY_CODE, + Amount: amount, + }, + SellerNote: this.constants.SELLER_NOTE, + SellerOrderAttributes: { + SellerOrderId: common.uuid(), + StoreName: this.constants.STORE_NAME, + }, + }, + }); + + await this.confirmOrderReference({ AmazonOrderReferenceId: orderReferenceId }); + + await this.authorize({ + AmazonOrderReferenceId: orderReferenceId, + AuthorizationReferenceId: common.uuid().substring(0, 32), + AuthorizationAmount: { + CurrencyCode: this.constants.CURRENCY_CODE, + Amount: amount, + }, + SellerAuthorizationNote: this.constants.SELLER_NOTE, + TransactionTimeout: 0, + CaptureNow: true, + }); + + await this.closeOrderReference({ AmazonOrderReferenceId: orderReferenceId }); + + // execute payment + let method = this.constants.METHOD_BUY_GEMS; + + let data = { + user, + paymentMethod: this.constants.PAYMENT_METHOD_AMAZON, + headers, + }; + + if (gift) { + if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) method = this.constants.METHOD_CREATE_SUBSCRIPTION; + gift.member = await User.findById(gift ? gift.uuid : undefined).exec(); + data.gift = gift; + data.paymentMethod = this.constants.PAYMENT_METHOD_AMAZON_GIFT; + } + + await payments[method](data); }; + +/** + * Cancel an Amazon Subscription + * + * @param options + * @param options.user The user object who is canceling + * @param options.groupId The id of the group that is canceling + * @param options.headers The request headers + * + * @return undefined + */ +api.cancelSubscription = async function cancelSubscription (options = {}) { + let {user, groupId, headers} = options; + + let billingAgreementId; + let planId; + let lastBillingDate; + + if (groupId) { + let groupFields = basicGroupFields.concat(' purchased'); + let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); + + if (!group) { + throw new NotFound(i18n.t('groupNotFound')); + } + + if (group.leader !== user._id) { + throw new NotAuthorized(i18n.t('onlyGroupLeaderCanManageSubscription')); + } + + billingAgreementId = group.purchased.plan.customerId; + planId = group.purchased.plan.planId; + lastBillingDate = group.purchased.plan.lastBillingDate; + } else { + billingAgreementId = user.purchased.plan.customerId; + planId = user.purchased.plan.planId; + lastBillingDate = user.purchased.plan.lastBillingDate; + } + + if (!billingAgreementId) throw new NotAuthorized(i18n.t('missingSubscription')); + + let details = await this.getBillingAgreementDetails({ + AmazonBillingAgreementId: billingAgreementId, + }); + + if (details.BillingAgreementDetails.BillingAgreementStatus.State !== 'Closed') { + await this.closeBillingAgreement({ + AmazonBillingAgreementId: billingAgreementId, + }); + } + + let subscriptionBlock = common.content.subscriptionBlocks[planId]; + let subscriptionLength = subscriptionBlock.months * 30; + + await payments.cancelSubscription({ + user, + groupId, + nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: this.constants.PAYMENT_METHOD_AMAZON, + headers, + }); +}; + +/** + * Allows for purchasing a user subscription or group subscription with Amazon + * + * @param options + * @param options.billingAgreementId The Amazon billingAgreementId generated on the front end + * @param options.user The user object who is purchasing + * @param options.sub The subscription data to purchase + * @param options.coupon The coupon to discount the sub + * @param options.groupId The id of the group purchasing a subscription + * @param options.headers The request headers to store on analytics + * @return undefined + */ +api.subscribe = async function subscribe (options) { + let { + billingAgreementId, + sub, + coupon, + user, + groupId, + headers, + } = options; + + if (!sub) throw new BadRequest(i18n.t('missingSubscriptionCode')); + if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId'); + + if (sub.discount) { // apply discount + if (!coupon) throw new BadRequest(i18n.t('couponCodeRequired')); + let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); + if (!result) throw new NotAuthorized(i18n.t('invalidCoupon')); + } + + await this.setBillingAgreementDetails({ + AmazonBillingAgreementId: billingAgreementId, + BillingAgreementAttributes: { + SellerNote: this.constants.SELLER_NOTE_SUBSCRIPTION, + SellerBillingAgreementAttributes: { + SellerBillingAgreementId: common.uuid(), + StoreName: this.constants.STORE_NAME, + CustomInformation: this.constants.SELLER_NOTE_SUBSCRIPTION, + }, + }, + }); + + await this.confirmBillingAgreement({ + AmazonBillingAgreementId: billingAgreementId, + }); + + await this.authorizeOnBillingAgreement({ + AmazonBillingAgreementId: billingAgreementId, + AuthorizationReferenceId: common.uuid().substring(0, 32), + AuthorizationAmount: { + CurrencyCode: this.constants.CURRENCY_CODE, + Amount: sub.price, + }, + SellerAuthorizationNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, + TransactionTimeout: 0, + CaptureNow: true, + SellerNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, + SellerOrderAttributes: { + SellerOrderId: common.uuid(), + StoreName: this.constants.STORE_NAME, + }, + }); + + await payments.createSubscription({ + user, + customerId: billingAgreementId, + paymentMethod: this.constants.PAYMENT_METHOD_AMAZON, + sub, + headers, + groupId, + }); +}; + +module.exports = api; diff --git a/website/server/libs/payments.js b/website/server/libs/payments.js index 01338ae7d3..f68e6c9492 100644 --- a/website/server/libs/payments.js +++ b/website/server/libs/payments.js @@ -20,7 +20,6 @@ import { import slack from './slack'; import nconf from 'nconf'; import stripeModule from 'stripe'; -import amzLib from './amazonPayments'; import { BadRequest, } from './errors'; @@ -485,78 +484,4 @@ api.payWithStripe = async function payWithStripe (options, stripeInc) { } }; -/** - * Allows for purchasing a user subscription or group subscription with Amazon - * - * @param options - * @param options.billingAgreementId The Amazon billingAgreementId generated on the front end - * @param options.user The user object who is purchasing - * @param options.sub The subscription data to purchase - * @param options.coupon The coupon to discount the sub - * @param options.groupId The id of the group purchasing a subscription - * @param options.headers The request headers to store on analytics - * @return undefined - */ -api.subscribeWithAmazon = async function subscribeWithAmazon (options) { - let { - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - } = options; - - if (!sub) throw new BadRequest(shared.i18n.t('missingSubscriptionCode')); - if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId'); - - if (sub.discount) { // apply discount - if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); - let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); - if (!result) throw new NotAuthorized(shared.i18n.t('invalidCoupon')); - } - - await amzLib.setBillingAgreementDetails({ - AmazonBillingAgreementId: billingAgreementId, - BillingAgreementAttributes: { - SellerNote: 'Habitica Subscription', - SellerBillingAgreementAttributes: { - SellerBillingAgreementId: shared.uuid(), - StoreName: 'Habitica', - CustomInformation: 'Habitica Subscription', - }, - }, - }); - - await amzLib.confirmBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - }); - - await amzLib.authorizeOnBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - AuthorizationReferenceId: shared.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: 'USD', - Amount: sub.price, - }, - SellerAuthorizationNote: 'Habitica Subscription Payment', - TransactionTimeout: 0, - CaptureNow: true, - SellerNote: 'Habitica Subscription Payment', - SellerOrderAttributes: { - SellerOrderId: shared.uuid(), - StoreName: 'Habitica', - }, - }); - - await this.createSubscription({ - user, - customerId: billingAgreementId, - paymentMethod: 'Amazon Payments', - sub, - headers, - groupId, - }); -}; - module.exports = api;