mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
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:
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
180
test/api/v3/unit/libs/payments/amazon/cancel.test.js
Normal file
180
test/api/v3/unit/libs/payments/amazon/cancel.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
193
test/api/v3/unit/libs/payments/amazon/checkout.test.js
Normal file
193
test/api/v3/unit/libs/payments/amazon/checkout.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
267
test/api/v3/unit/libs/payments/amazon/subscribe.test.js
Normal file
267
test/api/v3/unit/libs/payments/amazon/subscribe.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
7
test/api/v3/unit/libs/payments/paymentHelpers.js
Normal file
7
test/api/v3/unit/libs/payments/paymentHelpers.js
Normal 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();
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
127
test/api/v3/unit/libs/payments/paypal/checkout.test.js
Normal file
127
test/api/v3/unit/libs/payments/paypal/checkout.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
66
test/api/v3/unit/libs/payments/paypal/ipn.test.js
Normal file
66
test/api/v3/unit/libs/payments/paypal/ipn.test.js
Normal 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' });
|
||||||
|
});
|
||||||
|
});
|
||||||
124
test/api/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal file
124
test/api/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
112
test/api/v3/unit/libs/payments/paypal/subscribe.test.js
Normal file
112
test/api/v3/unit/libs/payments/paypal/subscribe.test.js
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
193
test/api/v3/unit/libs/payments/stripe/checkout.test.js
Normal file
193
test/api/v3/unit/libs/payments/stripe/checkout.test.js
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
147
test/api/v3/unit/libs/payments/stripe/edit-subscription.test.js
Normal file
147
test/api/v3/unit/libs/payments/stripe/edit-subscription.test.js
Normal 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 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
257
test/api/v3/unit/libs/payments/stripe/handle-webhook.test.js
Normal file
257
test/api/v3/unit/libs/payments/stripe/handle-webhook.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
Reference in New Issue
Block a user