diff --git a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js index f25038002c..8ff7e5d7c3 100644 --- a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js +++ b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js @@ -443,8 +443,7 @@ describe('Purchasing a group plan for group', () => { await api.createSubscription(data); - let updatedUser = await User.findById(recipient._id).exec(); - + const updatedUser = await User.findById(recipient._id).exec(); expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3); }); diff --git a/test/api/v3/unit/libs/payments/stripe/checkout.test.js b/test/api/v3/unit/libs/payments/stripe/checkout.test.js index fca369540c..faba2dd5ef 100644 --- a/test/api/v3/unit/libs/payments/stripe/checkout.test.js +++ b/test/api/v3/unit/libs/payments/stripe/checkout.test.js @@ -37,6 +37,22 @@ describe('checkout', () => { payments.createSubscription.restore(); }); + it('should error if there is no token', async () => { + await expect(stripePayments.checkout({ + user, + gift, + groupId, + email, + headers, + coupon, + }, stripe)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + message: 'Missing req.body.id', + name: 'BadRequest', + }); + }); + it('should error if gem amount is too low', async () => { let receivingUser = new User(); receivingUser.save(); @@ -64,7 +80,6 @@ describe('checkout', () => { }); }); - it('should error if user cannot get gems', async () => { gift = undefined; sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); diff --git a/website/server/libs/payments/stripe.js b/website/server/libs/payments/stripe.js index c2c218682d..ff7483a6f5 100644 --- a/website/server/libs/payments/stripe.js +++ b/website/server/libs/payments/stripe.js @@ -1,7 +1,5 @@ -import stripeModule from 'stripe'; -import nconf from 'nconf'; -import cc from 'coupon-code'; import moment from 'moment'; + import logger from '../logger'; import { BadRequest, @@ -10,38 +8,22 @@ import { } from '../errors'; import payments from './payments'; import { model as User } from '../../models/user'; -import { model as Coupon } from '../../models/coupon'; import { model as Group, basicFields as basicGroupFields, } from '../../models/group'; import shared from '../../../common'; +import stripeConstants from './stripe/constants'; +import { checkout } from './stripe/checkout'; +import { getStripeApi, setStripeApi } from './stripe/api'; -let stripe = stripeModule(nconf.get('STRIPE_API_KEY')); const i18n = shared.i18n; let api = {}; -api.constants = { - // CURRENCY_CODE: 'USD', - // SELLER_NOTE: 'Habitica Payment', - // SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', - // SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', - // STORE_NAME: 'Habitica', - // - // GIFT_TYPE_GEMS: 'gems', - // GIFT_TYPE_SUBSCRIPTION: 'subscription', - // - // METHOD_BUY_GEMS: 'buyGems', - // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', - PAYMENT_METHOD: 'Stripe', - // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', -}; - -api.setStripeApi = function setStripeApi (stripeInc) { - stripe = stripeInc; -}; +api.constants = Object.assign({}, stripeConstants); +api.setStripeApi = setStripeApi; /** * Allows for purchasing a user subscription, group subscription or gems with Stripe @@ -56,110 +38,7 @@ api.setStripeApi = function setStripeApi (stripeInc) { * @param options.headers The request headers to store on analytics * @return undefined */ -api.checkout = async function checkout (options, stripeInc) { - let { - token, - user, - gift, - sub, - groupId, - email, - headers, - coupon, - } = options; - let response; - let subscriptionId; - - // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; - if (stripeInc) stripeApi = stripeInc; - - if (!token) throw new BadRequest('Missing req.body.id'); - - if (gift) { - const member = await User.findById(gift.uuid).exec(); - gift.member = member; - } - - if (sub) { - if (sub.discount) { - if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); - coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); - if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); - } - - let customerObject = { - email, - metadata: { uuid: user._id }, - card: token, - plan: sub.key, - }; - - if (groupId) { - customerObject.quantity = sub.quantity; - const groupFields = basicGroupFields.concat(' purchased'); - const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); - const membersCount = await group.getMemberCount(); - customerObject.quantity = membersCount + sub.quantity - 1; - } - - response = await stripeApi.customers.create(customerObject); - - if (groupId) subscriptionId = response.subscriptions.data[0].id; - } else { - let amount = 500; // $5 - - if (gift) { - if (gift.type === 'subscription') { - amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; - } else { - if (gift.gems.amount <= 0) { - throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase')); - } - amount = `${gift.gems.amount / 4 * 100}`; - } - } - - if (!gift || gift.type === 'gems') { - const receiver = gift ? gift.member : user; - const receiverCanGetGems = await receiver.canGetGems(); - if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); - } - - response = await stripeApi.charges.create({ - amount, - currency: 'usd', - card: token, - }); - } - - if (sub) { - await payments.createSubscription({ - user, - customerId: response.id, - paymentMethod: this.constants.PAYMENT_METHOD, - sub, - headers, - groupId, - subscriptionId, - }); - } else { - let method = 'buyGems'; - let data = { - user, - customerId: response.id, - paymentMethod: this.constants.PAYMENT_METHOD, - gift, - }; - - if (gift) { - if (gift.type === 'subscription') method = 'createSubscription'; - data.paymentMethod = 'Gift'; - } - - await payments[method](data); - } -}; +api.checkout = checkout; /** * Edits a subscription created by Stripe @@ -176,7 +55,7 @@ api.editSubscription = async function editSubscription (options, stripeInc) { let customerId; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; if (groupId) { @@ -220,7 +99,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc) let customerId; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; if (groupId) { @@ -271,7 +150,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc) }; api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) { - let stripeApi = stripe; + let stripeApi = getStripeApi(); let plan = shared.content.subscriptionBlocks.group_monthly; await stripeApi.subscriptions.update( @@ -298,7 +177,7 @@ api.handleWebhooks = async function handleWebhooks (options, stripeInc) { let {requestBody} = options; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; // Verify the event by fetching it from Stripe diff --git a/website/server/libs/payments/stripe/api.js b/website/server/libs/payments/stripe/api.js new file mode 100644 index 0000000000..43a99402b1 --- /dev/null +++ b/website/server/libs/payments/stripe/api.js @@ -0,0 +1,14 @@ +import stripeModule from 'stripe'; +import nconf from 'nconf'; + +let stripe = stripeModule(nconf.get('STRIPE_API_KEY')); + +function setStripeApi (stripeInc) { + stripe = stripeInc; +} + +function getStripeApi () { + return stripe; +} + +module.exports = { getStripeApi, setStripeApi }; diff --git a/website/server/libs/payments/stripe/checkout.js b/website/server/libs/payments/stripe/checkout.js new file mode 100644 index 0000000000..8dafa3536c --- /dev/null +++ b/website/server/libs/payments/stripe/checkout.js @@ -0,0 +1,146 @@ +import cc from 'coupon-code'; + +import { getStripeApi } from './api'; +import { model as User } from '../../../models/user'; +import { model as Coupon } from '../../../models/coupon'; +import { + model as Group, + basicFields as basicGroupFields, +} from '../../../models/group'; +import shared from '../../../../common'; +import { + BadRequest, + NotAuthorized, +} from '../../errors'; +import payments from './../payments'; +import stripeConstants from './constants'; + +function getGiftAmount (gift) { + if (gift.type === 'subscription') { + return `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; + } + + if (gift.gems.amount <= 0) { + throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase')); + } + + return `${gift.gems.amount / 4 * 100}`; +} + +async function buyGems (gift, user, token, stripeApi) { + let amount = 500; // $5 + + if (gift) amount = getGiftAmount(gift); + + if (!gift || gift.type === 'gems') { + const receiver = gift ? gift.member : user; + const receiverCanGetGems = await receiver.canGetGems(); + if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); + } + + const response = await stripeApi.charges.create({ + amount, + currency: 'usd', + card: token, + }); + + return response; +} + +async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) { + if (sub.discount) { + if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); + coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); + if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); + } + + let customerObject = { + email, + metadata: { uuid: user._id }, + card: token, + plan: sub.key, + }; + + if (groupId) { + customerObject.quantity = sub.quantity; + const groupFields = basicGroupFields.concat(' purchased'); + const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); + const membersCount = await group.getMemberCount(); + customerObject.quantity = membersCount + sub.quantity - 1; + } + + const response = await stripeApi.customers.create(customerObject); + + let subscriptionId; + if (groupId) subscriptionId = response.subscriptions.data[0].id; + + return { subResponse: response, subId: subscriptionId }; +} + +async function applyGemPayment (user, response, gift) { + let method = 'buyGems'; + const data = { + user, + customerId: response.id, + paymentMethod: stripeConstants.PAYMENT_METHOD, + gift, + }; + + if (gift) { + if (gift.type === 'subscription') method = 'createSubscription'; + data.paymentMethod = 'Gift'; + } + + await payments[method](data); +} + +async function checkout (options, stripeInc) { + let { + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + } = options; + let response; + let subscriptionId; + + // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? + let stripeApi = getStripeApi(); + if (stripeInc) stripeApi = stripeInc; + + if (!token) throw new BadRequest('Missing req.body.id'); + + if (gift) { + const member = await User.findById(gift.uuid).exec(); + gift.member = member; + } + + if (sub) { + const { subId, subResponse } = await buySubscription(sub, coupon, email, user, token, groupId, stripeApi); + subscriptionId = subId; + response = subResponse; + } else { + response = await buyGems(gift, user, token, stripeApi); + } + + if (sub) { + await payments.createSubscription({ + user, + customerId: response.id, + paymentMethod: this.constants.PAYMENT_METHOD, + sub, + headers, + groupId, + subscriptionId, + }); + return; + } + + await applyGemPayment(user, response, gift); +} + +module.exports = { checkout }; diff --git a/website/server/libs/payments/stripe/constants.js b/website/server/libs/payments/stripe/constants.js new file mode 100644 index 0000000000..00bddb1e87 --- /dev/null +++ b/website/server/libs/payments/stripe/constants.js @@ -0,0 +1,15 @@ +module.exports = { + // CURRENCY_CODE: 'USD', + // SELLER_NOTE: 'Habitica Payment', + // SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', + // SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', + // STORE_NAME: 'Habitica', + // + // GIFT_TYPE_GEMS: 'gems', + // GIFT_TYPE_SUBSCRIPTION: 'subscription', + // + // METHOD_BUY_GEMS: 'buyGems', + // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', + PAYMENT_METHOD: 'Stripe', + // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', +};