From fa21577c46600b0a40fe11bae209aa27815d0ecc Mon Sep 17 00:00:00 2001 From: Victor Pudeyev Date: Wed, 27 Apr 2016 14:26:32 -0500 Subject: [PATCH] V3 payments 6 (#7104) * payments api: cancelSubscription * some more tests for amazon payments * promisifying amazon payments * somehow payment stub is not working * cleaning up tests * renaming tests in api/v3/integration/payments * improvements * cleanup, lint * fixes as per comments * moment.zone() is back in. --- common/locales/en/api-v3.json | 3 +- tasks/gulp-tests.js | 3 +- ...ET-payments_amazon_subscribeCancel.test.js | 21 ++ .../POST-payments_amazon_checkout.test.js | 22 ++ ...ents_amazon_createOrderReferenceId.test.js | 22 ++ .../POST-payments_amazon_subscribe.test.js | 21 ++ ...payments_amazon_verifyAccessToken.test.js} | 0 test/api/v3/unit/libs/amazonPayments.test.js | 103 ---------- test/api/v3/unit/libs/payments.test.js | 75 +++++++ test/api/v3/unit/libs/paymentsIndex.test.js | 11 - website/src/controllers/api-v3/auth.js | 2 - website/src/controllers/api-v3/chat.js | 2 - .../controllers/top-level/payments/amazon.js | 193 ++++++++---------- .../controllers/top-level/payments/paypal.js | 2 +- .../controllers/top-level/payments/stripe.js | 2 +- website/src/libs/api-v3/amazonPayments.js | 103 ++++------ .../index.js => libs/api-v3/payments.js} | 89 +++----- 17 files changed, 319 insertions(+), 355 deletions(-) create mode 100644 test/api/v3/integration/payments/GET-payments_amazon_subscribeCancel.test.js create mode 100644 test/api/v3/integration/payments/POST-payments_amazon_checkout.test.js create mode 100644 test/api/v3/integration/payments/POST-payments_amazon_createOrderReferenceId.test.js create mode 100644 test/api/v3/integration/payments/POST-payments_amazon_subscribe.test.js rename test/api/v3/integration/payments/{POST-payments_amazon_verify_access_token.test.js => POST-payments_amazon_verifyAccessToken.test.js} (100%) delete mode 100644 test/api/v3/unit/libs/amazonPayments.test.js create mode 100644 test/api/v3/unit/libs/payments.test.js delete mode 100644 test/api/v3/unit/libs/paymentsIndex.test.js rename website/src/{controllers/top-level/payments/index.js => libs/api-v3/payments.js} (74%) diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 5b1d57383e..0a3662bf80 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -100,6 +100,8 @@ "noAdminAccess": "You don't have admin access.", "pageMustBeNumber": "req.query.page must be a number", "missingUnsubscriptionCode": "Missing unsubscription code.", + "missingSubscription": "User does not have a plan subscription", + "missingSubscriptionCode": "Missing subscription code. Possible values: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo.", "userNotFound": "User not found.", "spellNotFound": "Spell \"<%= spellId %>\" not found.", "partyNotFound": "Party not found", @@ -173,6 +175,5 @@ "equipmentAlreadyOwned": "You already own that piece of equipment", "missingAccessToken": "The request is missing a required parameter : access_token", "missingBillingAgreementId": "Missing billing agreement id", - "missingAttributesFromAmazon": "Missing attributes from Amazon", "paymentNotSuccessful": "The payment was not successful" } diff --git a/tasks/gulp-tests.js b/tasks/gulp-tests.js index 2cdc301b44..a58113b77d 100644 --- a/tasks/gulp-tests.js +++ b/tasks/gulp-tests.js @@ -373,7 +373,8 @@ gulp.task('test:api-v3:integration', (done) => { }); gulp.task('test:api-v3:integration:watch', () => { - gulp.watch(['website/src/controllers/api-v3/**/*', 'test/api/v3/integration/**/*', 'common/script/ops/*'], ['test:api-v3:integration']); + gulp.watch(['website/src/controllers/api-v3/**/*', 'common/script/ops/*', 'website/src/libs/api-v3/*.js', + 'test/api/v3/integration/**/*'], ['test:api-v3:integration']); }); gulp.task('test:api-v3:integration:separate-server', (done) => { diff --git a/test/api/v3/integration/payments/GET-payments_amazon_subscribeCancel.test.js b/test/api/v3/integration/payments/GET-payments_amazon_subscribeCancel.test.js new file mode 100644 index 0000000000..66562c3721 --- /dev/null +++ b/test/api/v3/integration/payments/GET-payments_amazon_subscribeCancel.test.js @@ -0,0 +1,21 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('payments : amazon #subscribeCancel', () => { + let endpoint = '/payments/amazon/subscribeCancel'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies subscription', async () => { + await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingSubscription'), + }); + }); +}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_checkout.test.js b/test/api/v3/integration/payments/POST-payments_amazon_checkout.test.js new file mode 100644 index 0000000000..846416ed5d --- /dev/null +++ b/test/api/v3/integration/payments/POST-payments_amazon_checkout.test.js @@ -0,0 +1,22 @@ +import { + generateUser, +} from '../../../../helpers/api-integration/v3'; + +describe('payments - amazon - #checkout', () => { + let endpoint = '/payments/amazon/checkout'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies credentials', async (done) => { + try { + await user.post(endpoint); + } catch (e) { + expect(e.error).to.eql('BadRequest'); + expect(e.message.type).to.eql('InvalidParameterValue'); + done(); + } + }); +}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_createOrderReferenceId.test.js b/test/api/v3/integration/payments/POST-payments_amazon_createOrderReferenceId.test.js new file mode 100644 index 0000000000..3eb00b7c3c --- /dev/null +++ b/test/api/v3/integration/payments/POST-payments_amazon_createOrderReferenceId.test.js @@ -0,0 +1,22 @@ +import { + generateUser, +} from '../../../../helpers/api-integration/v3'; + +describe('payments - amazon - #createOrderReferenceId', () => { + let endpoint = '/payments/amazon/createOrderReferenceId'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies billingAgreementId', async (done) => { + try { + await user.post(endpoint); + } catch (e) { + // Parameter AWSAccessKeyId cannot be empty. + expect(e.error).to.eql('BadRequest'); + done(); + } + }); +}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_subscribe.test.js b/test/api/v3/integration/payments/POST-payments_amazon_subscribe.test.js new file mode 100644 index 0000000000..02a30a7ce5 --- /dev/null +++ b/test/api/v3/integration/payments/POST-payments_amazon_subscribe.test.js @@ -0,0 +1,21 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('payments - amazon - #subscribe', () => { + let endpoint = '/payments/amazon/subscribe'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies subscription code', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingSubscriptionCode'), + }); + }); +}); diff --git a/test/api/v3/integration/payments/POST-payments_amazon_verify_access_token.test.js b/test/api/v3/integration/payments/POST-payments_amazon_verifyAccessToken.test.js similarity index 100% rename from test/api/v3/integration/payments/POST-payments_amazon_verify_access_token.test.js rename to test/api/v3/integration/payments/POST-payments_amazon_verifyAccessToken.test.js diff --git a/test/api/v3/unit/libs/amazonPayments.test.js b/test/api/v3/unit/libs/amazonPayments.test.js deleted file mode 100644 index b2bf480e01..0000000000 --- a/test/api/v3/unit/libs/amazonPayments.test.js +++ /dev/null @@ -1,103 +0,0 @@ -import * as amzLib from '../../../../../website/src/libs/api-v3/amazonPayments'; -// import * as amzStub from 'amazon-payments'; -import amazonPayments from 'amazon-payments'; -var User = require('mongoose').model('User'); - -describe('amazonPayments', () => { - beforeEach(() => { - }); - - describe('#getTokenInfo stubbed', () => { - let thisToken = 'this token info'; - let amzOldConnect; - - beforeEach(() => { - amzOldConnect = amazonPayments.connect; - amazonPayments.connect = () => { - let api = { getTokenInfo: (token, cb) => { - return cb(undefined, thisToken); - } }; - return { api }; - }; - }); - - afterEach(() => { - amazonPayments.connect = amzOldConnect; - }); - - it('returns tokenInfo', async (done) => { - let result = await amzLib.getTokenInfo(); - expect(result).to.eql(thisToken); - done(); - }); - }); - - describe('#getTokenInfo', () => { - it('validates access_token parameter', async (done) => { - try { - await amzLib.getTokenInfo(); - } catch (e) { - expect(e.type).to.eql('invalid_request'); - done(); - } - }); - }); - - describe('#createOrderReferenceId', () => { - it('verifies billingAgreementId', async (done) => { - try { - let inputSet = {}; - delete inputSet.Id; - await amzLib.createOrderReferenceId(inputSet); - } catch (e) { - - /* console.log('error!', e); - console.log('error keys!', Object.keys(e)); - for (var key in e) { - console.log(e[key]); - } // */ - - expect(e.type).to.eql('InvalidParameterValue'); - expect(e.body.ErrorResponse.Error.Message).to.eql('Parameter AWSAccessKeyId cannot be empty.'); - done(); - } - }); - - xit('succeeds', () => { - }); - }); - - describe('#checkout', () => { - xit('succeeds'); - }); - - describe('#setOrderReferenceDetails', () => { - xit('succeeds'); - }); - - describe('#confirmOrderReference', () => { - xit('succeeds'); - }); - - describe('#authorize', () => { - xit('succeeds'); - - xit('was declined'); - - xit('had an error'); - }); - - describe('#closeOrderReference', () => { - xit('succeeds'); - }); - - describe.only('#executePayment', () => { - it('succeeds not as a gift', () => { - }); - - it('succeeds as a gift', () => { - }); - }); - - -}); diff --git a/test/api/v3/unit/libs/payments.test.js b/test/api/v3/unit/libs/payments.test.js new file mode 100644 index 0000000000..ad846e6488 --- /dev/null +++ b/test/api/v3/unit/libs/payments.test.js @@ -0,0 +1,75 @@ +import * as sender from '../../../../../website/src/libs/api-v3/email'; +import * as api from '../../../../../website/src/libs/api-v3/payments'; +import { model as User } from '../../../../../website/src/models/user'; +import moment from 'moment'; + +describe('payments/index', () => { + let fakeSend; + let data; + let user; + + describe('#createSubscription', () => { + beforeEach(async () => { + user = new User(); + }); + + it('succeeds', async () => { + data = { user, sub: { key: 'basic_3mo' } }; + expect(user.purchased.plan.planId).to.not.exist; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.exist; + }); + }); + + describe('#cancelSubscription', () => { + beforeEach(() => { + fakeSend = sinon.spy(sender, 'sendTxn'); + data = { user: new User() }; + }); + + afterEach(() => { + fakeSend.restore(); + }); + + it('plan.extraMonths is defined', () => { + api.cancelSubscription(data); + let terminated = data.user.purchased.plan.dateTerminated; + data.user.purchased.plan.extraMonths = 2; + api.cancelSubscription(data); + let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days')); + expect(difference - 60).to.be.lessThan(3); // the difference is approximately two months, +/- 2 days + }); + + it('plan.extraMonth is a fraction', () => { + api.cancelSubscription(data); + let terminated = data.user.purchased.plan.dateTerminated; + data.user.purchased.plan.extraMonths = 0.3; + api.cancelSubscription(data); + let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days')); + expect(difference - 10).to.be.lessThan(3); // the difference should be 10 days. + }); + + it('nextBill is defined', () => { + api.cancelSubscription(data); + let terminated = data.user.purchased.plan.dateTerminated; + data.nextBill = moment().add({ days: 25 }); + api.cancelSubscription(data); + let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days')); + expect(difference - 5).to.be.lessThan(2); // the difference should be 5 days, +/- 1 day + }); + + it('saves the canceled subscription for the user', () => { + expect(data.user.purchased.plan.dateTerminated).to.not.exist; + api.cancelSubscription(data); + expect(data.user.purchased.plan.dateTerminated).to.exist; + }); + + it('sends a text', async () => { + await api.cancelSubscription(data); + sinon.assert.calledOnce(fakeSend); + }); + }); + + describe('#buyGems', async () => { + }); +}); diff --git a/test/api/v3/unit/libs/paymentsIndex.test.js b/test/api/v3/unit/libs/paymentsIndex.test.js deleted file mode 100644 index 74b9d29273..0000000000 --- a/test/api/v3/unit/libs/paymentsIndex.test.js +++ /dev/null @@ -1,11 +0,0 @@ - -describe('payments/index', () => { - beforeEach(() => { - }); - - describe('#createSubscription', async () => { - }); - - describe('#buyGems', async () => { - }); -}); diff --git a/website/src/controllers/api-v3/auth.js b/website/src/controllers/api-v3/auth.js index 106cc95987..08c405424e 100644 --- a/website/src/controllers/api-v3/auth.js +++ b/website/src/controllers/api-v3/auth.js @@ -2,8 +2,6 @@ import validator from 'validator'; import moment from 'moment'; import passport from 'passport'; import nconf from 'nconf'; -import setupNconf from '../../libs/api-v3/setupNconf'; -setupNconf(); import { authWithHeaders, } from '../../middlewares/api-v3/auth'; diff --git a/website/src/controllers/api-v3/chat.js b/website/src/controllers/api-v3/chat.js index 6aa8319f7c..42864fdab7 100644 --- a/website/src/controllers/api-v3/chat.js +++ b/website/src/controllers/api-v3/chat.js @@ -12,8 +12,6 @@ import _ from 'lodash'; import { removeFromArray } from '../../libs/api-v3/collectionManipulators'; import { sendTxn } from '../../libs/api-v3/email'; import nconf from 'nconf'; -import setupNconf from '../../libs/api-v3/setupNconf'; -setupNconf(); const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => { return { email, canSend: true }; diff --git a/website/src/controllers/top-level/payments/amazon.js b/website/src/controllers/top-level/payments/amazon.js index 785efb06b9..74998a13f4 100644 --- a/website/src/controllers/top-level/payments/amazon.js +++ b/website/src/controllers/top-level/payments/amazon.js @@ -1,18 +1,17 @@ -/* import async from 'async'; -import cc from 'coupon-code'; +/* import mongoose from 'mongoose'; -import moment from 'moment'; -import payments from './index'; -import shared from '../../../../../common'; import { model as User } from '../../../models/user'; */ import { - // NotFound, - // NotAuthorized, BadRequest, } from '../../../libs/api-v3/errors'; import amzLib from '../../../libs/api-v3/amazonPayments'; import { authWithHeaders } from '../../../middlewares/api-v3/auth'; -var payments = require('./index'); +import shared from '../../../../../common'; +import payments from '../../../libs/api-v3/payments'; +import moment from 'moment'; +import { model as Coupon } from '../../../models/coupon'; +import { model as User } from '../../../models/user'; +import cc from 'coupon-code'; let api = {}; @@ -21,19 +20,22 @@ let api = {}; * @apiVersion 3.0.0 * @apiName AmazonVerifyAccessToken * @apiGroup Payments + * * @apiParam {string} access_token the access token + * * @apiSuccess {} empty **/ api.verifyAccessToken = { method: 'POST', url: '/payments/amazon/verifyAccessToken', + middlewares: [authWithHeaders()], async handler (req, res) { - await amzLib.getTokenInfo(req.body.access_token) - .then(() => { + try { + await amzLib.getTokenInfo(req.body.access_token); res.respond(200, {}); - }).catch((error) => { + } catch (error) { throw new BadRequest(error.body.error_description); - }); + } }, }; @@ -42,21 +44,21 @@ api.verifyAccessToken = { * @apiVersion 3.0.0 * @apiName AmazonCreateOrderReferenceId * @apiGroup Payments + * * @apiParam {string} billingAgreementId billing agreement id - * @apiSuccess {object} object containing { orderReferenceId } + * + * @apiSuccess {object} data.orderReferenceId The order reference id. **/ api.createOrderReferenceId = { method: 'POST', url: '/payments/amazon/createOrderReferenceId', - // middlewares: [authWithHeaders()], + middlewares: [authWithHeaders()], async handler (req, res) { - try { let response = await amzLib.createOrderReferenceId({ Id: req.body.billingAgreementId, IdType: 'BillingAgreement', ConfirmNow: false, - AWSAccessKeyId: 'something', }); res.respond(200, { orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId, @@ -64,7 +66,6 @@ api.createOrderReferenceId = { } catch (error) { throw new BadRequest(error); } - }, }; @@ -75,6 +76,7 @@ api.createOrderReferenceId = { * @apiGroup Payments * * @apiParam {string} billingAgreementId billing agreement id + * * @apiSuccess {object} object containing { orderReferenceId } **/ api.checkout = { @@ -95,10 +97,6 @@ api.checkout = { } } - /* if (!req.body || !req.body.orderReferenceId) { - return res.status(400).json({err: 'Billing Agreement Id not supplied.'}); - } */ - try { await amzLib.setOrderReferenceDetails({ AmazonOrderReferenceId: orderReferenceId, @@ -132,53 +130,57 @@ api.checkout = { await amzLib.closeOrderReference({ AmazonOrderReferenceId: orderReferenceId }); // execute payment - let giftUser = await User.findById(gift ? gift.uuid : undefined); - let data = { giftUser, paymentMethod: 'Amazon Payments' }; let method = 'buyGems'; + let data = { user, paymentMethod: 'Amazon Payments' }; if (gift) { if (gift.type === 'subscription') method = 'createSubscription'; - gift.member = giftUser; + gift.member = await User.findById(gift ? gift.uuid : undefined); data.gift = gift; data.paymentMethod = 'Gift'; } await payments[method](data); res.respond(200); - } catch(error) { + } catch (error) { throw new BadRequest(error); } + }, }; +/** + * @api {post} /api/v3/payments/amazon/subscribe Subscribe + * @apiVersion 3.0.0 + * @apiName AmazonSubscribe + * @apiGroup Payments + * + * @apiParam {string} billingAgreementId billing agreement id + * @apiParam {string} subscription Subscription plan + * @apiParam {string} coupon Coupon + * + * @apiSuccess {object} data.orderReferenceId The order reference id. + **/ +api.subscribe = { + method: 'POST', + url: '/payments/amazon/subscribe', + middlewares: [authWithHeaders()], + async handler (req, res) { + let billingAgreementId = req.body.billingAgreementId; + let sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false; + let coupon = req.body.coupon; + let user = res.locals.user; + if (!sub) { + throw new BadRequest(res.t('missingSubscriptionCode')); + } -/* -api.subscribe = function subscribe (req, res, next) { - if (!req.body || !req.body.billingAgreementId) { - return res.status(400).json({err: 'Billing Agreement Id not supplied.'}); - } + try { + if (sub.discount) { // apply discount + if (!coupon) throw new BadRequest(res.t('couponCodeRequired')); + let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}); + if (!result) throw new BadRequest(res.t('invalidCoupon')); + } - let billingAgreementId = req.body.billingAgreementId; - let sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false; - let coupon = req.body.coupon; - let user = res.locals.user; - - if (!sub) { - return res.status(400).json({err: 'Subscription plan not found.'}); - } - - async.series({ - applyDiscount (cb) { - if (!sub.discount) return cb(); - if (!coupon) return cb(new Error('Please provide a coupon code for this plan.')); - mongoose.model('Coupon').findOne({_id: cc.validate(coupon), event: sub.key}, function couponResult (err) { - if (err) return cb(err); - if (!coupon) return cb(new Error('Coupon code not found.')); - cb(); - }); - }, - - setBillingAgreementDetails (cb) { - amzPayment.offAmazonPayments.setBillingAgreementDetails({ + await amzLib.setBillingAgreementDetails({ AmazonBillingAgreementId: billingAgreementId, BillingAgreementAttributes: { SellerNote: 'HabitRPG Subscription', @@ -188,17 +190,13 @@ api.subscribe = function subscribe (req, res, next) { CustomInformation: 'HabitRPG Subscription', }, }, - }, cb); - }, + }); - confirmBillingAgreement (cb) { - amzPayment.offAmazonPayments.confirmBillingAgreement({ + await amzLib.confirmBillingAgreement({ AmazonBillingAgreementId: billingAgreementId, - }, cb); - }, + }); - authorizeOnBillingAgreement (cb) { - amzPayment.offAmazonPayments.authorizeOnBillingAgreement({ + await amzLib.authorizeOnBillingAgreement({ AmazonBillingAgreementId: billingAgreementId, AuthorizationReferenceId: shared.uuid().substring(0, 32), AuthorizationAmount: { @@ -213,68 +211,57 @@ api.subscribe = function subscribe (req, res, next) { SellerOrderId: shared.uuid(), StoreName: 'HabitRPG', }, - }, function billingAgreementResult (err) { - if (err) return cb(err); - - if (res.AuthorizationDetails.AuthorizationStatus.State === 'Declined') { - return cb(new Error('The payment was not successful.')); - } - - return cb(); }); - }, - createSubscription (cb) { - payments.createSubscription({ + await payments.createSubscription({ user, customerId: billingAgreementId, paymentMethod: 'Amazon Payments', sub, - }, cb); - }, - }, function subscribeResult (err) { - if (err) return next(err); + }); - res.sendStatus(200); - }); + res.respond(200); + } catch (error) { + throw new BadRequest(error); + } + }, }; -api.subscribeCancel = function subscribeCancel (req, res, next) { - let user = res.locals.user; - if (!user.purchased.plan.customerId) - return res.status(401).json({err: 'User does not have a plan subscription'}); +/** + * @api {get} /api/v3/payments/amazon/subscribe/cancel SubscribeCancel + * @apiVersion 3.0.0 + * @apiName AmazonSubscribe + * @apiGroup Payments + * + * @apiSuccess {object} empty object + **/ +api.subscribeCancel = { + method: 'GET', + url: '/payments/amazon/subscribe/cancel', + middlewares: [authWithHeaders()], + async handler (req, res) { + let user = res.locals.user; + let billingAgreementId = user.purchased.plan.customerId; - let billingAgreementId = user.purchased.plan.customerId; + if (!billingAgreementId) throw new BadRequest(res.t('missingSubscription')); - async.series({ - closeBillingAgreement (cb) { - amzPayment.offAmazonPayments.closeBillingAgreement({ + try { + await amzLib.closeBillingAgreement({ AmazonBillingAgreementId: billingAgreementId, - }, cb); - }, + }); - cancelSubscription (cb) { let data = { user, - // Date of next bill - nextBill: moment(user.purchased.plan.lastBillingDate).add({days: 30}), + nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: 30 }), paymentMethod: 'Amazon Payments', }; + await payments.cancelSubscription(data); - payments.cancelSubscription(data, cb); - }, - }, function subscribeCancelResult (err) { - if (err) return next(err); // don't json this, let toString() handle errors - - if (req.query.noRedirect) { - res.sendStatus(200); - } else { - res.redirect('/'); + res.respond(200, {}); + } catch (error) { + throw new BadRequest(error.message); } - - user = null; - }); + }, }; -*/ module.exports = api; diff --git a/website/src/controllers/top-level/payments/paypal.js b/website/src/controllers/top-level/payments/paypal.js index 05e8594ac1..c23fa6247d 100644 --- a/website/src/controllers/top-level/payments/paypal.js +++ b/website/src/controllers/top-level/payments/paypal.js @@ -4,7 +4,7 @@ var async = require('async'); var _ = require('lodash'); var url = require('url'); var User = require('mongoose').model('User'); -var payments = require('./index'); +var payments = require('../../../libs/api-v3/payments'); var logger = require('../../../libs/api-v2/logging'); var ipn = require('paypal-ipn'); var paypal = require('paypal-rest-sdk'); diff --git a/website/src/controllers/top-level/payments/stripe.js b/website/src/controllers/top-level/payments/stripe.js index 34bc598b3e..6a553dacd5 100644 --- a/website/src/controllers/top-level/payments/stripe.js +++ b/website/src/controllers/top-level/payments/stripe.js @@ -1,7 +1,7 @@ /* import nconf from 'nconf'; import stripeModule from 'stripe'; import async from 'async'; -import payments from './index'; +import payments from '../../../libs/api-v3/payments'; import { model as User } from '../../../models/user'; import shared from '../../../../../common'; import mongoose from 'mongoose'; diff --git a/website/src/libs/api-v3/amazonPayments.js b/website/src/libs/api-v3/amazonPayments.js index 3d92132f2f..38403ba4d0 100644 --- a/website/src/libs/api-v3/amazonPayments.js +++ b/website/src/libs/api-v3/amazonPayments.js @@ -3,65 +3,41 @@ import nconf from 'nconf'; import common from '../../../../common'; let t = common.i18n.t; const IS_PROD = nconf.get('NODE_ENV') === 'production'; +import Q from 'q'; -let api = {}; +let amzPayment = amazonPayments.connect({ + environment: amazonPayments.Environment[IS_PROD ? 'Production' : 'Sandbox'], + sellerId: nconf.get('AMAZON_PAYMENTS:SELLER_ID'), + mwsAccessKey: nconf.get('AMAZON_PAYMENTS:MWS_KEY'), + mwsSecretKey: nconf.get('AMAZON_PAYMENTS:MWS_SECRET'), + clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'), +}); -function connect (amazonPayments) { // eslint-disable-line no-shadow - return amazonPayments.connect({ - environment: amazonPayments.Environment[IS_PROD ? 'Production' : 'Sandbox'], - sellerId: nconf.get('AMAZON_PAYMENTS:SELLER_ID'), - mwsAccessKey: nconf.get('AMAZON_PAYMENTS:MWS_KEY'), - mwsSecretKey: nconf.get('AMAZON_PAYMENTS:MWS_SECRET'), - clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'), - }); -} +/** + * From: https://payments.amazon.com/documentation/apireference/201751670#201751670 + */ -api.getTokenInfo = (token) => { - let amzPayment = connect(amazonPayments); +let getTokenInfo = Q.nbind(amzPayment.api.getTokenInfo, amzPayment.api); +let createOrderReferenceId = Q.nbind(amzPayment.offAmazonPayments.createOrderReferenceForId, amzPayment.offAmazonPayments); +let setOrderReferenceDetails = Q.nbind(amzPayment.offAmazonPayments.setOrderReferenceDetails, amzPayment.offAmazonPayments); +let confirmOrderReference = Q.nbind(amzPayment.offAmazonPayments.confirmOrderReference, amzPayment.offAmazonPayments); +let closeOrderReference = Q.nbind(amzPayment.offAmazonPayments.closeOrderReference, amzPayment.offAmazonPayments); +let setBillingAgreementDetails = Q.nbind(amzPayment.offAmazonPayments.setBillingAgreementDetails, amzPayment.offAmazonPayments); +let confirmBillingAgreement = Q.nbind(amzPayment.offAmazonPayments.confirmBillingAgreement, amzPayment.offAmazonPayments); +let closeBillingAgreement = Q.nbind(amzPayment.offAmazonPayments.closeBillingAgreement, amzPayment.offAmazonPayments); + +let authorizeOnBillingAgreement = (inputSet) => { return new Promise((resolve, reject) => { - amzPayment.api.getTokenInfo(token, (err, tokenInfo) => { + amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => { if (err) return reject(err); - return resolve(tokenInfo); - }); - }); -}; - -api.createOrderReferenceId = (inputSet) => { - let amzPayment = connect(amazonPayments); - return new Promise((resolve, reject) => { - amzPayment.offAmazonPayments.createOrderReferenceForId(inputSet, (err, response) => { - if (err) return reject(err); - if (!response.OrderReferenceDetails || !response.OrderReferenceDetails.AmazonOrderReferenceId) { - return reject(t('missingAttributesFromAmazon')); - } + if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(t('paymentNotSuccessful')); return resolve(response); }); }); }; -api.setOrderReferenceDetails = (inputSet) => { - let amzPayment = connect(amazonPayments); +let authorize = (inputSet) => { return new Promise((resolve, reject) => { - amzPayment.offAmazonPayments.setOrderReferenceDetails(inputSet, (err, response) => { - if (err) return reject(err); - return resolve(response); - }); - }); -}; - -api.confirmOrderReference = (inputSet) => { - let amzPayment = connect(amazonPayments); - return new Promise((resolve, reject) => { - amzPayment.offAmazonPayments.confirmOrderReference(inputSet, (err, response) => { - if (err) return reject(err); - return resolve(response); - }); - }); -}; - -api.authorize = (inputSet) => { - let amzPayment = connect(amazonPayments); - return new Promize((resolve, reject) => { amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => { if (err) return reject(err); if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(t('paymentNotSuccessful')); @@ -70,24 +46,15 @@ api.authorize = (inputSet) => { }); }; -api.closeOrderReference = (inputSet) => { - let amzPayment = connect(amazonPayments); - return new Promize((resolve, reject) => { - amzPayment.offAmazonPayments.closeOrderReference(inputSet, (err, response) => { - if (err) return reject(err); - return resolve(response); - }); - }); +module.exports = { + getTokenInfo, + createOrderReferenceId, + setOrderReferenceDetails, + confirmOrderReference, + closeOrderReference, + confirmBillingAgreement, + setBillingAgreementDetails, + closeBillingAgreement, + authorizeOnBillingAgreement, + authorize, }; - -api.executePayment = (inputSet) => { - let amzPayment = connect(amazonPayments); - return new Promize((resolve, reject) => { - amzPayment.offAmazonPayments.closeOrderReference(inputSet, (err, response) => { - if (err) return reject(err); - return resolve(response); - }); - }); -}; - -module.exports = api; diff --git a/website/src/controllers/top-level/payments/index.js b/website/src/libs/api-v3/payments.js similarity index 74% rename from website/src/controllers/top-level/payments/index.js rename to website/src/libs/api-v3/payments.js index 6ee56bf0d2..0e94153150 100644 --- a/website/src/controllers/top-level/payments/index.js +++ b/website/src/libs/api-v3/payments.js @@ -1,24 +1,22 @@ import _ from 'lodash' ; -import analytics from '../../../libs/api-v3/analyticsService'; -import async from 'async'; +import analytics from './analyticsService'; import cc from 'coupon-code'; import { getUserInfo, sendTxn as txnEmail, -} from '../../../libs/api-v3/email'; -import members from '../../api-v3/members'; +} from './email'; +import members from '../../controllers/api-v3/members'; import moment from 'moment'; import mongoose from 'mongoose'; import nconf from 'nconf'; -import pushNotify from '../../../libs/api-v3/pushNotifications'; -import shared from '../../../../../common' ; +import pushNotify from './pushNotifications'; +import shared from '../../../../common' ; -import amazon from './amazon'; -import iap from './iap'; -import paypal from './paypal'; -import stripe from './stripe'; +import iap from '../../controllers/top-level/payments/iap'; +import paypal from '../../controllers/top-level/payments/paypal'; +import stripe from '../../controllers/top-level/payments/stripe'; -const IS_PROD = nconf.get('NODE_ENV') === 'production'; +const IS_PROD = nconf.get('IS_PROD'); let api = {}; @@ -36,10 +34,7 @@ function revealMysteryItems (user) { }); } -// @TODO: HEREHERE api.createSubscription = async function createSubscription (data) { -} -api.createSubscription = function createSubscription (data, cb) { let recipient = data.gift ? data.gift.member : data.user; let plan = recipient.purchased.plan; let block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key]; @@ -81,6 +76,7 @@ api.createSubscription = function createSubscription (data, cb) { plan.consecutive.trinkets += perks; } revealMysteryItems(recipient); + if (IS_PROD) { if (!data.gift) txnEmail(data.user, 'subscription-begins'); @@ -96,7 +92,9 @@ api.createSubscription = function createSubscription (data, cb) { }; analytics.trackPurchase(analyticsData); } + data.user.purchased.txnCount++; + if (data.gift) { members.sendMessage(data.user, data.gift.member, data.gift); @@ -113,50 +111,41 @@ api.createSubscription = function createSubscription (data, cb) { pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`); } } - async.parallel([ - function saveGiftingUserData (cb2) { - data.user.save(cb2); - }, - function saveRecipientUserData (cb2) { - if (data.gift) { - data.gift.member.save(cb2); - } else { - cb2(null); - } - }, - ], cb); + + await data.user.save(); + if (data.gift) await data.gift.member.save(); }; /** * Sets their subscription to be cancelled later */ -api.cancelSubscription = function cancelSubscription (data, cb) { +api.cancelSubscription = async function cancelSubscription (data) { let plan = data.user.purchased.plan; let now = moment(); let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30; + let nowStr = `${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`; + let nowStrFormat = 'MM/DD/YYYY'; plan.dateTerminated = - moment(`${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`) + moment(nowStr, nowStrFormat) .add({days: remaining}) // end their subscription 1mo from their last payment - .add({months: Math.ceil(plan.extraMonths)})// plus any extra time (carry-over, gifted subscription, etc) they have. FIXME: moment can't add months in fractions... + .add({days: Math.ceil(30 * plan.extraMonths)}) // plus any extra time (carry-over, gifted subscription, etc) they have. .toDate(); plan.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated - data.user.save(cb); + await data.user.save(); + txnEmail(data.user, 'cancel-subscription'); - let analyticsData = { + + analytics.track('unsubscribe', { uuid: data.user._id, gaCategory: 'commerce', gaLabel: data.paymentMethod, paymentMethod: data.paymentMethod, - }; - analytics.track('unsubscribe', analyticsData); + }); }; -// @TODO: HEREHERE api.buyGems = async function buyGems (data) { -}; -api.buyGems = function buyGems (data, cb) { let amt = data.amount || 5; amt = data.gift ? data.gift.gems.amount / 4 : amt; (data.gift ? data.gift.member : data.user).balance += amt; @@ -192,27 +181,9 @@ api.buyGems = function buyGems (data, cb) { if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`); } + await data.gift.member.save(); } - async.parallel([ - function saveGiftingUserData (cb2) { - data.user.save(cb2); - }, - function saveRecipientUserData (cb2) { - if (data.gift) { - data.gift.member.save(cb2); - } else { - cb2(null); - } - }, - ], cb); -}; - -api.validCoupon = function validCoupon (req, res, next) { - mongoose.model('Coupon').findOne({_id: cc.validate(req.params.code), event: 'google_6mo'}, function couponErrorCheck (err, coupon) { - if (err) return next(err); - if (!coupon) return res.status(401).json({err: 'Invalid coupon code'}); - return res.sendStatus(200); - }); + await data.user.save(); }; api.stripeCheckout = stripe.checkout; @@ -226,12 +197,6 @@ api.paypalCheckout = paypal.createPayment; api.paypalCheckoutSuccess = paypal.executePayment; api.paypalIPN = paypal.ipn; -api.amazonVerifyAccessToken = amazon.verifyAccessToken; -api.amazonCreateOrderReferenceId = amazon.createOrderReferenceId; -api.amazonCheckout = amazon.checkout; -api.amazonSubscribe = amazon.subscribe; -api.amazonSubscribeCancel = amazon.subscribeCancel; - api.iapAndroidVerify = iap.androidVerify; api.iapIosVerify = iap.iosVerify;