diff --git a/test/api/v3/integration/payments/GET-payments_stripe_subscribe_cancel.test.js b/test/api/v3/integration/payments/GET-payments_stripe_subscribe_cancel.test.js deleted file mode 100644 index 6d7ac87d0f..0000000000 --- a/test/api/v3/integration/payments/GET-payments_stripe_subscribe_cancel.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -describe('payments - stripe - #subscribeCancel', () => { - let endpoint = '/stripe/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/POST-payments_stripe_checkout.test.js b/test/api/v3/integration/payments/POST-payments_stripe_checkout.test.js deleted file mode 100644 index 1443a3af74..0000000000 --- a/test/api/v3/integration/payments/POST-payments_stripe_checkout.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - generateUser, -} from '../../../../helpers/api-integration/v3'; - -describe('payments - stripe - #checkout', () => { - let endpoint = '/stripe/checkout'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', async () => { - await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'Error', - message: 'Invalid API Key provided: ****************************1111', - }); - }); -}); diff --git a/test/api/v3/integration/payments/POST-payments_stripe_subscribe_edit.test.js b/test/api/v3/integration/payments/POST-payments_stripe_subscribe_edit.test.js deleted file mode 100644 index d6d568ace4..0000000000 --- a/test/api/v3/integration/payments/POST-payments_stripe_subscribe_edit.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; - -describe('payments - stripe - #subscribeEdit', () => { - let endpoint = '/stripe/subscribe/edit'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('missingSubscription'), - }); - }); -}); diff --git a/test/api/v3/integration/payments/stripe/GET-payments_stripe_subscribe_cancel.test.js b/test/api/v3/integration/payments/stripe/GET-payments_stripe_subscribe_cancel.test.js new file mode 100644 index 0000000000..82259ac195 --- /dev/null +++ b/test/api/v3/integration/payments/stripe/GET-payments_stripe_subscribe_cancel.test.js @@ -0,0 +1,74 @@ +import { + generateUser, + generateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import stripePayments from '../../../../../../website/server/libs/stripePayments'; + +describe('payments - stripe - #subscribeCancel', () => { + let endpoint = '/stripe/subscribe/cancel'; + let user, group, stripeCancelSubscriptionStub; + + 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', () => { + beforeEach(async () => { + stripeCancelSubscriptionStub = sinon.stub(stripePayments, 'cancelSubscription').returnsPromise().resolves({}); + }); + + afterEach(() => { + stripePayments.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}?redirect=none`); + + expect(stripeCancelSubscriptionStub).to.be.calledOnce; + expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id); + expect(stripeCancelSubscriptionStub.args[0][0].groupId).to.eql(undefined); + }); + + 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}&redirect=none`); + + expect(stripeCancelSubscriptionStub).to.be.calledOnce; + expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id); + expect(stripeCancelSubscriptionStub.args[0][0].groupId).to.eql(group._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/stripe/POST-payments_stripe_checkout.test.js b/test/api/v3/integration/payments/stripe/POST-payments_stripe_checkout.test.js new file mode 100644 index 0000000000..7b59930c87 --- /dev/null +++ b/test/api/v3/integration/payments/stripe/POST-payments_stripe_checkout.test.js @@ -0,0 +1,75 @@ +import { + generateUser, + generateGroup, +} from '../../../../../helpers/api-integration/v3'; +import stripePayments from '../../../../../../website/server/libs/stripePayments'; + +describe('payments - stripe - #checkout', () => { + let endpoint = '/stripe/checkout'; + let user, group; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies credentials', async () => { + await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'Error', + message: 'Invalid API Key provided: ****************************1111', + }); + }); + + describe('success', () => { + let stripeCheckoutSubscriptionStub; + + beforeEach(async () => { + stripeCheckoutSubscriptionStub = sinon.stub(stripePayments, 'checkout').returnsPromise().resolves({}); + }); + + afterEach(() => { + stripePayments.checkout.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.post(endpoint); + + expect(stripeCheckoutSubscriptionStub).to.be.calledOnce; + expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id); + expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(undefined); + }); + + 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.post(`${endpoint}?groupId=${group._id}`); + + expect(stripeCheckoutSubscriptionStub).to.be.calledOnce; + expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id); + expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(group._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/stripe/POST-payments_stripe_subscribe_edit.test.js b/test/api/v3/integration/payments/stripe/POST-payments_stripe_subscribe_edit.test.js new file mode 100644 index 0000000000..4351077fdb --- /dev/null +++ b/test/api/v3/integration/payments/stripe/POST-payments_stripe_subscribe_edit.test.js @@ -0,0 +1,78 @@ +import { + generateUser, + generateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import stripePayments from '../../../../../../website/server/libs/stripePayments'; + +describe('payments - stripe - #subscribeEdit', () => { + let endpoint = '/stripe/subscribe/edit'; + let user, group; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies credentials', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('missingSubscription'), + }); + }); + + describe('success', () => { + let stripeEditSubscriptionStub; + + beforeEach(async () => { + stripeEditSubscriptionStub = sinon.stub(stripePayments, 'editSubscription').returnsPromise().resolves({}); + }); + + afterEach(() => { + stripePayments.editSubscription.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.post(endpoint); + + expect(stripeEditSubscriptionStub).to.be.calledOnce; + expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id); + expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(undefined); + }); + + 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.post(endpoint, { + groupId: group._id, + }); + + expect(stripeEditSubscriptionStub).to.be.calledOnce; + expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id); + expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(group._id); + }); + }); +}); diff --git a/test/api/v3/unit/libs/payments.test.js b/test/api/v3/unit/libs/payments.test.js index 6f021cb005..7ba4fbc1a5 100644 --- a/test/api/v3/unit/libs/payments.test.js +++ b/test/api/v3/unit/libs/payments.test.js @@ -817,59 +817,4 @@ describe('payments/index', () => { expect(updatedGroup.purchased.plan.quantity).to.eql(3); }); }); - - describe('payWithStripe', () => { - let spy; - let stripeCreateCustomerSpy; - let createSubSpy; - - beforeEach(function () { - spy = sinon.stub(stripe.subscriptions, 'update'); - spy.returnsPromise().resolves; - - stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create'); - let stripCustomerResponse = { - subscriptions: { - data: [{id: 'test-id'}], - }, - }; - stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse); - - createSubSpy = sinon.stub(api, 'createSubscription'); - createSubSpy.returnsPromise().resolves({}); - - data.groupId = group._id; - data.sub.quantity = 3; - }); - - afterEach(function () { - sinon.restore(stripe.subscriptions.update); - stripe.customers.create.restore(); - api.createSubscription.restore(); - }); - - it('subscribes with stripe', async () => { - let token = 'test-token'; - let gift; - let sub = data.sub; - let groupId = group._id; - let email = 'test@test.com'; - let headers = {}; - let coupon; - - await api.payWithStripe({ - token, - user, - gift, - sub, - groupId, - email, - headers, - coupon, - }, stripe); - - expect(stripeCreateCustomerSpy.calledOnce).to.be.true; - expect(createSubSpy.calledOnce).to.be.true; - }); - }); }); diff --git a/test/api/v3/unit/libs/stripePayments.test.js b/test/api/v3/unit/libs/stripePayments.test.js new file mode 100644 index 0000000000..e1ff3cdf3d --- /dev/null +++ b/test/api/v3/unit/libs/stripePayments.test.js @@ -0,0 +1,661 @@ +import stripeModule from 'stripe'; +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 stripePayments from '../../../../../website/server/libs/stripePayments'; +import payments from '../../../../../website/server/libs/payments'; +import common from '../../../../../website/common'; + +const i18n = common.i18n; + +describe('Stripe Payments', () => { + let subKey = 'basic_3mo'; + let stripe = stripeModule('test'); + + describe('checkout', () => { + let stripeChargeStub, paymentBuyGemsStub, paymentCreateSubscritionStub; + let user, gift, groupId, email, headers, coupon, customerIdResponse, token; + + beforeEach(() => { + user = new User(); + user.profile.name = 'sender'; + user.purchased.plan.customerId = 'customer-id'; + user.purchased.plan.planId = subKey; + user.purchased.plan.lastBillingDate = new Date(); + + token = 'test-token'; + + customerIdResponse = 'example-customerIdResponse'; + let stripCustomerResponse = { + id: customerIdResponse, + }; + stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse); + paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({}); + paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); + }); + + afterEach(() => { + stripe.charges.create.restore(); + payments.buyGems.restore(); + payments.createSubscription.restore(); + }); + + it('should purchase gems', async () => { + await stripePayments.checkout({ + token, + user, + gift, + groupId, + email, + headers, + coupon, + }, stripe); + + expect(stripeChargeStub).to.be.calledOnce; + expect(stripeChargeStub).to.be.calledWith({ + amount: 500, + currency: 'usd', + card: token, + }); + + expect(paymentBuyGemsStub).to.be.calledOnce; + expect(paymentBuyGemsStub).to.be.calledWith({ + user, + customerId: customerIdResponse, + paymentMethod: 'Stripe', + gift, + }); + }); + + it('should gift gems', async () => { + let receivingUser = new User(); + receivingUser.save(); + gift = { + type: 'gems', + gems: { + amount: 16, + uuid: receivingUser._id, + }, + }; + + await stripePayments.checkout({ + token, + user, + gift, + groupId, + email, + headers, + coupon, + }, stripe); + + gift.member = receivingUser; + expect(stripeChargeStub).to.be.calledOnce; + expect(stripeChargeStub).to.be.calledWith({ + amount: '400', + currency: 'usd', + card: token, + }); + + expect(paymentBuyGemsStub).to.be.calledOnce; + expect(paymentBuyGemsStub).to.be.calledWith({ + user, + customerId: customerIdResponse, + paymentMethod: 'Gift', + gift, + }); + }); + + it('should gift a subscription', async () => { + let receivingUser = new User(); + receivingUser.save(); + gift = { + type: 'subscription', + subscription: { + key: subKey, + uuid: receivingUser._id, + }, + }; + + await stripePayments.checkout({ + token, + user, + gift, + groupId, + email, + headers, + coupon, + }, stripe); + + gift.member = receivingUser; + expect(stripeChargeStub).to.be.calledOnce; + expect(stripeChargeStub).to.be.calledWith({ + amount: '1500', + currency: 'usd', + card: token, + }); + + expect(paymentCreateSubscritionStub).to.be.calledOnce; + expect(paymentCreateSubscritionStub).to.be.calledWith({ + user, + customerId: customerIdResponse, + paymentMethod: 'Gift', + gift, + }); + }); + }); + + describe('checkout with subscription', () => { + let user, group, data, gift, sub, groupId, email, headers, coupon, customerIdResponse, subscriptionId, token; + let spy; + let stripeCreateCustomerSpy; + let stripePaymentsCreateSubSpy; + + 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(); + + sub = { + key: 'basic_3mo', + }; + + data = { + user, + sub, + customerId: 'customer-id', + paymentMethod: 'Payment Method', + }; + + email = 'example@example.com'; + customerIdResponse = 'test-id'; + subscriptionId = 'test-sub-id'; + token = 'test-token'; + + spy = sinon.stub(stripe.subscriptions, 'update'); + spy.returnsPromise().resolves; + + stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create'); + let stripCustomerResponse = { + id: customerIdResponse, + subscriptions: { + data: [{id: subscriptionId}], + }, + }; + stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse); + + stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription'); + stripePaymentsCreateSubSpy.returnsPromise().resolves({}); + + data.groupId = group._id; + data.sub.quantity = 3; + }); + + afterEach(function () { + sinon.restore(stripe.subscriptions.update); + stripe.customers.create.restore(); + payments.createSubscription.restore(); + }); + + it('should throw an error if we are missing a token', async () => { + await expect(stripePayments.checkout({ + user, + gift, + sub, + groupId, + email, + headers, + coupon, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + name: 'BadRequest', + message: 'Missing req.body.id', + }); + }); + + it('should throw an error when coupon code is missing', async () => { + sub.discount = 40; + + await expect(stripePayments.checkout({ + token, + user, + gift, + sub, + groupId, + email, + headers, + 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(stripePayments.checkout({ + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + name: 'BadRequest', + 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 stripePayments.checkout({ + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + }, stripe); + + expect(stripeCreateCustomerSpy).to.be.calledOnce; + expect(stripeCreateCustomerSpy).to.be.calledWith({ + email, + metadata: { uuid: user._id }, + card: token, + plan: sub.key, + }); + + expect(stripePaymentsCreateSubSpy).to.be.calledOnce; + expect(stripePaymentsCreateSubSpy).to.be.calledWith({ + user, + customerId: customerIdResponse, + paymentMethod: 'Stripe', + sub, + headers, + groupId: undefined, + subscriptionId: undefined, + }); + + cc.validate.restore(); + }); + + it('subscribes a user', async () => { + sub = data.sub; + + await stripePayments.checkout({ + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + }, stripe); + + expect(stripeCreateCustomerSpy).to.be.calledOnce; + expect(stripeCreateCustomerSpy).to.be.calledWith({ + email, + metadata: { uuid: user._id }, + card: token, + plan: sub.key, + }); + + expect(stripePaymentsCreateSubSpy).to.be.calledOnce; + expect(stripePaymentsCreateSubSpy).to.be.calledWith({ + user, + customerId: customerIdResponse, + paymentMethod: 'Stripe', + sub, + headers, + groupId: undefined, + subscriptionId: undefined, + }); + }); + + it('subscribes a group', async () => { + token = 'test-token'; + sub = data.sub; + groupId = group._id; + email = 'test@test.com'; + headers = {}; + + await stripePayments.checkout({ + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + }, stripe); + + expect(stripeCreateCustomerSpy).to.be.calledOnce; + expect(stripeCreateCustomerSpy).to.be.calledWith({ + email, + metadata: { uuid: user._id }, + card: token, + plan: sub.key, + quantity: 3, + }); + + expect(stripePaymentsCreateSubSpy).to.be.calledOnce; + expect(stripePaymentsCreateSubSpy).to.be.calledWith({ + user, + customerId: customerIdResponse, + paymentMethod: 'Stripe', + sub, + headers, + groupId, + subscriptionId, + }); + }); + }); + + describe('edit subscription', () => { + let user, groupId, group, token; + + 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(); + + groupId = group._id; + + token = 'test-token'; + }); + + it('throws an error if there is no customer id', async () => { + user.purchased.plan.customerId = undefined; + + await expect(stripePayments.editSubscription({ + user, + groupId: undefined, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('missingSubscription'), + }); + }); + + it('throws an error if a token is not provided', async () => { + await expect(stripePayments.editSubscription({ + user, + groupId: undefined, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + name: 'BadRequest', + message: 'Missing req.body.id', + }); + }); + + it('throws an error if the group is not found', async () => { + await expect(stripePayments.editSubscription({ + token, + user, + groupId: 'fake-group', + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 404, + name: 'NotFound', + message: i18n.t('groupNotFound'), + }); + }); + + it('throws an error if user is not the group leader', async () => { + let nonLeader = new User(); + nonLeader.guilds.push(groupId); + await nonLeader.save(); + + await expect(stripePayments.editSubscription({ + token, + user: nonLeader, + groupId, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('onlyGroupLeaderCanManageSubscription'), + }); + }); + + describe('success', () => { + let stripeListSubscriptionStub, stripeUpdateSubscriptionStub, subscriptionId; + + beforeEach(() => { + subscriptionId = 'subId'; + stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions') + .returnsPromise().resolves({ + data: [{id: subscriptionId}], + }); + + stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({}); + }); + + afterEach(() => { + stripe.customers.listSubscriptions.restore(); + stripe.customers.updateSubscription.restore(); + }); + + it('edits a user subscription', async () => { + await stripePayments.editSubscription({ + token, + user, + groupId: undefined, + }, stripe); + + expect(stripeListSubscriptionStub).to.be.calledOnce; + expect(stripeListSubscriptionStub).to.be.calledWith(user.purchased.plan.customerId); + expect(stripeUpdateSubscriptionStub).to.be.calledOnce; + expect(stripeUpdateSubscriptionStub).to.be.calledWith( + user.purchased.plan.customerId, + subscriptionId, + { card: token } + ); + }); + + it('edits a group subscription', async () => { + await stripePayments.editSubscription({ + token, + user, + groupId, + }, stripe); + + expect(stripeListSubscriptionStub).to.be.calledOnce; + expect(stripeListSubscriptionStub).to.be.calledWith(group.purchased.plan.customerId); + expect(stripeUpdateSubscriptionStub).to.be.calledOnce; + expect(stripeUpdateSubscriptionStub).to.be.calledWith( + group.purchased.plan.customerId, + subscriptionId, + { card: token } + ); + }); + }); + }); + + describe('cancel subscription', () => { + let user, groupId, group; + + 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(); + + groupId = group._id; + }); + + it('throws an error if there is no customer id', async () => { + user.purchased.plan.customerId = undefined; + + await expect(stripePayments.cancelSubscription({ + user, + groupId: undefined, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('missingSubscription'), + }); + }); + + it('throws an error if the group is not found', async () => { + await expect(stripePayments.cancelSubscription({ + user, + groupId: 'fake-group', + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 404, + name: 'NotFound', + message: i18n.t('groupNotFound'), + }); + }); + + it('throws an error if user is not the group leader', async () => { + let nonLeader = new User(); + nonLeader.guilds.push(groupId); + await nonLeader.save(); + + await expect(stripePayments.cancelSubscription({ + user: nonLeader, + groupId, + })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('onlyGroupLeaderCanManageSubscription'), + }); + }); + + describe('success', () => { + let stripeDeleteCustomerStub, paymentsCancelSubStub, stripeRetrieveStub, subscriptionId, currentPeriodEndTimeStamp; + + beforeEach(() => { + subscriptionId = 'subId'; + stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({}); + paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); + + currentPeriodEndTimeStamp = (new Date()).getTime(); + stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve') + .returnsPromise().resolves({ + subscriptions: { + data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase + }, + }); + }); + + afterEach(() => { + stripe.customers.del.restore(); + stripe.customers.retrieve.restore(); + payments.cancelSubscription.restore(); + }); + + it('cancels a user subscription', async () => { + await stripePayments.cancelSubscription({ + user, + groupId: undefined, + }, stripe); + + expect(stripeDeleteCustomerStub).to.be.calledOnce; + expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId); + expect(stripeRetrieveStub).to.be.calledOnce; + expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId); + expect(paymentsCancelSubStub).to.be.calledOnce; + expect(paymentsCancelSubStub).to.be.calledWith({ + user, + groupId: undefined, + nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds + paymentMethod: 'Stripe', + }); + }); + + it('cancels a group subscription', async () => { + await stripePayments.cancelSubscription({ + user, + groupId, + }, stripe); + + expect(stripeDeleteCustomerStub).to.be.calledOnce; + expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId); + expect(stripeRetrieveStub).to.be.calledOnce; + expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId); + expect(paymentsCancelSubStub).to.be.calledOnce; + expect(paymentsCancelSubStub).to.be.calledWith({ + user, + groupId, + nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds + paymentMethod: 'Stripe', + }); + }); + }); + }); +}); diff --git a/website/server/controllers/top-level/payments/stripe.js b/website/server/controllers/top-level/payments/stripe.js index 33b654672b..7ec1fdd5ef 100644 --- a/website/server/controllers/top-level/payments/stripe.js +++ b/website/server/controllers/top-level/payments/stripe.js @@ -1,25 +1,9 @@ -import stripeModule from 'stripe'; import shared from '../../../../common'; -import { - BadRequest, - NotAuthorized, - NotFound, -} from '../../../libs/errors'; -import { model as Coupon } from '../../../models/coupon'; -import payments from '../../../libs/payments'; -import nconf from 'nconf'; -import { model as User } from '../../../models/user'; -import { - model as Group, - basicFields as basicGroupFields, -} from '../../../models/group'; -import cc from 'coupon-code'; import { authWithHeaders, authWithUrl, } from '../../../middlewares/auth'; - -const stripe = stripeModule(nconf.get('STRIPE_API_KEY')); +import stripePayments from '../../../libs/stripePayments'; let api = {}; @@ -42,86 +26,15 @@ api.checkout = { url: '/stripe/checkout', middlewares: [authWithHeaders()], async handler (req, res) { + // @TODO: These quer params need to be changed to body let token = req.body.id; let user = res.locals.user; let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined; let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false; let groupId = req.query.groupId; let coupon; - let response; - let subscriptionId; - // @TODO: Update this to use payments.payWithStripe - - if (!token) throw new BadRequest('Missing req.body.id'); - - if (sub) { - if (sub.discount) { - if (!req.query.coupon) throw new BadRequest(res.t('couponCodeRequired')); - coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key}).exec(); - if (!coupon) throw new BadRequest(res.t('invalidCoupon')); - } - - let customerObject = { - email: req.body.email, - metadata: { uuid: user._id }, - card: token, - plan: sub.key, - }; - - if (groupId) { - customerObject.quantity = sub.quantity; - } - - response = await stripe.customers.create(customerObject); - - if (groupId) subscriptionId = response.subscriptions.data[0].id; - } else { - let amount = 500; // $5 - - if (gift) { - if (gift.type === 'subscription') { - amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; - } else { - amount = `${gift.gems.amount / 4 * 100}`; - } - } - - response = await stripe.charges.create({ - amount, - currency: 'usd', - card: token, - }); - } - - if (sub) { - await payments.createSubscription({ - user, - customerId: response.id, - paymentMethod: 'Stripe', - sub, - headers: req.headers, - groupId, - subscriptionId, - }); - } else { - let method = 'buyGems'; - let data = { - user, - customerId: response.id, - paymentMethod: 'Stripe', - gift, - }; - - if (gift) { - let member = await User.findById(gift.uuid).exec(); - gift.member = member; - if (gift.type === 'subscription') method = 'createSubscription'; - data.paymentMethod = 'Stripe (Gift)'; - } - - await payments[method](data); - } + await stripePayments.checkout({token, user, gift, sub, groupId, coupon}); res.respond(200, {}); }, @@ -145,33 +58,8 @@ api.subscribeEdit = { let token = req.body.id; let groupId = req.body.groupId; let user = res.locals.user; - let customerId; - // If we are buying a group subscription - 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')); - } - - let allowedManagers = [group.leader, group.purchased.plan.owner]; - - if (allowedManagers.indexOf(user._id) === -1) { - 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')); - if (!token) throw new BadRequest('Missing req.body.id'); - - let subscriptions = await stripe.customers.listSubscriptions(customerId); - let subscriptionId = subscriptions.data[0].id; - await stripe.customers.updateSubscription(customerId, subscriptionId, { card: token }); + await stripePayments.editSubscription({token, groupId, user}); res.respond(200, {}); }, @@ -190,42 +78,12 @@ api.subscribeCancel = { async handler (req, res) { let user = res.locals.user; let groupId = req.query.groupId; - let customerId; + let redirect = req.query.redirect; - if (groupId) { - let groupFields = basicGroupFields.concat(' purchased'); - let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); + await stripePayments.cancelSubscription({user, groupId}); - 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 stripe.customers.retrieve(customerId); - - let subscription = customer.subscription; - if (!subscription) { - subscription = customer.subscriptions.data[0]; - } - - await stripe.customers.del(customerId); - await payments.cancelSubscription({ - user, - groupId, - nextBill: subscription.current_period_end * 1000, // timestamp in seconds - paymentMethod: 'Stripe', - }); - - res.redirect('/'); + if (redirect === 'none') return res.respond(200, {}); + return res.redirect('/'); }, }; diff --git a/website/server/libs/payments.js b/website/server/libs/payments.js index f68e6c9492..7dfc0c517c 100644 --- a/website/server/libs/payments.js +++ b/website/server/libs/payments.js @@ -11,20 +11,14 @@ import { model as Group, basicFields as basicGroupFields, } from '../models/group'; -import { model as Coupon } from '../models/coupon'; -import { model as User } from '../models/user'; import { NotAuthorized, NotFound, } from './errors'; import slack from './slack'; import nconf from 'nconf'; -import stripeModule from 'stripe'; -import { - BadRequest, -} from './errors'; -import cc from 'coupon-code'; +import stripeModule from 'stripe'; const stripe = stripeModule(nconf.get('STRIPE_API_KEY')); @@ -382,106 +376,4 @@ api.buyGems = async function buyGems (data) { await data.user.save(); }; -/** - * Allows for purchasing a user subscription, group subscription or gems with Stripe - * - * @param options - * @param options.token The stripe token generated on the front end - * @param options.user The user object who is purchasing - * @param options.gift The gift details if any - * @param options.sub The subscription data to purchase - * @param options.groupId The id of the group purchasing a subscription - * @param options.email The email enter by the user on the Stripe form - * @param options.headers The request headers to store on analytics - * @return undefined - */ -api.payWithStripe = async function payWithStripe (options, stripeInc) { - let { - token, - user, - gift, - sub, - groupId, - email, - headers, - coupon, - } = options; - let response; - let subscriptionId; - // @TODO: We need to mock this, but curently we don't have correct Dependency Injection - let stripeApi = stripe; - - if (stripeInc) stripeApi = stripeInc; - - if (!token) throw new BadRequest('Missing req.body.id'); - - if (sub) { - if (sub.discount) { - if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); - coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); - if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); - } - - let customerObject = { - email, - metadata: { uuid: user._id }, - card: token, - plan: sub.key, - }; - - if (groupId) { - customerObject.quantity = sub.quantity; - } - - response = await stripeApi.customers.create(customerObject); - - if (groupId) subscriptionId = response.subscriptions.data[0].id; - } else { - let amount = 500; // $5 - - if (gift) { - if (gift.type === 'subscription') { - amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; - } else { - amount = `${gift.gems.amount / 4 * 100}`; - } - } - - response = await stripe.charges.create({ - amount, - currency: 'usd', - card: token, - }); - } - - if (sub) { - await this.createSubscription({ - user, - customerId: response.id, - paymentMethod: 'Stripe', - sub, - headers, - groupId, - subscriptionId, - }); - } else { - let method = 'buyGems'; - let data = { - user, - customerId: response.id, - paymentMethod: 'Stripe', - gift, - }; - - if (gift) { - let member = await User.findById(gift.uuid).exec(); - gift.member = member; - if (gift.type === 'subscription') method = 'createSubscription'; - data.paymentMethod = 'Gift'; - } - - await this[method](data); - } -}; - module.exports = api; diff --git a/website/server/libs/stripePayments.js b/website/server/libs/stripePayments.js new file mode 100644 index 0000000000..d8ccd82f95 --- /dev/null +++ b/website/server/libs/stripePayments.js @@ -0,0 +1,223 @@ +import stripeModule from 'stripe'; +import nconf from 'nconf'; +import cc from 'coupon-code'; + +import { + BadRequest, + NotAuthorized, + NotFound, +} from './errors'; +import payments from './payments'; +import { model as User } from '../models/user'; +import { model as Coupon } from '../models/coupon'; +import { + model as Group, + basicFields as basicGroupFields, +} from '../models/group'; +import shared from '../../common'; + +const stripe = stripeModule(nconf.get('STRIPE_API_KEY')); +const i18n = shared.i18n; + +let api = {}; + +/** + * Allows for purchasing a user subscription, group subscription or gems with Stripe + * + * @param options + * @param options.token The stripe token generated on the front end + * @param options.user The user object who is purchasing + * @param options.gift The gift details if any + * @param options.sub The subscription data to purchase + * @param options.groupId The id of the group purchasing a subscription + * @param options.email The email enter by the user on the Stripe form + * @param options.headers The request headers to store on analytics + * @return undefined + */ +api.checkout = async function checkout (options, stripeInc) { + let { + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + } = options; + let response; + let subscriptionId; + + // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? + let stripeApi = stripe; + if (stripeInc) stripeApi = stripeInc; + + if (!token) throw new BadRequest('Missing req.body.id'); + + if (sub) { + if (sub.discount) { + if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); + coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); + if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); + } + + let customerObject = { + email, + metadata: { uuid: user._id }, + card: token, + plan: sub.key, + }; + + if (groupId) { + customerObject.quantity = sub.quantity; + } + + response = await stripeApi.customers.create(customerObject); + + if (groupId) subscriptionId = response.subscriptions.data[0].id; + } else { + let amount = 500; // $5 + + if (gift) { + if (gift.type === 'subscription') { + amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; + } else { + amount = `${gift.gems.amount / 4 * 100}`; + } + } + + response = await stripeApi.charges.create({ + amount, + currency: 'usd', + card: token, + }); + } + + if (sub) { + await payments.createSubscription({ + user, + customerId: response.id, + paymentMethod: 'Stripe', + sub, + headers, + groupId, + subscriptionId, + }); + } else { + let method = 'buyGems'; + let data = { + user, + customerId: response.id, + paymentMethod: 'Stripe', + gift, + }; + + if (gift) { + let member = await User.findById(gift.uuid).exec(); + gift.member = member; + if (gift.type === 'subscription') method = 'createSubscription'; + data.paymentMethod = 'Gift'; + } + + await payments[method](data); + } +}; + +/** + * Edits a subscription created by Stripe + * + * @param options + * @param options.token The stripe token generated on the front end + * @param options.user The user object who is purchasing + * @param options.groupId The id of the group purchasing a subscription + * + * @return undefined + */ +api.editSubscription = async function editSubscription (options, stripeInc) { + let {token, groupId, user} = options; + let customerId; + + // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? + let stripeApi = stripe; + if (stripeInc) stripeApi = stripeInc; + + 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')); + } + + let allowedManagers = [group.leader, group.purchased.plan.owner]; + + if (allowedManagers.indexOf(user._id) === -1) { + 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')); + if (!token) throw new BadRequest('Missing req.body.id'); + + let subscriptions = await stripeApi.customers.listSubscriptions(customerId); // @TODO: Handle Stripe Error response + let subscriptionId = subscriptions.data[0].id; + await stripeApi.customers.updateSubscription(customerId, subscriptionId, { card: token }); +}; + +/** + * Cancels a subscription created by Stripe + * + * @param options + * @param options.user The user object who is purchasing + * @param options.groupId The id of the group purchasing a subscription + * + * @return undefined + */ +api.cancelSubscription = async function cancelSubscription (options, stripeInc) { + let {groupId, user} = options; + let customerId; + + // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? + let stripeApi = stripe; + if (stripeInc) stripeApi = stripeInc; + + 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')); + } + + let allowedManagers = [group.leader, group.purchased.plan.owner]; + + if (allowedManagers.indexOf(user._id) === -1) { + 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 stripeApi.customers.retrieve(customerId); + + let subscription = customer.subscription; + if (!subscription) { + subscription = customer.subscriptions.data[0]; + } + + await stripeApi.customers.del(customerId); + await payments.cancelSubscription({ + user, + groupId, + nextBill: subscription.current_period_end * 1000, // timestamp in seconds + paymentMethod: 'Stripe', + }); +}; + +module.exports = api;