Stripe refactor to lib (#8417)

* Moved stripe tests to folder

* Abstracted stripe payments logic to lib

* Added initial unit test for stripe payment

* Added subscription tests

* Added tests for regulare purchases

* Added tests for edit subscription

* Added cancel tests

* Added integration tests

* Fixed lint issues

* Fixed lint issue
This commit is contained in:
Keith Holliday
2017-01-20 16:08:52 -07:00
committed by GitHub
parent 2c37ba3cee
commit 638525f8d8
11 changed files with 1120 additions and 376 deletions

View File

@@ -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'),
});
});
});

View File

@@ -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',
});
});
});

View File

@@ -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'),
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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;
});
});
});

View File

@@ -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',
});
});
});
});
});

View File

@@ -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('/');
},
};

View File

@@ -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;

View File

@@ -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;