Payment tests refactor (#9695)

* Reorganized files, reduced function size, reduced duplication

* Refactored amazon tests organization

* Reduced duplication

* Reorganized paypal tests

* Reorganized stripe tests

* Fixed lint issues

* Fixed gem purchase expectations

* Added cloning so we don't modify the common block
This commit is contained in:
Keith Holliday
2017-12-14 09:59:45 -06:00
committed by GitHub
parent e7fd2b4c79
commit 54db84fddc
20 changed files with 2436 additions and 2359 deletions

View File

@@ -1,739 +0,0 @@
import moment from 'moment';
import cc from 'coupon-code';
import uuid from 'uuid';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import { model as Coupon } from '../../../../../website/server/models/coupon';
import amzLib from '../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../website/server/libs/payments';
import common from '../../../../../website/common';
const i18n = common.i18n;
describe('Amazon Payments', () => {
let subKey = 'basic_3mo';
describe('checkout', () => {
let user, orderReferenceId, headers;
let setOrderReferenceDetailsSpy;
let confirmOrderReferenceSpy;
let authorizeSpy;
let closeOrderReferenceSpy;
let paymentBuyGemsStub;
let paymentCreateSubscritionStub;
let amount = 5;
function expectAmazonStubs () {
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: {
OrderTotal: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerNote: amzLib.constants.SELLER_NOTE,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
},
});
expect(confirmOrderReferenceSpy).to.be.calledOnce;
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
expect(authorizeSpy).to.be.calledOnce;
expect(authorizeSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
TransactionTimeout: 0,
CaptureNow: true,
});
expect(closeOrderReferenceSpy).to.be.calledOnce;
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
}
beforeEach(function () {
user = new User();
headers = {};
orderReferenceId = 'orderReferenceId';
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
confirmOrderReferenceSpy.returnsPromise().resolves({});
authorizeSpy = sinon.stub(amzLib, 'authorize');
authorizeSpy.returnsPromise().resolves({});
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
closeOrderReferenceSpy.returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscritionStub.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setOrderReferenceDetails.restore();
amzLib.confirmOrderReference.restore();
amzLib.authorize.restore();
amzLib.closeOrderReference.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
it('should purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await amzLib.checkout({user, orderReferenceId, headers});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
});
expectAmazonStubs();
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
amount = 16 / 4;
await amzLib.checkout({gift, user, orderReferenceId, headers});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
amount = common.content.subscriptionBlocks[subKey].price;
await amzLib.checkout({user, orderReferenceId, headers, gift});
gift.member = receivingUser;
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
});
describe('subscribe', () => {
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
amount = common.content.subscriptionBlocks[subKey].price;
billingAgreementId = 'billingAgreementId';
sub = {
key: subKey,
price: amount,
};
groupId = group._id;
headers = {};
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(payments, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
it('should throw an error if we are missing a subscription', async () => {
await expect(amzLib.subscribe({
billingAgreementId,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if we are missing a billingAgreementId', async () => {
await expect(amzLib.subscribe({
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.billingAgreementId',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with amazon with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
cc.validate.restore();
});
it('subscribes with amazon', async () => {
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
},
},
});
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
});
it('subscribes with amazon with price to existing users', async () => {
user = new User();
user.guilds.push(groupId);
await user.save();
group.memberCount = 2;
await group.save();
sub.key = 'group_monthly';
sub.price = 9;
amount = 12;
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
},
},
});
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
});
});
describe('cancelSubscription', () => {
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
let getBillingAgreementDetailsSpy;
let paymentCancelSubscriptionSpy;
function expectAmazonStubs () {
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
}
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
subscriptionBlock = common.content.subscriptionBlocks[subKey];
subscriptionLength = subscriptionBlock.months * 30;
headers = {};
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
getBillingAgreementDetailsSpy.returnsPromise().resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.getBillingAgreementDetails.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(amzLib.cancelSubscription({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should cancel a user subscription', async () => {
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
it('should close a user subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});
it('should throw an error if group is not found', async () => {
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('should throw an error if user is not group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(group._id);
await nonLeader.save();
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a group subscription', async () => {
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
it('should close a group subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});
});
describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString;
beforeEach(async function () {
user = new User();
user.profile.name = 'sender';
data = {
user,
sub: {
key: 'basic_3mo', // @TODO: Validate that this is group
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
spy.returnsPromise().resolves([]);
uuidString = 'uuid-v4';
sinon.stub(uuid, 'v4').returns(uuidString);
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(amzLib.authorizeOnBillingAgreement);
uuid.v4.restore();
});
it('charges for a new member', async () => {
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
await payments.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
updatedGroup.memberCount += 1;
await updatedGroup.save();
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
expect(spy.calledOnce).to.be.true;
expect(spy).to.be.calledWith({
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
AuthorizationReferenceId: uuidString.substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: 3,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
SellerOrderAttributes: {
SellerOrderId: uuidString,
StoreName: amzLib.constants.STORE_NAME,
},
});
});
});
});

View File

@@ -0,0 +1,180 @@
import moment from 'moment';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers';
const i18n = common.i18n;
describe('Amazon Payments - Cancel Subscription', () => {
const subKey = 'basic_3mo';
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
let getBillingAgreementDetailsSpy;
let paymentCancelSubscriptionSpy;
function expectAmazonStubs () {
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
}
function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) {
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId,
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
}
function expectAmazonCancelUserSubscriptionSpy () {
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate);
}
function expectAmazonCancelGroupSubscriptionSpy (groupId) {
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate);
}
function expectBillingAggreementDetailSpy () {
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
}
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
subscriptionBlock = common.content.subscriptionBlocks[subKey];
subscriptionLength = subscriptionBlock.months * 30;
headers = {};
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
getBillingAgreementDetailsSpy.returnsPromise().resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.getBillingAgreementDetails.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(amzLib.cancelSubscription({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should cancel a user subscription', async () => {
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expectAmazonCancelUserSubscriptionSpy();
expectAmazonStubs();
});
it('should close a user subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
expectBillingAggreementDetailSpy();
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonCancelUserSubscriptionSpy();
amzLib.closeBillingAgreement.restore();
});
it('should throw an error if group is not found', async () => {
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('should throw an error if user is not group leader', async () => {
let nonLeader = await createNonLeaderGroupMember(group);
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a group subscription', async () => {
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expectAmazonCancelGroupSubscriptionSpy(group._id);
expectAmazonStubs();
});
it('should close a group subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
expectBillingAggreementDetailSpy();
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonCancelGroupSubscriptionSpy(group._id);
amzLib.closeBillingAgreement.restore();
});
});

View File

@@ -0,0 +1,193 @@
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('Amazon Payments - Checkout', () => {
const subKey = 'basic_3mo';
let user, orderReferenceId, headers;
let setOrderReferenceDetailsSpy;
let confirmOrderReferenceSpy;
let authorizeSpy;
let closeOrderReferenceSpy;
let paymentBuyGemsStub;
let paymentCreateSubscritionStub;
let amount = 5;
function expectOrderReferenceSpy () {
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: {
OrderTotal: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerNote: amzLib.constants.SELLER_NOTE,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
},
});
}
function expectAuthorizeSpy () {
expect(authorizeSpy).to.be.calledOnce;
expect(authorizeSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
TransactionTimeout: 0,
CaptureNow: true,
});
}
function expectAmazonStubs () {
expectOrderReferenceSpy();
expect(confirmOrderReferenceSpy).to.be.calledOnce;
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
expectAuthorizeSpy();
expect(closeOrderReferenceSpy).to.be.calledOnce;
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
}
beforeEach(function () {
user = new User();
headers = {};
orderReferenceId = 'orderReferenceId';
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
confirmOrderReferenceSpy.returnsPromise().resolves({});
authorizeSpy = sinon.stub(amzLib, 'authorize');
authorizeSpy.returnsPromise().resolves({});
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
closeOrderReferenceSpy.returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscritionStub.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setOrderReferenceDetails.restore();
amzLib.confirmOrderReference.restore();
amzLib.authorize.restore();
amzLib.closeOrderReference.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
function expectBuyGemsStub (paymentMethod, gift) {
expect(paymentBuyGemsStub).to.be.calledOnce;
let expectedArgs = {
user,
paymentMethod,
headers,
};
if (gift) expectedArgs.gift = gift;
expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs);
}
it('should purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await amzLib.checkout({user, orderReferenceId, headers});
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD);
expectAmazonStubs();
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
amount = 16 / 4;
await amzLib.checkout({gift, user, orderReferenceId, headers});
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD_GIFT, gift);
expectAmazonStubs();
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
amount = common.content.subscriptionBlocks[subKey].price;
await amzLib.checkout({user, orderReferenceId, headers, gift});
gift.member = receivingUser;
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
});

View File

@@ -0,0 +1,267 @@
import cc from 'coupon-code';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('Amazon Payments - Subscribe', () => {
const subKey = 'basic_3mo';
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazonAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
amount = common.content.subscriptionBlocks[subKey].price;
billingAgreementId = 'billingAgreementId';
sub = {
key: subKey,
price: amount,
};
groupId = group._id;
headers = {};
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazonAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(payments, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
function expectAmazonAuthorizeBillingAgreementSpy () {
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
});
}
function expectAmazonSetBillingAgreementDetailsSpy () {
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
},
},
});
}
function expectCreateSpy () {
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
}
it('should throw an error if we are missing a subscription', async () => {
await expect(amzLib.subscribe({
billingAgreementId,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if we are missing a billingAgreementId', async () => {
await expect(amzLib.subscribe({
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.billingAgreementId',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with amazon with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expectCreateSpy();
cc.validate.restore();
});
it('subscribes with amazon', async () => {
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expectAmazonSetBillingAgreementDetailsSpy();
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonAuthorizeBillingAgreementSpy();
expectCreateSpy();
});
it('subscribes with amazon with price to existing users', async () => {
user = new User();
user.guilds.push(groupId);
await user.save();
group.memberCount = 2;
await group.save();
sub.key = 'group_monthly';
sub.price = 9;
amount = 12;
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expectAmazonSetBillingAgreementDetailsSpy();
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonAuthorizeBillingAgreementSpy();
expectCreateSpy();
});
});

View File

@@ -0,0 +1,83 @@
import uuid from 'uuid';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString;
beforeEach(async function () {
user = new User();
user.profile.name = 'sender';
data = {
user,
sub: {
key: 'basic_3mo', // @TODO: Validate that this is group
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
spy.returnsPromise().resolves([]);
uuidString = 'uuid-v4';
sinon.stub(uuid, 'v4').returns(uuidString);
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(amzLib.authorizeOnBillingAgreement);
uuid.v4.restore();
});
it('charges for a new member', async () => {
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
await payments.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
updatedGroup.memberCount += 1;
await updatedGroup.save();
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
expect(spy.calledOnce).to.be.true;
expect(spy).to.be.calledWith({
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
AuthorizationReferenceId: uuidString.substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: 3,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
SellerOrderAttributes: {
SellerOrderId: uuidString,
StoreName: amzLib.constants.STORE_NAME,
},
});
});
});

View File

@@ -0,0 +1,7 @@
import { model as User } from '../../../../../../website/server/models/user';
export async function createNonLeaderGroupMember (group) {
let nonLeader = new User();
nonLeader.guilds.push(group._id);
return await nonLeader.save();
}

View File

@@ -0,0 +1,87 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as User } from '../../../../../../../website/server/models/user';
describe('checkout success', () => {
const subKey = 'basic_3mo';
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,
});
});
});

View File

@@ -0,0 +1,127 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
const BASE_URL = nconf.get('BASE_URL');
const i18n = common.i18n;
describe('checkout', () => {
const subKey = 'basic_3mo';
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({user: new User()});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
expect(link).to.eql(approvalHerf);
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(paypalPayments.checkout({gift}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if the user cannot get gems', async () => {
let user = new User();
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('creates a link for gifting gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
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);
});
});

View File

@@ -0,0 +1,66 @@
/* eslint-disable camelcase */
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';
describe('ipn', () => {
const subKey = 'basic_3mo';
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' });
});
});

View File

@@ -0,0 +1,124 @@
/* eslint-disable camelcase */
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 common from '../../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers';
const i18n = common.i18n;
describe('subscribeCancel', () => {
const subKey = 'basic_3mo';
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 = await createNonLeaderGroupMember(group);
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,
cancellationReason: undefined,
});
});
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,
cancellationReason: undefined,
});
});
});

View File

@@ -0,0 +1,77 @@
/* eslint-disable camelcase */
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 common from '../../../../../../../website/common';
describe('subscribeSuccess', () => {
const subKey = 'basic_3mo';
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,
});
});
});

View File

@@ -0,0 +1,112 @@
/* eslint-disable camelcase */
import moment from 'moment';
import cc from 'coupon-code';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('subscribe', () => {
const subKey = 'basic_3mo';
let coupon, sub, approvalHerf;
let paypalBillingAgreementCreateStub;
beforeEach(() => {
approvalHerf = 'approvalHerf-test';
sub = Object.assign({}, 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',
},
});
});
});

View File

@@ -0,0 +1,143 @@
import stripeModule from 'stripe';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('cancel subscription', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
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',
cancellationReason: undefined,
});
});
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',
cancellationReason: undefined,
});
});
});
});

View File

@@ -0,0 +1,307 @@
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('checkout with subscription', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
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,
});
});
it('subscribes a group with the correct number of group members', async () => {
token = 'test-token';
sub = data.sub;
groupId = group._id;
email = 'test@test.com';
headers = {};
user = new User();
user.guilds.push(groupId);
await user.save();
group.memberCount = 2;
await group.save();
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: 4,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId,
subscriptionId,
});
});
});

View File

@@ -0,0 +1,193 @@
import stripeModule from 'stripe';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('checkout', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
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 error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe)).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('should purchase gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
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,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
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,
});
});
});

View File

@@ -0,0 +1,147 @@
import stripeModule from 'stripe';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('edit subscription', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
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 }
);
});
});
});

View File

@@ -0,0 +1,257 @@
import stripeModule from 'stripe';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
import logger from '../../../../../../../website/server/libs/logger';
import { v4 as uuid } from 'uuid';
import moment from 'moment';
const i18n = common.i18n;
describe('Stripe - Webhooks', () => {
const stripe = stripeModule('test');
describe('all events', () => {
const eventType = 'account.updated';
const event = {id: 123};
const eventRetrieved = {type: eventType};
beforeEach(() => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
sinon.stub(logger, 'error');
});
afterEach(() => {
stripe.events.retrieve.restore();
logger.error.restore();
});
it('logs an error if an unsupported webhook event is passed', async () => {
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
await stripePayments.handleWebhooks({requestBody: event}, stripe);
expect(logger.error).to.have.been.called.once;
expect(logger.error).to.have.been.calledWith(error, {event: eventRetrieved});
});
it('retrieves and validates the event from Stripe', async () => {
await stripePayments.handleWebhooks({requestBody: event}, stripe);
expect(stripe.events.retrieve).to.have.been.called.once;
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
});
});
describe('customer.subscription.deleted', () => {
const eventType = 'customer.subscription.deleted';
beforeEach(() => {
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.customers.del.restore();
payments.cancelSubscription.restore();
});
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
request: 123,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.events.retrieve).to.have.been.called.once;
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
describe('user subscription', () => {
it('throws an error if the user is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'basic_earned',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('userNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
const customerId = '456';
let subscriber = new User();
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'basic_earned',
},
customer: customerId,
},
},
request: null,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.customers.del).to.have.been.calledOnce;
expect(stripe.customers.del).to.have.been.calledWith(customerId);
expect(payments.cancelSubscription).to.have.been.calledOnce;
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
expect(cancelSubscriptionOpts.groupId).to.be.undefined;
stripe.events.retrieve.restore();
});
});
describe('group plan subscription', () => {
it('throws an error if the group is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('groupNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('throws an error if the group leader is not found', async () => {
const customerId = 456;
let subscriber = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: uuid(),
});
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('userNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
const customerId = '456';
let leader = new User();
await leader.save();
let subscriber = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: leader._id,
});
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.customers.del).to.have.been.calledOnce;
expect(stripe.customers.del).to.have.been.calledWith(customerId);
expect(payments.cancelSubscription).to.have.been.calledOnce;
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
stripe.events.retrieve.restore();
});
});
});
});

View File

@@ -0,0 +1,66 @@
import stripeModule from 'stripe';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
describe('Stripe - Upgrade Group Plan', () => {
const stripe = stripeModule('test');
let spy, data, user, group;
beforeEach(async function () {
user = new User();
user.profile.name = 'sender';
data = {
user,
sub: {
key: 'basic_3mo', // @TODO: Validate that this is group
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
data.groupId = group._id;
data.sub.quantity = 3;
stripePayments.setStripeApi(stripe);
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await payments.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await stripePayments.chargeForAdditionalGroupMember(updatedGroup);
expect(spy.calledOnce).to.be.true;
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
});

View File

@@ -1,561 +0,0 @@
/* 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({user: new User()});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
expect(link).to.eql(approvalHerf);
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(paypalPayments.checkout({gift}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if the user cannot get gems', async () => {
let user = new User();
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('creates a link for gifting gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
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,
cancellationReason: undefined,
});
});
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,
cancellationReason: undefined,
});
});
});
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' });
});
});
});

File diff suppressed because it is too large Load Diff