diff --git a/test/api/v3/integration/payments/GET-payments_paypal_checkout.test.js b/test/api/v3/integration/payments/GET-payments_paypal_checkout.test.js deleted file mode 100644 index 7c692f31d1..0000000000 --- a/test/api/v3/integration/payments/GET-payments_paypal_checkout.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -xdescribe('payments : paypal #checkout', () => { - let endpoint = '/paypal/checkout'; - 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/GET-payments_paypal_checkout_success.test.js b/test/api/v3/integration/payments/GET-payments_paypal_checkout_success.test.js deleted file mode 100644 index 6de04c8848..0000000000 --- a/test/api/v3/integration/payments/GET-payments_paypal_checkout_success.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -xdescribe('payments : paypal #checkoutSuccess', () => { - let endpoint = '/paypal/checkout/success'; - 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/GET-payments_paypal_subscribe.test.js b/test/api/v3/integration/payments/GET-payments_paypal_subscribe.test.js deleted file mode 100644 index 54c540ee39..0000000000 --- a/test/api/v3/integration/payments/GET-payments_paypal_subscribe.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -xdescribe('payments : paypal #subscribe', () => { - let endpoint = '/paypal/subscribe'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', 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/GET-payments_paypal_subscribe_cancel.test.js b/test/api/v3/integration/payments/GET-payments_paypal_subscribe_cancel.test.js deleted file mode 100644 index 1ba8b7af16..0000000000 --- a/test/api/v3/integration/payments/GET-payments_paypal_subscribe_cancel.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -describe('payments : paypal #subscribeCancel', () => { - let endpoint = '/paypal/subscribe/cancel'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', 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/GET-payments_paypal_subscribe_success.test.js b/test/api/v3/integration/payments/GET-payments_paypal_subscribe_success.test.js deleted file mode 100644 index 1a38342e9c..0000000000 --- a/test/api/v3/integration/payments/GET-payments_paypal_subscribe_success.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -xdescribe('payments : paypal #subscribeSuccess', () => { - let endpoint = '/paypal/subscribe/success'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', 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_paypal_ipn.test.js b/test/api/v3/integration/payments/POST-payments_paypal_ipn.test.js deleted file mode 100644 index 219e9ce35b..0000000000 --- a/test/api/v3/integration/payments/POST-payments_paypal_ipn.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - generateUser, -} from '../../../../helpers/api-integration/v3'; - -describe('payments - paypal - #ipn', () => { - let endpoint = '/paypal/ipn'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', async () => { - let result = await user.post(endpoint); - expect(result).to.eql('OK'); - }); -}); diff --git a/test/api/v3/integration/payments/paypal/GET-payments_paypal_checkout.test.js b/test/api/v3/integration/payments/paypal/GET-payments_paypal_checkout.test.js new file mode 100644 index 0000000000..a0da70c4e4 --- /dev/null +++ b/test/api/v3/integration/payments/paypal/GET-payments_paypal_checkout.test.js @@ -0,0 +1,40 @@ +import { + generateUser, +} from '../../../../../helpers/api-integration/v3'; +import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; + +describe('payments : paypal #checkout', () => { + let endpoint = '/paypal/checkout'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + describe('success', () => { + let checkoutStub; + + beforeEach(async () => { + checkoutStub = sinon.stub(paypalPayments, 'checkout').returnsPromise().resolves('/'); + }); + + afterEach(() => { + paypalPayments.checkout.restore(); + }); + + it('creates a purchase link', 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(checkoutStub).to.be.calledOnce; + expect(checkoutStub.args[0][0].gift).to.eql(undefined); + }); + }); +}); diff --git a/test/api/v3/integration/payments/paypal/GET-payments_paypal_checkout_success.test.js b/test/api/v3/integration/payments/paypal/GET-payments_paypal_checkout_success.test.js new file mode 100644 index 0000000000..d42d66c9f8 --- /dev/null +++ b/test/api/v3/integration/payments/paypal/GET-payments_paypal_checkout_success.test.js @@ -0,0 +1,66 @@ +import { + generateUser, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; + +describe('payments : paypal #checkoutSuccess', () => { + let endpoint = '/paypal/checkout/success'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies paymentId', async () => { + await expect(user.get(endpoint)) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingPaymentId'), + }); + }); + + it('verifies customerId', async () => { + await expect(user.get(`${endpoint}?paymentId=test-paymentid`)) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingCustomerId'), + }); + }); + + describe('success', () => { + let checkoutSuccessStub; + + beforeEach(async () => { + checkoutSuccessStub = sinon.stub(paypalPayments, 'checkoutSuccess').returnsPromise().resolves({}); + }); + + afterEach(() => { + paypalPayments.checkoutSuccess.restore(); + }); + + it('makes a purchase', async () => { + let paymentId = 'test-paymentid'; + let customerId = 'test-customerId'; + + 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}?PayerID=${customerId}&paymentId=${paymentId}`); + + expect(checkoutSuccessStub).to.be.calledOnce; + + expect(checkoutSuccessStub.args[0][0].user._id).to.eql(user._id); + expect(checkoutSuccessStub.args[0][0].gift).to.eql(undefined); + expect(checkoutSuccessStub.args[0][0].paymentId).to.eql(paymentId); + expect(checkoutSuccessStub.args[0][0].customerId).to.eql(customerId); + }); + }); +}); diff --git a/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe.test.js b/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe.test.js new file mode 100644 index 0000000000..48b4f4bcb0 --- /dev/null +++ b/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe.test.js @@ -0,0 +1,55 @@ +import { + generateUser, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; +import shared from '../../../../../../website/common'; + +describe('payments : paypal #subscribe', () => { + let endpoint = '/paypal/subscribe'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies sub key', async () => { + await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingSubKey'), + }); + }); + + describe('success', () => { + let subscribeStub; + + beforeEach(async () => { + subscribeStub = sinon.stub(paypalPayments, 'subscribe').returnsPromise().resolves('/'); + }); + + afterEach(() => { + paypalPayments.subscribe.restore(); + }); + + it('makes a purchase', async () => { + let subKey = 'basic_3mo'; + let sub = shared.content.subscriptionBlocks[subKey]; + + 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}?sub=${subKey}`); + + expect(subscribeStub).to.be.calledOnce; + + expect(subscribeStub.args[0][0].sub).to.eql(sub); + expect(subscribeStub.args[0][0].coupon).to.eql(undefined); + }); + }); +}); diff --git a/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe_cancel.test.js b/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe_cancel.test.js new file mode 100644 index 0000000000..d641d8d385 --- /dev/null +++ b/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe_cancel.test.js @@ -0,0 +1,52 @@ +import { + generateUser, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; + +describe('payments : paypal #subscribeCancel', () => { + let endpoint = '/paypal/subscribe/cancel'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies credentials', async () => { + await expect(user.get(endpoint)) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('missingSubscription'), + }); + }); + + describe('success', () => { + let subscribeCancelStub; + + beforeEach(async () => { + subscribeCancelStub = sinon.stub(paypalPayments, 'subscribeCancel').returnsPromise().resolves('/'); + }); + + afterEach(() => { + paypalPayments.subscribeCancel.restore(); + }); + + it('cancels a 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(subscribeCancelStub).to.be.calledOnce; + + expect(subscribeCancelStub.args[0][0].user._id).to.eql(user._id); + expect(subscribeCancelStub.args[0][0].groupId).to.eql(undefined); + }); + }); +}); diff --git a/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe_success.test.js b/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe_success.test.js new file mode 100644 index 0000000000..bf0b564158 --- /dev/null +++ b/test/api/v3/integration/payments/paypal/GET-payments_paypal_subscribe_success.test.js @@ -0,0 +1,56 @@ +import { + generateUser, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; + +describe('payments : paypal #subscribeSuccess', () => { + let endpoint = '/paypal/subscribe/success'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies Paypal Block', async () => { + await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingPaypalBlock'), + }); + }); + + xdescribe('success', () => { + let subscribeSuccessStub; + + beforeEach(async () => { + subscribeSuccessStub = sinon.stub(paypalPayments, 'subscribeSuccess').returnsPromise().resolves({}); + }); + + afterEach(() => { + paypalPayments.subscribeSuccess.restore(); + }); + + it('creates a subscription', async () => { + let token = 'test-token'; + + 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}?token=${token}`); + + expect(subscribeSuccessStub).to.be.calledOnce; + + expect(subscribeSuccessStub.args[0][0].user._id).to.eql(user._id); + expect(subscribeSuccessStub.args[0][0].block).to.eql(undefined); + expect(subscribeSuccessStub.args[0][0].groupId).to.eql(undefined); + expect(subscribeSuccessStub.args[0][0].token).to.eql(token); + expect(subscribeSuccessStub.args[0][0].headers).to.exist; + }); + }); +}); diff --git a/test/api/v3/integration/payments/paypal/POST-payments_paypal_ipn.test.js b/test/api/v3/integration/payments/paypal/POST-payments_paypal_ipn.test.js new file mode 100644 index 0000000000..7202ed3263 --- /dev/null +++ b/test/api/v3/integration/payments/paypal/POST-payments_paypal_ipn.test.js @@ -0,0 +1,44 @@ +import { + generateUser, +} from '../../../../../helpers/api-integration/v3'; +import paypalPayments from '../../../../../../website/server/libs/paypalPayments'; + +describe('payments - paypal - #ipn', () => { + let endpoint = '/paypal/ipn'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies credentials', async () => { + let result = await user.post(endpoint); + expect(result).to.eql('OK'); + }); + + describe('success', () => { + let ipnStub; + + beforeEach(async () => { + ipnStub = sinon.stub(paypalPayments, 'ipn').returnsPromise().resolves({}); + }); + + afterEach(() => { + paypalPayments.ipn.restore(); + }); + + it('makes a purchase', 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); + + expect(ipnStub).to.be.calledOnce; + }); + }); +}); diff --git a/test/api/v3/unit/libs/paypalPayments.test.js b/test/api/v3/unit/libs/paypalPayments.test.js new file mode 100644 index 0000000000..ddc05a92fb --- /dev/null +++ b/test/api/v3/unit/libs/paypalPayments.test.js @@ -0,0 +1,528 @@ +/* eslint-disable camelcase */ +import nconf from 'nconf'; +import moment from 'moment'; +import cc from 'coupon-code'; + +import payments from '../../../../../website/server/libs/payments'; +import paypalPayments from '../../../../../website/server/libs/paypalPayments'; +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 common from '../../../../../website/common'; + +const BASE_URL = nconf.get('BASE_URL'); +const i18n = common.i18n; + +describe('Paypal Payments', () => { + let subKey = 'basic_3mo'; + + describe('checkout', () => { + let paypalPaymentCreateStub; + let approvalHerf; + + function getPaypalCreateOptions (description, amount) { + return { + intent: 'sale', + payer: { payment_method: 'Paypal' }, + redirect_urls: { + return_url: `${BASE_URL}/paypal/checkout/success`, + cancel_url: `${BASE_URL}`, + }, + transactions: [{ + item_list: { + items: [{ + name: description, + price: amount, + currency: 'USD', + quantity: 1, + }], + }, + amount: { + currency: 'USD', + total: amount, + }, + description, + }], + }; + } + + beforeEach(() => { + approvalHerf = 'approval_href'; + paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate') + .returnsPromise().resolves({ + links: [{ rel: 'approval_url', href: approvalHerf }], + }); + }); + + afterEach(() => { + paypalPayments.paypalPaymentCreate.restore(); + }); + + it('creates a link for gem purchases', async () => { + let link = await paypalPayments.checkout(); + + expect(paypalPaymentCreateStub).to.be.calledOnce; + expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00)); + expect(link).to.eql(approvalHerf); + }); + + it('creates a link for gifting gems', async () => { + let receivingUser = new User(); + let gift = { + type: 'gems', + gems: { + amount: 16, + uuid: receivingUser._id, + }, + }; + + let link = await paypalPayments.checkout({gift}); + + expect(paypalPaymentCreateStub).to.be.calledOnce; + expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00')); + expect(link).to.eql(approvalHerf); + }); + + it('creates a link for gifting a subscription', async () => { + let receivingUser = new User(); + receivingUser.save(); + let gift = { + type: 'subscription', + subscription: { + key: subKey, + uuid: receivingUser._id, + }, + }; + + let link = await paypalPayments.checkout({gift}); + + expect(paypalPaymentCreateStub).to.be.calledOnce; + expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00')); + expect(link).to.eql(approvalHerf); + }); + }); + + describe('checkout success', () => { + let user, gift, customerId, paymentId; + let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub; + + beforeEach(() => { + user = new User(); + customerId = 'customerId-test'; + paymentId = 'paymentId-test'; + + paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({}); + paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({}); + paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); + }); + + afterEach(() => { + paypalPayments.paypalPaymentExecute.restore(); + payments.buyGems.restore(); + payments.createSubscription.restore(); + }); + + it('purchases gems', async () => { + await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId}); + + expect(paypalPaymentExecuteStub).to.be.calledOnce; + expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId }); + expect(paymentBuyGemsStub).to.be.calledOnce; + expect(paymentBuyGemsStub).to.be.calledWith({ + user, + customerId, + paymentMethod: 'Paypal', + }); + }); + + it('gifts gems', async () => { + let receivingUser = new User(); + await receivingUser.save(); + gift = { + type: 'gems', + gems: { + amount: 16, + uuid: receivingUser._id, + }, + }; + + await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId}); + + expect(paypalPaymentExecuteStub).to.be.calledOnce; + expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId }); + expect(paymentBuyGemsStub).to.be.calledOnce; + expect(paymentBuyGemsStub).to.be.calledWith({ + user, + customerId, + paymentMethod: 'PayPal (Gift)', + gift, + }); + }); + + it('gifts subscription', async () => { + let receivingUser = new User(); + await receivingUser.save(); + gift = { + type: 'subscription', + subscription: { + key: subKey, + uuid: receivingUser._id, + }, + }; + + await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId}); + + expect(paypalPaymentExecuteStub).to.be.calledOnce; + expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId }); + expect(paymentsCreateSubscritionStub).to.be.calledOnce; + expect(paymentsCreateSubscritionStub).to.be.calledWith({ + user, + customerId, + paymentMethod: 'PayPal (Gift)', + gift, + }); + }); + }); + + describe('subscribe', () => { + let coupon, sub, approvalHerf; + let paypalBillingAgreementCreateStub; + + beforeEach(() => { + approvalHerf = 'approvalHerf-test'; + sub = common.content.subscriptionBlocks[subKey]; + + paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate') + .returnsPromise().resolves({ + links: [{ rel: 'approval_url', href: approvalHerf }], + }); + }); + + afterEach(() => { + paypalPayments.paypalBillingAgreementCreate.restore(); + }); + + it('should throw an error when coupon code is missing', async () => { + sub.discount = 40; + + await expect(paypalPayments.subscribe({sub, coupon})) + .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(paypalPayments.subscribe({sub, coupon})) + .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); + + let link = await paypalPayments.subscribe({sub, coupon}); + + expect(link).to.eql(approvalHerf); + expect(paypalBillingAgreementCreateStub).to.be.calledOnce; + let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`; + expect(paypalBillingAgreementCreateStub).to.be.calledWith({ + name: billingPlanTitle, + description: billingPlanTitle, + start_date: moment().add({ minutes: 5 }).format(), + plan: { + id: sub.paypalKey, + }, + payer: { + payment_method: 'Paypal', + }, + }); + + cc.validate.restore(); + }); + + it('creates a link for a subscription', async () => { + delete sub.discount; + + let link = await paypalPayments.subscribe({sub, coupon}); + + expect(link).to.eql(approvalHerf); + expect(paypalBillingAgreementCreateStub).to.be.calledOnce; + let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`; + expect(paypalBillingAgreementCreateStub).to.be.calledWith({ + name: billingPlanTitle, + description: billingPlanTitle, + start_date: moment().add({ minutes: 5 }).format(), + plan: { + id: sub.paypalKey, + }, + payer: { + payment_method: 'Paypal', + }, + }); + }); + }); + + describe('subscribeSuccess', () => { + let user, group, block, groupId, token, headers, customerId; + let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub; + + beforeEach(async () => { + user = new User(); + + group = generateGroup({ + name: 'test group', + type: 'guild', + privacy: 'public', + leader: user._id, + }); + + token = 'test-token'; + headers = {}; + block = common.content.subscriptionBlocks[subKey]; + customerId = 'test-customerId'; + + paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute') + .returnsPromise({}).resolves({ + id: customerId, + }); + paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); + }); + + afterEach(() => { + paypalPayments.paypalBillingAgreementExecute.restore(); + payments.createSubscription.restore(); + }); + + it('creates a user subscription', async () => { + await paypalPayments.subscribeSuccess({user, block, groupId, token, headers}); + + expect(paypalBillingAgreementExecuteStub).to.be.calledOnce; + expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {}); + + expect(paymentsCreateSubscritionStub).to.be.calledOnce; + expect(paymentsCreateSubscritionStub).to.be.calledWith({ + user, + groupId, + customerId, + paymentMethod: 'Paypal', + sub: block, + headers, + }); + }); + + it('create a group subscription', async () => { + groupId = group._id; + + await paypalPayments.subscribeSuccess({user, block, groupId, token, headers}); + + expect(paypalBillingAgreementExecuteStub).to.be.calledOnce; + expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {}); + + expect(paymentsCreateSubscritionStub).to.be.calledOnce; + expect(paymentsCreateSubscritionStub).to.be.calledWith({ + user, + groupId, + customerId, + paymentMethod: 'Paypal', + sub: block, + headers, + }); + }); + }); + + describe('subscribeCancel', () => { + let user, group, groupId, customerId, groupCustomerId, nextBillingDate; + let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub; + + beforeEach(async () => { + customerId = 'customer-id'; + groupCustomerId = 'groupCustomerId-test'; + + user = new User(); + user.profile.name = 'sender'; + user.purchased.plan.customerId = customerId; + 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 = groupCustomerId; + group.purchased.plan.planId = subKey; + group.purchased.plan.lastBillingDate = new Date(); + await group.save(); + + nextBillingDate = new Date(); + + paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({}); + paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet') + .returnsPromise().resolves({ + agreement_details: { + next_billing_date: nextBillingDate, + cycles_completed: 1, + }, + }); + paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); + }); + + afterEach(function () { + paypalPayments.paypalBillingAgreementGet.restore(); + paypalPayments.paypalBillingAgreementCancel.restore(); + payments.cancelSubscription.restore(); + }); + + it('should throw an error if we are missing a subscription', async () => { + user.purchased.plan.customerId = undefined; + + await expect(paypalPayments.subscribeCancel({user})) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('missingSubscription'), + }); + }); + + it('should throw an error if group is not found', async () => { + await expect(paypalPayments.subscribeCancel({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(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id})) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('onlyGroupLeaderCanManageSubscription'), + }); + }); + + it('should cancel a user subscription', async () => { + await paypalPayments.subscribeCancel({user}); + + expect(paypalBillingAgreementGetStub).to.be.calledOnce; + expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId); + expect(paypalBillingAgreementCancelStub).to.be.calledOnce; + expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') }); + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId, + paymentMethod: 'Paypal', + nextBill: nextBillingDate, + }); + }); + + it('should cancel a group subscription', async () => { + await paypalPayments.subscribeCancel({user, groupId: group._id}); + + expect(paypalBillingAgreementGetStub).to.be.calledOnce; + expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId); + expect(paypalBillingAgreementCancelStub).to.be.calledOnce; + expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') }); + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId: group._id, + paymentMethod: 'Paypal', + nextBill: nextBillingDate, + }); + }); + }); + + describe('ipn', () => { + let user, group, txn_type, userPaymentId, groupPaymentId; + let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy; + + beforeEach(async () => { + txn_type = 'recurring_payment_profile_cancel'; + userPaymentId = 'userPaymentId-test'; + groupPaymentId = 'groupPaymentId-test'; + + user = new User(); + user.profile.name = 'sender'; + user.purchased.plan.customerId = userPaymentId; + user.purchased.plan.planId = subKey; + user.purchased.plan.lastBillingDate = new Date(); + await user.save(); + + group = generateGroup({ + name: 'test group', + type: 'guild', + privacy: 'public', + leader: user._id, + }); + group.purchased.plan.customerId = groupPaymentId; + group.purchased.plan.planId = subKey; + group.purchased.plan.lastBillingDate = new Date(); + await group.save(); + + ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({}); + paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); + }); + + afterEach(function () { + paypalPayments.ipnVerifyAsync.restore(); + payments.cancelSubscription.restore(); + }); + + it('should cancel a user subscription', async () => { + await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId}); + + expect(ipnVerifyAsyncStub).to.be.calledOnce; + expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId}); + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id); + expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal'); + }); + + it('should cancel a group subscription', async () => { + await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId}); + + expect(ipnVerifyAsyncStub).to.be.calledOnce; + expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId}); + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' }); + }); + }); +}); diff --git a/website/common/locales/en/subscriber.json b/website/common/locales/en/subscriber.json index 3ebf1481b3..113218042c 100644 --- a/website/common/locales/en/subscriber.json +++ b/website/common/locales/en/subscriber.json @@ -154,5 +154,9 @@ "noSudoAccess": "You don't have sudo access.", "couponCodeRequired": "The coupon code is required.", "eventRequired": "\"req.params.event\" is required.", - "countRequired": "\"req.query.count\" is required." + "countRequired": "\"req.query.count\" is required.", + "missingPaymentId": "Missing req.query.paymentId", + "missingCustomerId": "Missing req.query.customerId", + "missingPaypalBlock": "Missing req.session.paypalBlock", + "missingSubKey": "Missing req.query.sub" } diff --git a/website/server/controllers/top-level/payments/paypal.js b/website/server/controllers/top-level/payments/paypal.js index 8b695555d1..6c3c110217 100644 --- a/website/server/controllers/top-level/payments/paypal.js +++ b/website/server/controllers/top-level/payments/paypal.js @@ -1,54 +1,15 @@ /* eslint-disable camelcase */ - -import nconf from 'nconf'; -import moment from 'moment'; -import _ from 'lodash'; -import payments from '../../../libs/payments'; -import ipn from 'paypal-ipn'; -import paypal from 'paypal-rest-sdk'; +import paypalPayments from '../../../libs/paypalPayments'; import shared from '../../../../common'; -import cc from 'coupon-code'; -import Bluebird from 'bluebird'; -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 { authWithUrl, authWithSession, } from '../../../middlewares/auth'; import { BadRequest, - NotAuthorized, - NotFound, } from '../../../libs/errors'; -const BASE_URL = nconf.get('BASE_URL'); - -// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have -// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created -// there, get it's plan.id and store it in config.json -_.each(shared.content.subscriptionBlocks, (block) => { - block.paypalKey = nconf.get(`PAYPAL:billing_plans:${block.key}`); -}); - -paypal.configure({ - mode: nconf.get('PAYPAL:mode'), // sandbox or live - client_id: nconf.get('PAYPAL:client_id'), - client_secret: nconf.get('PAYPAL:client_secret'), -}); - -// TODO better handling of errors -const paypalPaymentCreate = Bluebird.promisify(paypal.payment.create, {context: paypal.payment}); -const paypalPaymentExecute = Bluebird.promisify(paypal.payment.execute, {context: paypal.payment}); -const paypalBillingAgreementCreate = Bluebird.promisify(paypal.billingAgreement.create, {context: paypal.billingAgreement}); -const paypalBillingAgreementExecute = Bluebird.promisify(paypal.billingAgreement.execute, {context: paypal.billingAgreement}); -const paypalBillingAgreementGet = Bluebird.promisify(paypal.billingAgreement.get, {context: paypal.billingAgreement}); -const paypalBillingAgreementCancel = Bluebird.promisify(paypal.billingAgreement.cancel, {context: paypal.billingAgreement}); - -const ipnVerifyAsync = Bluebird.promisify(ipn.verify, {context: ipn}); +const i18n = shared.i18n; let api = {}; @@ -66,45 +27,8 @@ api.checkout = { let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined; req.session.gift = req.query.gift; - let amount = 5.00; - let description = 'Habitica Gems'; - if (gift) { - if (gift.type === 'gems') { - amount = Number(gift.gems.amount / 4).toFixed(2); - description = `${description} (Gift)`; - } else { - amount = Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2); - description = 'mo. Habitica Subscription (Gift)'; - } - } + let link = await paypalPayments.checkout({gift}); - let createPayment = { - intent: 'sale', - payer: { payment_method: 'Paypal' }, - redirect_urls: { - return_url: `${BASE_URL}/paypal/checkout/success`, - cancel_url: `${BASE_URL}`, - }, - transactions: [{ - item_list: { - items: [{ - name: description, - // sku: 1, - price: amount, - currency: 'USD', - quantity: 1, - }], - }, - amount: { - currency: 'USD', - total: amount, - }, - description, - }], - }; - - let result = await paypalPaymentCreate(createPayment); - let link = _.find(result.links, { rel: 'approval_url' }).href; res.redirect(link); }, }; @@ -122,29 +46,15 @@ api.checkoutSuccess = { async handler (req, res) { let paymentId = req.query.paymentId; let customerId = req.query.PayerID; - - let method = 'buyGems'; - let data = { - user: res.locals.user, - customerId, - paymentMethod: 'Paypal', - }; - + let user = res.locals.user; let gift = req.session.gift ? JSON.parse(req.session.gift) : undefined; delete req.session.gift; - if (gift) { - gift.member = await User.findById(gift.uuid).exec(); - if (gift.type === 'subscription') { - method = 'createSubscription'; - } + if (!paymentId) throw new BadRequest(i18n.t('missingPaymentId')); + if (!customerId) throw new BadRequest(i18n.t('missingCustomerId')); - data.paymentMethod = 'PayPal (Gift)'; - data.gift = gift; - } + await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId}); - await paypalPaymentExecute(paymentId, { payer_id: customerId }); - await payments[method](data); res.redirect('/'); }, }; @@ -160,31 +70,16 @@ api.subscribe = { url: '/paypal/subscribe', middlewares: [authWithUrl], async handler (req, res) { + if (!req.query.sub) throw new BadRequest(i18n.t('missingSubKey')); + let sub = shared.content.subscriptionBlocks[req.query.sub]; + let coupon = req.query.coupon; - if (sub.discount) { - if (!req.query.coupon) throw new BadRequest(res.t('couponCodeRequired')); - let coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key}).exec(); - if (!coupon) throw new NotAuthorized(res.t('invalidCoupon')); - } - - let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`; - let billingAgreementAttributes = { - name: billingPlanTitle, - description: billingPlanTitle, - start_date: moment().add({ minutes: 5 }).format(), - plan: { - id: sub.paypalKey, - }, - payer: { - payment_method: 'Paypal', - }, - }; - let billingAgreement = await paypalBillingAgreementCreate(billingAgreementAttributes); + let link = await paypalPayments.subscribe({sub, coupon}); req.session.paypalBlock = req.query.sub; req.session.groupId = req.query.groupId; - let link = _.find(billingAgreement.links, { rel: 'approval_url' }).href; + res.redirect(link); }, }; @@ -201,21 +96,17 @@ api.subscribeSuccess = { middlewares: [authWithSession], async handler (req, res) { let user = res.locals.user; + + if (!req.session.paypalBlock) throw new BadRequest(i18n.t('missingPaypalBlock')); + let block = shared.content.subscriptionBlocks[req.session.paypalBlock]; let groupId = req.session.groupId; + let token = req.query.token; delete req.session.paypalBlock; delete req.session.groupId; - let result = await paypalBillingAgreementExecute(req.query.token, {}); - await payments.createSubscription({ - user, - groupId, - customerId: result.id, - paymentMethod: 'Paypal', - sub: block, - headers: req.headers, - }); + await paypalPayments.subscribeSuccess({user, block, groupId, token, headers: req.headers}); res.redirect('/'); }, @@ -235,39 +126,7 @@ api.subscribeCancel = { let user = res.locals.user; let groupId = req.query.groupId; - let customerId; - 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')); - } - customerId = group.purchased.plan.customerId; - } else { - customerId = user.purchased.plan.customerId; - } - - if (!customerId) throw new NotAuthorized(res.t('missingSubscription')); - - let customer = await paypalBillingAgreementGet(customerId); - - let nextBillingDate = customer.agreement_details.next_billing_date; - if (customer.agreement_details.cycles_completed === '0') { // hasn't billed yet - throw new BadRequest(res.t('planNotActive', { nextBillingDate })); - } - - await paypalBillingAgreementCancel(customerId, { note: res.t('cancelingSubscription') }); - await payments.cancelSubscription({ - user, - groupId, - paymentMethod: 'Paypal', - nextBill: nextBillingDate, - }); + await paypalPayments.subscribeCancel({user, groupId}); res.redirect('/'); }, @@ -288,25 +147,7 @@ api.ipn = { async handler (req, res) { res.sendStatus(200); - await ipnVerifyAsync(req.body); - - if (req.body.txn_type === 'recurring_payment_profile_cancel' || req.body.txn_type === 'subscr_cancel') { - let user = await User.findOne({ 'purchased.plan.customerId': req.body.recurring_payment_id }).exec(); - if (user) { - await payments.cancelSubscription({ user, paymentMethod: 'Paypal' }); - return; - } - - let groupFields = basicGroupFields.concat(' purchased'); - let group = await Group - .findOne({ 'purchased.plan.customerId': req.body.recurring_payment_id }) - .select(groupFields) - .exec(); - - if (group) { - await payments.cancelSubscription({ groupId: group._id, paymentMethod: 'Paypal' }); - } - } + await paypalPayments.ipn(req.body); }, }; diff --git a/website/server/libs/paypalPayments.js b/website/server/libs/paypalPayments.js new file mode 100644 index 0000000000..4f48b7ee46 --- /dev/null +++ b/website/server/libs/paypalPayments.js @@ -0,0 +1,226 @@ +/* eslint-disable camelcase */ +import nconf from 'nconf'; +import moment from 'moment'; +import _ from 'lodash'; +import payments from './payments'; +import ipn from 'paypal-ipn'; +import paypal from 'paypal-rest-sdk'; +import shared from '../../common'; +import cc from 'coupon-code'; +import Bluebird from 'bluebird'; +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 { + BadRequest, + NotAuthorized, + NotFound, +} from './errors'; + + +const BASE_URL = nconf.get('BASE_URL'); +const i18n = shared.i18n; + +// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have +// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created +// there, get it's plan.id and store it in config.json +_.each(shared.content.subscriptionBlocks, (block) => { + block.paypalKey = nconf.get(`PAYPAL:billing_plans:${block.key}`); +}); + +paypal.configure({ + mode: nconf.get('PAYPAL:mode'), // sandbox or live + client_id: nconf.get('PAYPAL:client_id'), + client_secret: nconf.get('PAYPAL:client_secret'), +}); + +// TODO better handling of errors +// @TODO: Create constants + +let api = {}; + +api.paypalPaymentCreate = Bluebird.promisify(paypal.payment.create, {context: paypal.payment}); +api.paypalPaymentExecute = Bluebird.promisify(paypal.payment.execute, {context: paypal.payment}); +api.paypalBillingAgreementCreate = Bluebird.promisify(paypal.billingAgreement.create, {context: paypal.billingAgreement}); +api.paypalBillingAgreementExecute = Bluebird.promisify(paypal.billingAgreement.execute, {context: paypal.billingAgreement}); +api.paypalBillingAgreementGet = Bluebird.promisify(paypal.billingAgreement.get, {context: paypal.billingAgreement}); +api.paypalBillingAgreementCancel = Bluebird.promisify(paypal.billingAgreement.cancel, {context: paypal.billingAgreement}); + +api.ipnVerifyAsync = Bluebird.promisify(ipn.verify, {context: ipn}); + +api.checkout = async function checkout (options = {}) { + let {gift} = options; + + let amount = 5.00; + let description = 'Habitica Gems'; + if (gift) { + if (gift.type === 'gems') { + amount = Number(gift.gems.amount / 4).toFixed(2); + description = `${description} (Gift)`; + } else { + amount = Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2); + description = 'mo. Habitica Subscription (Gift)'; + } + } + + let createPayment = { + intent: 'sale', + payer: { payment_method: 'Paypal' }, + redirect_urls: { + return_url: `${BASE_URL}/paypal/checkout/success`, + cancel_url: `${BASE_URL}`, + }, + transactions: [{ + item_list: { + items: [{ + name: description, + // sku: 1, + price: amount, + currency: 'USD', + quantity: 1, + }], + }, + amount: { + currency: 'USD', + total: amount, + }, + description, + }], + }; + + let result = await this.paypalPaymentCreate(createPayment); + let link = _.find(result.links, { rel: 'approval_url' }).href; + return link; +}; + +api.checkoutSuccess = async function checkoutSuccess (options = {}) { + let {user, gift, paymentId, customerId} = options; + + let method = 'buyGems'; + let data = { + user, + customerId, + paymentMethod: 'Paypal', + }; + + if (gift) { + gift.member = await User.findById(gift.uuid).exec(); + if (gift.type === 'subscription') { + method = 'createSubscription'; + } + + data.paymentMethod = 'PayPal (Gift)'; + data.gift = gift; + } + + await this.paypalPaymentExecute(paymentId, { payer_id: customerId }); + await payments[method](data); +}; + +api.subscribe = async function subscribe (options = {}) { + let {sub, coupon} = options; + + if (sub.discount) { + if (!coupon) throw new BadRequest(i18n.t('couponCodeRequired')); + let couponResult = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); + if (!couponResult) throw new NotAuthorized(i18n.t('invalidCoupon')); + } + + let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`; + let billingAgreementAttributes = { + name: billingPlanTitle, + description: billingPlanTitle, + start_date: moment().add({ minutes: 5 }).format(), + plan: { + id: sub.paypalKey, + }, + payer: { + payment_method: 'Paypal', + }, + }; + let billingAgreement = await this.paypalBillingAgreementCreate(billingAgreementAttributes); + + let link = _.find(billingAgreement.links, { rel: 'approval_url' }).href; + return link; +}; + +api.subscribeSuccess = async function subscribeSuccess (options = {}) { + let {user, groupId, block, headers, token} = options; + let result = await this.paypalBillingAgreementExecute(token, {}); + await payments.createSubscription({ + user, + groupId, + customerId: result.id, + paymentMethod: 'Paypal', + sub: block, + headers, + }); +}; + +api.subscribeCancel = async function subscribeCancel (options = {}) { + let {groupId, user} = options; + + let customerId; + 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')); + } + customerId = group.purchased.plan.customerId; + } else { + customerId = user.purchased.plan.customerId; + } + + if (!customerId) throw new NotAuthorized(i18n.t('missingSubscription')); + + let customer = await this.paypalBillingAgreementGet(customerId); + + // @TODO: Handle error response + let nextBillingDate = customer.agreement_details.next_billing_date; + if (customer.agreement_details.cycles_completed === '0') { // hasn't billed yet + throw new BadRequest(i18n.t('planNotActive', { nextBillingDate })); + } + + await this.paypalBillingAgreementCancel(customerId, { note: i18n.t('cancelingSubscription') }); + await payments.cancelSubscription({ + user, + groupId, + paymentMethod: 'Paypal', + nextBill: nextBillingDate, + }); +}; + +api.ipn = async function ipnApi (options = {}) { + await this.ipnVerifyAsync(options); + + let {txn_type, recurring_payment_id} = options; + + if (['recurring_payment_profile_cancel', 'subscr_cancel'].indexOf(txn_type) === -1) return; + // @TODO: Should this request billing date? + let user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec(); + if (user) { + await payments.cancelSubscription({ user, paymentMethod: 'Paypal' }); + return; + } + + let groupFields = basicGroupFields.concat(' purchased'); + let group = await Group + .findOne({ 'purchased.plan.customerId': recurring_payment_id }) + .select(groupFields) + .exec(); + + if (group) { + await payments.cancelSubscription({ groupId: group._id, paymentMethod: 'Paypal' }); + } +}; + +module.exports = api;