V3 payments 7 stripe (#7124)

* 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.

* basic controller for stripe payments

* authWithUrl is in

* stripe cleanup

* making tests pass

* stripe bug fixes

* 400 error is right

* cleanup of sinon spy for fakeSend

* paypal payments

* lint of paypal

* require -> import
This commit is contained in:
Victor Pudeyev
2016-04-30 09:42:10 -05:00
committed by Matteo Pagliazzi
parent fa21577c46
commit a567476bb7
22 changed files with 659 additions and 346 deletions

View File

@@ -19,7 +19,6 @@ website/src/routes/payments.js
website/src/routes/pages.js
website/src/middlewares/apiThrottle.js
website/src/middlewares/forceRefresh.js
website/src/controllers/top-level/payments/paypal.js
debug-scripts/*
tasks/*.js

View File

@@ -1,5 +1,6 @@
{
"missingAuthHeaders": "Missing authentication headers.",
"missingAuthParams": "Missing authentication parameters.",
"missingUsernameEmail": "Missing username or email.",
"missingEmail": "Missing email.",
"missingUsername": "Missing username.",
@@ -175,5 +176,7 @@
"equipmentAlreadyOwned": "You already own that piece of equipment",
"missingAccessToken": "The request is missing a required parameter : access_token",
"missingBillingAgreementId": "Missing billing agreement id",
"paymentNotSuccessful": "The payment was not successful"
"paymentNotSuccessful": "The payment was not successful",
"planNotActive": "The plan hasn't activated yet (due to a PayPal bug). It will begin <%= nextBillingDate %>, after which you can cancel to retain your full benefits",
"cancelingSubscription": "Canceling the subscription"
}

View File

@@ -359,7 +359,7 @@ gulp.task('test:api-v3:unit', (done) => {
});
gulp.task('test:api-v3:unit:watch', () => {
gulp.watch(['website/src/libs/api-v3/*', 'test/api/v3/unit/libs/*', 'website/src/controllers/**/*'], ['test:api-v3:unit']);
gulp.watch(['website/src/libs/api-v3/*', 'test/api/v3/unit/**/*', 'website/src/controllers/**/*'], ['test:api-v3:unit']);
});
gulp.task('test:api-v3:integration', (done) => {

View File

@@ -4,7 +4,7 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/payments/amazon/subscribeCancel';
let endpoint = '/payments/amazon/subscribe/cancel';
let user;
beforeEach(async () => {
@@ -13,9 +13,9 @@ describe('payments : amazon #subscribeCancel', () => {
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscription'),
code: 401,
error: 'NotAuthorized',
message: t('missingAuthParams'),
});
});
});

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : paypal #checkout', () => {
let endpoint = '/payments/paypal/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingAuthParams'),
});
});
});

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/payments/paypal/checkout/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('invalidCredentials'),
});
});
});

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : paypal #subscribe', () => {
let endpoint = '/payments/paypal/subscribe';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingAuthParams'),
});
});
});

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/payments/paypal/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingAuthParams'),
});
});
});

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/payments/paypal/subscribe/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('invalidCredentials'),
});
});
});

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/payments/stripe/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingAuthParams'),
});
});
});

View File

@@ -0,0 +1,17 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - paypal - #ipn', () => {
let endpoint = '/payments/paypal/ipn';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
let result = await user.post(endpoint);
expect(result).to.eql({});
});
});

View File

@@ -0,0 +1,20 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #checkout', () => {
let endpoint = '/payments/stripe/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'Error',
message: 'Invalid API Key provided: ****************************1111',
});
});
});

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/payments/stripe/subscribe/edit';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscription'),
});
});
});

View File

@@ -66,10 +66,7 @@ describe('payments/index', () => {
it('sends a text', async () => {
await api.cancelSubscription(data);
sinon.assert.calledOnce(fakeSend);
sinon.assert.called(fakeSend);
});
});
describe('#buyGems', async () => {
});
});

View File

@@ -2,6 +2,9 @@ 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';

View File

@@ -12,6 +12,8 @@ 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 };

View File

@@ -1,11 +1,11 @@
/*
import mongoose from 'mongoose';
import { model as User } from '../../../models/user'; */
import {
BadRequest,
} from '../../../libs/api-v3/errors';
import amzLib from '../../../libs/api-v3/amazonPayments';
import { authWithHeaders } from '../../../middlewares/api-v3/auth';
import {
authWithHeaders,
authWithUrl,
} from '../../../middlewares/api-v3/auth';
import shared from '../../../../../common';
import payments from '../../../libs/api-v3/payments';
import moment from 'moment';
@@ -16,7 +16,7 @@ import cc from 'coupon-code';
let api = {};
/**
* @api {post} /api/v3/payments/amazon/verifyAccessToken verify access token
* @api {post} /amazon/verifyAccessToken verify access token
* @apiVersion 3.0.0
* @apiName AmazonVerifyAccessToken
* @apiGroup Payments
@@ -40,7 +40,7 @@ api.verifyAccessToken = {
};
/**
* @api {post} /api/v3/payments/amazon/createOrderReferenceId create order reference id
* @api {post} /amazon/createOrderReferenceId create order reference id
* @apiVersion 3.0.0
* @apiName AmazonCreateOrderReferenceId
* @apiGroup Payments
@@ -70,7 +70,7 @@ api.createOrderReferenceId = {
};
/**
* @api {post} /api/v3/payments/amazon/checkout do checkout
* @api {post} /amazon/checkout do checkout
* @apiVersion 3.0.0
* @apiName AmazonCheckout
* @apiGroup Payments
@@ -148,7 +148,7 @@ api.checkout = {
};
/**
* @api {post} /api/v3/payments/amazon/subscribe Subscribe
* @api {post} /amazon/subscribe Subscribe
* @apiVersion 3.0.0
* @apiName AmazonSubscribe
* @apiGroup Payments
@@ -228,7 +228,7 @@ api.subscribe = {
};
/**
* @api {get} /api/v3/payments/amazon/subscribe/cancel SubscribeCancel
* @api {get} /amazon/subscribe/cancel SubscribeCancel
* @apiVersion 3.0.0
* @apiName AmazonSubscribe
* @apiGroup Payments
@@ -238,7 +238,7 @@ api.subscribe = {
api.subscribeCancel = {
method: 'GET',
url: '/payments/amazon/subscribe/cancel',
middlewares: [authWithHeaders()],
middlewares: [authWithUrl],
async handler (req, res) {
let user = res.locals.user;
let billingAgreementId = user.purchased.plan.customerId;

View File

@@ -1,218 +1,296 @@
var nconf = require('nconf');
var moment = require('moment');
var async = require('async');
var _ = require('lodash');
var url = require('url');
var User = require('mongoose').model('User');
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');
var shared = require('../../../../../common');
var mongoose = require('mongoose');
var cc = require('coupon-code');
import nconf from 'nconf';
import moment from 'moment';
import _ from 'lodash';
import payments from '../../../libs/api-v3/payments';
import ipn from 'paypal-ipn';
import paypal from 'paypal-rest-sdk';
import shared from '../../../../../common';
import cc from 'coupon-code';
import { model as Coupon } from '../../../models/coupon';
import { model as User } from '../../../models/user';
import {
authWithUrl,
authWithSession,
} from '../../../middlewares/api-v3/auth';
import {
BadRequest,
} from '../../../libs/api-v3/errors';
import * as logger from '../../../libs/api-v3/logger';
// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created
// there, get it's plan.id and store it in config.json
_.each(shared.content.subscriptionBlocks, function(block){
block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.key);
_.each(shared.content.subscriptionBlocks, (block) => {
block.paypalKey = nconf.get(`PAYPAL:billing_plans:${block.key}`);
});
/* eslint-disable camelcase */
paypal.configure({
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
'client_id': nconf.get("PAYPAL:client_id"),
'client_secret': nconf.get("PAYPAL:client_secret")
mode: nconf.get('PAYPAL:mode'), // sandbox or live
client_id: nconf.get('PAYPAL:client_id'),
client_secret: nconf.get('PAYPAL:client_secret'),
});
var parseErr = function (res, err) {
//var error = err.response ? err.response.message || err.response.details[0].issue : err;
var error = JSON.stringify(err);
return res.status(400).json({err:error});
}
/*
exports.createBillingAgreement = function(req,res,next){
var sub = shared.content.subscriptionBlocks[req.query.sub];
async.waterfall([
function(cb){
if (!sub.discount) return cb(null, null);
if (!req.query.coupon) return cb('Please provide a coupon code for this plan.');
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb);
},
function(coupon, cb){
if (sub.discount && !coupon) return cb('Invalid coupon code.');
var billingPlanTitle = "HabitRPG Subscription" + ' ($'+sub.price+' every '+sub.months+' months, recurring)';
var billingAgreementAttributes = {
"name": billingPlanTitle,
"description": billingPlanTitle,
"start_date": moment().add({minutes:5}).format(),
"plan": {
"id": sub.paypalKey
},
"payer": {
"payment_method": "paypal"
}
};
paypal.billingAgreement.create(billingAgreementAttributes, cb);
}
], function(err, billingAgreement){
if (err) return parseErr(res, err);
// For approving subscription via Paypal, first redirect user to: approval_url
req.session.paypalBlock = req.query.sub;
var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href;
res.redirect(approval_url);
});
}
exports.executeBillingAgreement = function(req,res,next){
var block = shared.content.subscriptionBlocks[req.session.paypalBlock];
delete req.session.paypalBlock;
async.auto({
exec: function (cb) {
paypal.billingAgreement.execute(req.query.token, {}, cb);
},
get_user: function (cb) {
User.findById(req.session.userId, cb);
},
create_sub: ['exec', 'get_user', function (cb, results) {
payments.createSubscription({
user: results.get_user,
customerId: results.exec.id,
paymentMethod: 'Paypal',
sub: block
}, cb);
}]
},function(err){
if (err) return parseErr(res, err);
res.redirect('/');
})
}
exports.createPayment = function(req, res) {
// if we're gifting to a user, put it in session for the `execute()`
req.session.gift = req.query.gift || undefined;
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
var price = !gift ? 5.00
: gift.type=='gems' ? Number(gift.gems.amount/4).toFixed(2)
: Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
var description = !gift ? "HabitRPG Gems"
: gift.type=='gems' ? "HabitRPG Gems (Gift)"
: shared.content.subscriptionBlocks[gift.subscription.key].months + "mo. HabitRPG Subscription (Gift)";
var create_payment = {
"intent": "sale",
"payer": {
"payment_method": "paypal"
},
"redirect_urls": {
"return_url": nconf.get('BASE_URL') + '/paypal/checkout/success',
"cancel_url": nconf.get('BASE_URL')
},
"transactions": [{
"item_list": {
"items": [{
"name": description,
//"sku": "1",
"price": price,
"currency": "USD",
"quantity": 1
}]
},
"amount": {
"currency": "USD",
"total": price
},
"description": description
}]
};
paypal.payment.create(create_payment, function (err, payment) {
if (err) return parseErr(res, err);
var link = _.find(payment.links, {rel: 'approval_url'}).href;
res.redirect(link);
});
}
exports.executePayment = function(req, res) {
var paymentId = req.query.paymentId,
PayerID = req.query.PayerID,
gift = req.session.gift ? JSON.parse(req.session.gift) : undefined;
delete req.session.gift;
async.waterfall([
function(cb){
paypal.payment.execute(paymentId, {payer_id: PayerID}, cb);
},
function(payment, cb){
async.parallel([
function(cb2){ User.findById(req.session.userId, cb2); },
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); }
], cb);
},
function(results, cb){
if (_.isEmpty(results[0])) return cb("User not found when completing paypal transaction");
var data = {user:results[0], customerId:PayerID, paymentMethod:'Paypal', gift:gift}
var method = 'buyGems';
if (gift) {
gift.member = results[1];
if (gift.type=='subscription') method = 'createSubscription';
data.paymentMethod = 'Gift';
}
payments[method](data, cb);
}
],function(err){
if (err) return parseErr(res, err);
res.redirect('/');
})
}
exports.cancelSubscription = function(req, res, next){
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.status(401).json({err: "User does not have a plan subscription"});
async.auto({
get_cus: function(cb){
paypal.billingAgreement.get(user.purchased.plan.customerId, cb);
},
verify_cus: ['get_cus', function(cb, results){
var hasntBilledYet = results.get_cus.agreement_details.cycles_completed == "0";
if (hasntBilledYet)
return cb("The plan hasn't activated yet (due to a PayPal bug). It will begin "+results.get_cus.agreement_details.next_billing_date+", after which you can cancel to retain your full benefits");
cb();
}],
del_cus: ['verify_cus', function(cb, results){
paypal.billingAgreement.cancel(user.purchased.plan.customerId, {note: "Canceling the subscription"}, cb);
}],
cancel_sub: ['get_cus', 'verify_cus', function(cb, results){
var data = {user: user, paymentMethod: 'Paypal', nextBill: results.get_cus.agreement_details.next_billing_date};
payments.cancelSubscription(data, cb)
}]
}, function(err){
if (err) return parseErr(res, err);
res.redirect('/');
user = null;
});
} // */
let api = {};
/**
* General IPN handler. We catch cancelled HabitRPG subscriptions for users who manually cancel their
* recurring paypal payments in their paypal dashboard. Remove this when we can move to webhooks or some other solution
*/
/*
exports.ipn = function(req, res, next) {
console.log('IPN Called');
res.sendStatus(200); // Must respond to PayPal IPN request with an empty 200 first
ipn.verify(req.body, function(err, msg) {
if (err) return logger.error(msg);
switch (req.body.txn_type) {
// TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead...
case 'recurring_payment_profile_cancel':
case 'subscr_cancel':
User.findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){
if (err) return logger.error(err);
if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel)
payments.cancelSubscription({user:user, paymentMethod: 'Paypal'});
});
break;
* @api {get} /paypal/checkout checkout
* @apiVersion 3.0.0
* @apiName PaypalCheckout
* @apiGroup Payments
*
* @apiParam {string} gift The stringified object representing the user, the gift recepient.
*
* @apiSuccess {} redirect
**/
api.checkout = {
method: 'GET',
url: '/payments/paypal/checkout',
middlewares: [authWithUrl],
async handler (req, res) {
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
req.session.gift = req.query.gift;
let amount = 5.00;
let description = 'HabitRPG gems';
if (gift) {
if (gift.type === 'gems') {
amount = Number(gift.gems.amount / 4).toFixed(2);
description = `${description} (Gift)`;
} else {
amount = Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
description = 'monthly HabitRPG Subscription (Gift)';
}
}
});
let createPayment = {
intent: 'sale',
payer: { payment_method: 'Paypal' },
redirect_urls: {
return_url: `${nconf.get('BASE_URL')}/paypal/checkout/success`,
cancel_url: `${nconf.get('BASE_URL')}`,
},
transactions: [{
item_list: {
items: [{
name: description,
price: amount,
currency: 'USD',
quality: 1,
}],
},
amount: {
currency: 'USD',
total: amount,
},
description,
}],
};
try {
let result = await paypal.payment.create(createPayment);
let link = _.find(result.links, { rel: 'approval_url' }).href;
res.redirect(link);
} catch (e) {
throw new BadRequest(e);
}
},
};
*/
/**
* @api {get} /paypal/checkout/success Paypal checkout success
* @apiVersion 3.0.0
* @apiName PaypalCheckoutSuccess
* @apiGroup Payments
*
* @apiParam {string} paymentId The payment id
* @apiParam {string} payerID The payer id, notice ID not id
*
* @apiSuccess {} redirect
**/
api.checkoutSuccess = {
method: 'GET',
url: '/payments/paypal/checkout/success',
middlewares: [authWithSession],
async handler (req, res) {
let paymentId = req.query.paymentId;
let customerId = req.query.payerID;
let method = 'buyGems';
let data = {
user: res.locals.user,
customerId,
paymentMethod: 'Paypal',
};
try {
let gift = req.session.gift ? JSON.parse(req.session.gift) : undefined;
delete req.session.gift;
if (gift) {
gift.member = await User.findById(gift.uuid);
if (gift.type === 'subscription') {
method = 'createSubscription';
data.paymentMethod = 'Gift';
}
data.gift = gift;
}
await paypal.payment.execute(paymentId, { payer_id: customerId });
await payments[method](data);
res.redirect('/');
} catch (e) {
throw new BadRequest(e);
}
},
};
/**
* @api {get} /paypal/subscribe Paypal subscribe
* @apiVersion 3.0.0
* @apiName PaypalSubscribe
* @apiGroup Payments
*
* @apiParam {string} sub subscription, possible values are: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo
* @apiParam {string} coupon coupon for the matching subscription, required only for certain subscriptions
*
* @apiSuccess {} empty object
**/
api.subscribe = {
method: 'GET',
url: '/payments/paypal/subscribe',
middlewares: [authWithUrl],
async handler (req, res) {
let sub = shared.content.subscriptionBlocks[req.query.sub];
if (sub.discount) {
if (!req.query.coupon) throw new BadRequest(res.t('couponCodeRequired'));
let coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key});
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
}
let billingPlanTitle = `HabitRPG Subscription ($${sub.price} every ${sub.months} months, recurring)`;
let billingAgreementAttributes = {
name: billingPlanTitle,
description: billingPlanTitle,
start_date: moment().add({ minutes: 5}).format(),
plan: {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
},
};
try {
let billingAgreement = await paypal.billingAgreement.create(billingAgreementAttributes);
req.session.paypalBlock = req.query.sub;
let link = _.find(billingAgreement.links, { rel: 'approval_url' }).href;
res.redirect(link);
} catch (e) {
throw new BadRequest(e);
}
},
};
/**
* @api {get} /paypal/subscribe/success Paypal subscribe success
* @apiVersion 3.0.0
* @apiName PaypalSubscribeSuccess
* @apiGroup Payments
*
* @apiParam {string} token The token in query
*
* @apiSuccess {} redirect
**/
api.subscribeSuccess = {
method: 'GET',
url: '/payments/paypal/subscribe/success',
middlewares: [authWithSession],
async handler (req, res) {
let user = res.locals.user;
let block = shared.content.subscriptionBlocks[req.session.paypalBlock];
delete req.session.paypalBlock;
try {
let result = await paypal.billingAgreement.execute(req.query.token, {});
await payments.createSubscription({
user,
customerId: result.id,
paymentMethod: 'Paypal',
sub: block,
});
res.redirect('/');
} catch (e) {
throw new BadRequest(e);
}
},
};
/**
* @api {get} /paypal/subscribe/cancel Paypal subscribe cancel
* @apiVersion 3.0.0
* @apiName PaypalSubscribeCancel
* @apiGroup Payments
*
* @apiParam {string} token The token in query
*
* @apiSuccess {} redirect
**/
api.subscribeCancel = {
method: 'GET',
url: '/payments/paypal/subscribe/cancel',
middlewares: [authWithUrl],
async handler (req, res) {
let user = res.locals.user;
let customerId = user.purchased.plan.customerId;
if (!user.purchased.plan.customerId) throw new BadRequest(res.t('missingSubscription'));
try {
let customer = await paypal.billingAgreement.get(customerId);
let nextBillingDate = customer.agreement_details.next_billing_date;
if (customer.agreement_details.cycles_completed === '0') { // hasn't billed yet
throw new BadRequest(res.t('planNotActive', { nextBillingDate }));
}
await paypal.billingAgreement.cancel(customerId, { note: res.t('cancelingSubscription') });
let data = {
user,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
};
await payments.cancelSubscription(data);
res.redirect('/');
} catch (e) {
throw new BadRequest(e);
}
},
};
/**
* @api {post} /paypal/ipn Paypal IPN
* @apiVersion 3.0.0
* @apiName PaypalIpn
* @apiGroup Payments
*
* @apiParam {string} txn_type txn_type
* @apiParam {string} recurring_payment_id recurring_payment_id
*
* @apiSuccess {} empty object
**/
api.ipn = {
method: 'POST',
url: '/payments/paypal/ipn',
middlewares: [],
async handler (req, res) {
res.respond(200);
try {
await ipn.verify(req.body);
if (req.body.txn_type === 'recurring_payment_profile_cancel' || req.body.txn_type === 'subscr_cancel') {
let user = await User.findOne({ 'purchased.plan.customerId': req.body.recurring_payment_id });
if (user) {
payments.cancelSubscriptoin({ user, paymentMethod: 'Paypal' });
}
}
} catch (e) {
logger.error(e);
}
},
};
/* eslint-disable camelcase */
module.exports = api;

View File

@@ -38,6 +38,7 @@ let billingPlanAttributes = {
cycles: '0',
}],
};
_.each(blocks, function defineBlock (block) {
block.definition = _.cloneDeep(billingPlanAttributes);
_.merge(block.definition.payment_definitions[0], {

View File

@@ -1,137 +1,167 @@
/* import nconf from 'nconf';
import stripeModule from 'stripe';
import async from 'async';
import payments from '../../../libs/api-v3/payments';
import { model as User } from '../../../models/user';
import shared from '../../../../../common';
import mongoose from 'mongoose';
import cc from 'coupon-code'; */
import {
BadRequest,
} from '../../../libs/api-v3/errors';
import { model as Coupon } from '../../../models/coupon';
import payments from '../../../libs/api-v3/payments';
import nconf from 'nconf';
import { model as User } from '../../../models/user';
import cc from 'coupon-code';
import {
authWithHeaders,
authWithUrl,
} from '../../../middlewares/api-v3/auth';
// const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
let api = {};
/*
Setup Stripe response when posting payment
*/
/*
api.checkout = function checkout (req, res) {
let token = req.body.id;
let user = res.locals.user;
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
async.waterfall([
function stripeCharge (cb) {
if (sub) {
async.waterfall([
function handleCoupon (cb2) {
if (!sub.discount) return cb2(null, null);
if (!req.query.coupon) return cb2('Please provide a coupon code for this plan.');
mongoose.model('Coupon').findOne({_id: cc.validate(req.query.coupon), event: sub.key}, cb2);
},
function createCustomer (coupon, cb2) {
if (sub.discount && !coupon) return cb2('Invalid coupon code.');
let customer = {
email: req.body.email,
metadata: {uuid: user._id},
card: token,
plan: sub.key,
};
stripe.customers.create(customer, cb2);
},
], cb);
} else {
let amount;
if (!gift) {
amount = '500';
} else if (gift.type === 'subscription') {
/**
* @api {post} /stripe/checkout Stripe checkout
* @apiVersion 3.0.0
* @apiName StripeCheckout
* @apiGroup Payments
*
* @apiParam {string} id The token
* @apiParam {string} gift stringified json object, gift
* @apiParam {string} sub subscription, possible values are: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo
* @apiParam {string} coupon coupon for the matching subscription, required only for certain subscriptions
* @apiParam {string} email the customer email
*
* @apiSuccess {} empty object
**/
api.checkout = {
method: 'POST',
url: '/payments/stripe/checkout',
middlewares: [authWithHeaders()],
async handler (req, res) {
let token = req.body.id;
let user = res.locals.user;
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
let coupon;
let response;
if (sub) {
if (sub.discount) {
if (!req.query.coupon) throw new BadRequest(res.t('couponCodeRequired'));
coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key});
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
}
let customer = {
email: req.body.email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
};
response = await stripe.customers.create(customer);
} else {
let amount = 500; // $5
if (gift) {
if (gift.type === 'subscription') {
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
} else {
amount = `${gift.gems.amount / 4 * 100}`;
}
stripe.charges.create({
amount,
currency: 'usd',
card: token,
}, cb);
}
},
function saveUserData (response, cb) {
if (sub) return payments.createSubscription({user, customerId: response.id, paymentMethod: 'Stripe', sub}, cb);
async.waterfall([
function findUser (cb2) {
User.findById(gift ? gift.uuid : undefined, cb2);
},
function prepData (member, cb2) {
let data = {user, customerId: response.id, paymentMethod: 'Stripe', gift};
let method = 'buyGems';
if (gift) {
gift.member = member;
if (gift.type === 'subscription') method = 'createSubscription';
data.paymentMethod = 'Gift';
}
payments[method](data, cb2);
},
], cb);
},
], function handleResponse (err) {
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.sendStatus(200);
user = token = null;
});
};
response = await stripe.charges.create({
amount,
currency: 'usd',
card: token,
});
}
api.subscribeCancel = function subscribeCancel (req, res) {
let user = res.locals.user;
if (!user.purchased.plan.customerId) {
return res.status(401).json({err: 'User does not have a plan subscription'});
}
async.auto({
getCustomer: function getCustomer (cb) {
stripe.customers.retrieve(user.purchased.plan.customerId, cb);
},
deleteCustomer: ['getCustomer', function deleteCustomer (cb) {
stripe.customers.del(user.purchased.plan.customerId, cb);
}],
cancelSubscription: ['getCustomer', function cancelSubscription (cb, results) {
if (sub) {
await payments.createSubscription({
user,
customerId: response.id,
paymentMethod: 'Stripe',
sub,
});
} else {
let method = 'buyGems';
let data = {
user,
nextBill: results.get_cus.subscription.current_period_end * 1000, // timestamp is in seconds
customerId: response.id,
paymentMethod: 'Stripe',
gift,
};
if (gift) {
let member = await User.findById(gift.uuid);
gift.member = member;
if (gift.type === 'subscription') method = 'createSubscription';
data.paymentMethod = 'Gift';
}
await payments[method](data);
}
res.respond(200, {});
},
};
/**
* @api {post} /stripe/subscribe/edit Stripe subscribeEdit
* @apiVersion 3.0.0
* @apiName StripeSubscribeEdit
* @apiGroup Payments
*
* @apiParam {string} id The token
*
* @apiSuccess {}
**/
api.subscribeEdit = {
method: 'POST',
url: '/payments/stripe/subscribe/edit',
middlewares: [authWithHeaders()],
async handler (req, res) {
let token = req.body.id;
let user = res.locals.user;
let customerId = user.purchased.plan.customerId;
if (!customerId) throw new BadRequest(res.t('missingSubscription'));
try {
let subscriptions = await stripe.customers.listSubscriptions(customerId);
let subscriptionId = subscriptions.data[0].id;
await stripe.customers.updateSubscription(customerId, subscriptionId, { card: token });
res.respond(200, {});
} catch (error) {
throw new BadRequest(error.message);
}
},
};
/**
* @api {get} /stripe/subscribe/cancel Stripe subscribeCancel
* @apiVersion 3.0.0
* @apiName StripeSubscribeCancel
* @apiGroup Payments
*
* @apiParam
*
* @apiSuccess {}
**/
api.subscribeCancel = {
method: 'GET',
url: '/payments/stripe/subscribe/cancel',
middlewares: [authWithUrl],
async handler (req, res) {
let user = res.locals.user;
if (!user.purchased.plan.customerId) throw new BadRequest(res.t('missingSubscription'));
try {
let customer = await stripe.customers.retrieve(user.purchased.plan.customeerId);
await stripe.customers.del(user.purchased.plan.customerId);
let data = {
user,
nextBill: customer.subscription.current_period_end * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
};
payments.cancelSubscription(data, cb);
}],
}, function handleResponse (err) {
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.redirect('/');
user = null;
});
await payments.cancelSubscriptoin(data);
res.respond(200, {});
} catch (e) {
throw new BadRequest(e);
}
},
};
api.subscribeEdit = function subscribeEdit (req, res) {
let token = req.body.id;
let user = res.locals.user;
let userId = user.purchased.plan.customerId;
let subscriptionId;
async.waterfall([
function listSubscriptions (cb) {
stripe.customers.listSubscriptions(userId, cb);
},
function updateSubscription (response, cb) {
subscriptionId = response.data[0].id;
stripe.customers.updateSubscription(userId, subscriptionId, { card: token }, cb);
},
function saveUser (response, cb) {
user.save(cb);
},
], function handleResponse (err) {
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.sendStatus(200);
token = user = userId = subscriptionId;
});
};
*/
module.exports = api;

View File

@@ -1,13 +1,11 @@
import _ from 'lodash' ;
import analytics from './analyticsService';
import cc from 'coupon-code';
import {
getUserInfo,
sendTxn as txnEmail,
} from './email';
import members from '../../controllers/api-v3/members';
import moment from 'moment';
import mongoose from 'mongoose';
import nconf from 'nconf';
import pushNotify from './pushNotifications';
import shared from '../../../../common' ;

View File

@@ -55,3 +55,21 @@ export function authWithSession (req, res, next) {
})
.catch(next);
}
export function authWithUrl (req, res, next) {
let userId = req.query._id;
let apiToken = req.query.apiToken;
if (!userId || !apiToken) {
throw new NotAuthorized(res.t('missingAuthParams'));
}
User.findOne({ _id: userId, apiToken }).exec()
.then((user) => {
if (!user) throw new NotAuthorized(res.t('invalidCredentials'));
res.locals.user = user;
next();
})
.catch(next);
}