working on promisifying amazonPayments

This commit is contained in:
Victor Piousbox
2016-04-14 22:38:41 +00:00
parent 1d5d6e9146
commit 261a5a66b1
14 changed files with 521 additions and 1167 deletions

View File

@@ -170,5 +170,10 @@
"pushDeviceAdded": "Push device added successfully",
"pushDeviceAlreadyAdded": "The user already has the push device",
"resetComplete": "Reset completed",
"lvl10ChangeClass": "To change class you must be at least level 10."
"lvl10ChangeClass": "To change class you must be at least level 10.",
"resetComplete": "Reset has completed",
"missingAccessToken": "The request is missing a required parameter : access_token",
"missingBillingAgreementId": "Missing billing agreement id",
"missingAttributesFromAmazon": "Missing attributes from Amazon",
"errorFromAmazon": "Error from Amazon"
}

View File

@@ -358,6 +358,10 @@ gulp.task('test:api-v3:unit', (done) => {
pipe(runner);
});
gulp.task('test:api-v3:unit:watch', () => {
gulp.watch(['website/src/libs/api-v3/*', 'test/api/v3/unit/libs/*'], ['test:api-v3:unit']);
});
gulp.task('test:api-v3:integration', (done) => {
let runner = exec(
testBin('mocha test/api/v3/integration --recursive'),

View File

@@ -0,0 +1,21 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : amazon', () => {
let endpoint = '/payments/amazon/verifyAccessToken';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verify access token', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingAccessToken'),
});
});
});

View File

@@ -0,0 +1,24 @@
import * as amz from '../../../../../website/src/libs/api-v3/amazonPayments';
describe.only('amazonPayments', () => {
beforeEach(() => {
});
describe('#getTokenInfo', () => {
it('validates access_token parameter', async (done) => {
try {
let result = await amz.getTokenInfo();
} catch (e) {
expect(e.type).to.eql('invalid_request');
done();
}
});
});
describe('#createOrderReferenceId', () => {
it('is sane', () => {
expect(false).to.eql(true); // @TODO
});
});
});

View File

@@ -1,277 +0,0 @@
import amazonPayments from 'amazon-payments';
import async from 'async';
import cc from 'coupon-code';
import mongoose from 'mongoose';
import moment from 'moment';
import nconf from 'nconf';
import payments from './index';
import shared from '../../../../../common';
import { model as User } from '../../models/user';
const IS_PROD = nconf.get('NODE_ENV') === 'production';
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'),
});
api.verifyAccessToken = function verifyAccessToken (req, res) {
if (!req.body || !req.body.access_token) {
return res.status(400).json({err: 'Access token not supplied.'});
}
amzPayment.api.getTokenInfo(req.body.access_token, function getTokenInfo (err) {
if (err) return res.status(400).json({err});
res.sendStatus(200);
});
};
api.createOrderReferenceId = function createOrderReferenceId (req, res, next) {
if (!req.body || !req.body.billingAgreementId) {
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
}
amzPayment.offAmazonPayments.createOrderReferenceForId({
Id: req.body.billingAgreementId,
IdType: 'BillingAgreement',
ConfirmNow: false,
}, function createOrderReferenceForId (err, response) {
if (err) return next(err);
if (!response.OrderReferenceDetails || !response.OrderReferenceDetails.AmazonOrderReferenceId) {
return next(new Error('Missing attributes in Amazon response.'));
}
res.json({
orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId,
});
});
};
api.checkout = function checkout (req, res, next) {
if (!req.body || !req.body.orderReferenceId) {
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
}
let gift = req.body.gift;
let user = res.locals.user;
let orderReferenceId = req.body.orderReferenceId;
let amount = 5;
if (gift) {
if (gift.type === 'gems') {
amount = gift.gems.amount / 4;
} else if (gift.type === 'subscription') {
amount = shared.content.subscriptionBlocks[gift.subscription.key].price;
}
}
async.series({
setOrderReferenceDetails (cb) {
amzPayment.offAmazonPayments.setOrderReferenceDetails({
AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: {
OrderTotal: {
CurrencyCode: 'USD',
Amount: amount,
},
SellerNote: 'HabitRPG Payment',
SellerOrderAttributes: {
SellerOrderId: shared.uuid(),
StoreName: 'HabitRPG',
},
},
}, cb);
},
confirmOrderReference (cb) {
amzPayment.offAmazonPayments.confirmOrderReference({
AmazonOrderReferenceId: orderReferenceId,
}, cb);
},
authorize (cb) {
amzPayment.offAmazonPayments.authorize({
AmazonOrderReferenceId: orderReferenceId,
AuthorizationReferenceId: shared.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: 'USD',
Amount: amount,
},
SellerAuthorizationNote: 'HabitRPG Payment',
TransactionTimeout: 0,
CaptureNow: true,
}, function checkAuthorizationStatus (err) {
if (err) return cb(err);
if (res.AuthorizationDetails.AuthorizationStatus.State === 'Declined') {
return cb(new Error('The payment was not successfull.'));
}
return cb();
});
},
closeOrderReference (cb) {
amzPayment.offAmazonPayments.closeOrderReference({
AmazonOrderReferenceId: orderReferenceId,
}, cb);
},
executePayment (cb) {
async.waterfall([
function findUser (cb2) {
User.findById(gift ? gift.uuid : undefined, cb2);
},
function executeAmazonPayment (member, cb2) {
let data = {user, paymentMethod: 'Amazon Payments'};
let method = 'buyGems';
if (gift) {
if (gift.type === 'subscription') method = 'createSubscription';
gift.member = member;
data.gift = gift;
data.paymentMethod = 'Gift';
}
payments[method](data, cb2);
},
], cb);
},
}, function result (err) {
if (err) return next(err);
res.sendStatus(200);
});
};
api.subscribe = function subscribe (req, res, next) {
if (!req.body || !req.body.billingAgreementId) {
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
}
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({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: 'HabitRPG Subscription',
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: shared.uuid(),
StoreName: 'HabitRPG',
CustomInformation: 'HabitRPG Subscription',
},
},
}, cb);
},
confirmBillingAgreement (cb) {
amzPayment.offAmazonPayments.confirmBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
}, cb);
},
authorizeOnBillingAgreement (cb) {
amzPayment.offAmazonPayments.authorizeOnBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: shared.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: 'USD',
Amount: sub.price,
},
SellerAuthorizationNote: 'HabitRPG Subscription Payment',
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: 'HabitRPG Subscription Payment',
SellerOrderAttributes: {
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({
user,
customerId: billingAgreementId,
paymentMethod: 'Amazon Payments',
sub,
}, cb);
},
}, function subscribeResult (err) {
if (err) return next(err);
res.sendStatus(200);
});
};
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'});
let billingAgreementId = user.purchased.plan.customerId;
async.series({
closeBillingAgreement (cb) {
amzPayment.offAmazonPayments.closeBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
}, cb);
},
cancelSubscription (cb) {
let data = {
user,
// Date of next bill
nextBill: moment(user.purchased.plan.lastBillingDate).add({days: 30}),
paymentMethod: 'Amazon Payments',
};
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('/');
}
user = null;
});
};
module.exports = api;

View File

@@ -1,158 +0,0 @@
import {
iap,
inAppPurchase, }
from 'in-app-purchase';
import payments from './index';
import nconf from 'nconf';
inAppPurchase.config({
// this is the path to the directory containing iap-sanbox/iap-live files
googlePublicKeyPath: nconf.get('IAP_GOOGLE_KEYDIR'),
});
// Validation ERROR Codes
const INVALID_PAYLOAD = 6778001;
/* const CONNECTION_FAILED = 6778002;
const PURCHASE_EXPIRED = 6778003; */ // These variables were never used??
let api = {};
api.androidVerify = function androidVerify (req, res) {
let iapBody = req.body;
let user = res.locals.user;
iap.setup(function googleSetupResult (error) {
if (error) {
let resObj = {
ok: false,
data: 'IAP Error',
};
return res.json(resObj);
}
/*
google receipt must be provided as an object
{
"data": "{stringified data object}",
"signature": "signature from google"
}
*/
let testObj = {
data: iapBody.transaction.receipt,
signature: iapBody.transaction.signature,
};
// iap is ready
iap.validate(iap.GOOGLE, testObj, function googleValidateResult (err, googleRes) {
if (err) {
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString(),
},
};
return res.json(resObj);
}
if (iap.isValidated(googleRes)) {
let resObj = {
ok: true,
data: googleRes,
};
payments.buyGems({user, paymentMethod: 'IAP GooglePlay', amount: 5.25});
return res.json(resObj);
}
});
});
};
exports.iosVerify = function iosVerify (req, res) {
let iapBody = req.body;
let user = res.locals.user;
iap.setup(function iosSetupResult (error) {
if (error) {
let resObj = {
ok: false,
data: 'IAP Error',
};
return res.json(resObj);
}
// iap is ready
iap.validate(iap.APPLE, iapBody.transaction.receipt, function iosValidateResult (err, appleRes) {
if (err) {
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString(),
},
};
return res.json(resObj);
}
if (iap.isValidated(appleRes)) {
let purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length > 0) {
let correctReceipt = true;
for (let index of purchaseDataList) {
switch (purchaseDataList[index].productId) {
case 'com.habitrpg.ios.Habitica.4gems':
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1});
break;
case 'com.habitrpg.ios.Habitica.8gems':
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2});
break;
case 'com.habitrpg.ios.Habitica.20gems':
case 'com.habitrpg.ios.Habitica.21gems':
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25});
break;
case 'com.habitrpg.ios.Habitica.42gems':
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5});
break;
default:
correctReceipt = false;
}
}
if (correctReceipt) {
let resObj = {
ok: true,
data: appleRes,
};
// yay good!
return res.json(resObj);
}
}
// wrong receipt content
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: 'Incorrect receipt content',
},
};
return res.json(resObj);
}
// invalid receipt
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: 'Invalid receipt',
},
};
return res.json(resObj);
});
});
};
module.exports = api;

View File

@@ -1,232 +0,0 @@
import _ from 'lodash' ;
import analytics from '../../../libs/api-v3/analyticsService';
import async from 'async';
import cc from 'coupon-code';
import {
getUserInfo,
sendTxn as txnEmail,
} from '../../../libs/api-v3/email';
import members from '../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 amazon from './amazon';
import iap from './iap';
import paypal from './paypal';
import stripe from './stripe';
const IS_PROD = nconf.get('NODE_ENV') === 'production';
let api = {};
function revealMysteryItems (user) {
_.each(shared.content.gear.flat, function findMysteryItems (item) {
if (
item.klass === 'mystery' &&
moment().isAfter(shared.content.mystery[item.mystery].start) &&
moment().isBefore(shared.content.mystery[item.mystery].end) &&
!user.items.gear.owned[item.key] &&
user.purchased.plan.mysteryItems.indexOf(item.key) !== -1
) {
user.purchased.plan.mysteryItems.push(item.key);
}
});
}
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];
let months = Number(block.months);
if (data.gift) {
if (plan.customerId && !plan.dateTerminated) { // User has active plan
plan.extraMonths += months;
} else {
plan.dateTerminated = moment(plan.dateTerminated).add({months}).toDate();
if (!plan.dateUpdated) plan.dateUpdated = new Date();
}
if (!plan.customerId) plan.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId
} else {
_(plan).merge({ // override with these values
planId: block.key,
customerId: data.customerId,
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: data.paymentMethod,
extraMonths: Number(plan.extraMonths) +
Number(plan.dateTerminated ? moment(plan.dateTerminated).diff(new Date(), 'months', true) : 0),
dateTerminated: null,
// Specify a lastBillingDate just for Amazon Payments
// Resetted every time the subscription restarts
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? new Date() : undefined,
}).defaults({ // allow non-override if a plan was previously used
dateCreated: new Date(),
mysteryItems: [],
}).value();
}
// Block sub perks
let perks = Math.floor(months / 3);
if (perks) {
plan.consecutive.offset += months;
plan.consecutive.gemCapExtra += perks * 5;
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
plan.consecutive.trinkets += perks;
}
revealMysteryItems(recipient);
if (IS_PROD) {
if (!data.gift) txnEmail(data.user, 'subscription-begins');
let analyticsData = {
uuid: data.user._id,
itemPurchased: 'Subscription',
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: block.price,
};
analytics.trackPurchase(analyticsData);
}
data.user.purchased.txnCount++;
if (data.gift) {
members.sendMessage(data.user, data.gift.member, data.gift);
let byUserName = getUserInfo(data.user, ['name']).name;
if (data.gift.member.preferences.emailNotifications.giftedSubscription !== false) {
txnEmail(data.gift.member, 'gifted-subscription', [
{name: 'GIFTER', content: byUserName},
{name: 'X_MONTHS_SUBSCRIPTION', content: months},
]);
}
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('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);
};
/**
* Sets their subscription to be cancelled later
*/
api.cancelSubscription = function cancelSubscription (data, cb) {
let plan = data.user.purchased.plan;
let now = moment();
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
plan.dateTerminated =
moment(`${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`)
.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...
.toDate();
plan.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
data.user.save(cb);
txnEmail(data.user, 'cancel-subscription');
let analyticsData = {
uuid: data.user._id,
gaCategory: 'commerce',
gaLabel: data.paymentMethod,
paymentMethod: data.paymentMethod,
};
analytics.track('unsubscribe', analyticsData);
};
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;
data.user.purchased.txnCount++;
if (IS_PROD) {
if (!data.gift) txnEmail(data.user, 'donation');
let analyticsData = {
uuid: data.user._id,
itemPurchased: 'Gems',
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
purchaseType: 'checkout',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: amt,
};
analytics.trackPurchase(analyticsData);
}
if (data.gift) {
let byUsername = getUserInfo(data.user, ['name']).name;
let gemAmount = data.gift.gems.amount || 20;
members.sendMessage(data.user, data.gift.member, data.gift);
if (data.gift.member.preferences.emailNotifications.giftedGems !== false) {
txnEmail(data.gift.member, 'gifted-gems', [
{name: 'GIFTER', content: byUsername},
{name: 'X_GEMS_GIFTED', content: gemAmount},
]);
}
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}`);
}
}
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);
});
};
api.stripeCheckout = stripe.checkout;
api.stripeSubscribeCancel = stripe.subscribeCancel;
api.stripeSubscribeEdit = stripe.subscribeEdit;
api.paypalSubscribe = paypal.createBillingAgreement;
api.paypalSubscribeSuccess = paypal.executeBillingAgreement;
api.paypalSubscribeCancel = paypal.cancelSubscription;
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;
module.exports = api;

View File

@@ -1,135 +0,0 @@
import nconf from 'nconf';
import stripeModule from 'stripe';
import async from 'async';
import payments from './index';
import { model as User } from '../../models/user';
import shared from '../../../../../common';
import mongoose from 'mongoose';
import cc from 'coupon-code';
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') {
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;
});
};
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) {
let data = {
user,
nextBill: results.get_cus.subscription.current_period_end * 1000, // timestamp is 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;
});
};
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,112 +1,128 @@
var amazonPayments = require('amazon-payments');
var mongoose = require('mongoose');
var moment = require('moment');
var nconf = require('nconf');
var async = require('async');
var User = require('mongoose').model('User');
var shared = require('../../../../common');
var payments = require('./index');
var cc = require('coupon-code');
var isProd = nconf.get('NODE_ENV') === 'production';
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 amz from '../../../libs/api-v3/amazonPayments';
var amzPayment = amazonPayments.connect({
environment: amazonPayments.Environment[isProd ? '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')
});
let api = {};
exports.verifyAccessToken = function(req, res, next){
if(!req.body || !req.body['access_token']){
return res.status(400).json({err: 'Access token not supplied.'});
}
amzPayment.api.getTokenInfo(req.body['access_token'], function(err, tokenInfo){
if(err) return res.status(400).json({err:err});
res.sendStatus(200);
/**
* @api {post} /api/v3/payments/amazon/verifyAccessToken verify access token
* @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',
async handler (req, res) {
await amz.getTokenInfo(req.body.access_token)
.then(() => {
res.respond(200, {});
}).catch( (error) => {
throw new BadRequest(error.body.error_description);
});
},
};
exports.createOrderReferenceId = function(req, res, next){
if(!req.body || !req.body.billingAgreementId){
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
/**
* @api {post} /api/v3/payments/amazon/createOrderReferenceId create order reference id
* @apiVersion 3.0.0
* @apiName AmazonCreateOrderReferenceId
* @apiGroup Payments
* @apiParam {string} billingAgreementId billing agreement id
* @apiSuccess {object} object containing { orderReferenceId }
**/
api.createOrderReferenceId = {
method: 'POST',
url: '/payments/amazon/createOrderReferenceId',
async handler (req, res) {
if (!req.body.billingAgreementId) {
throw new BadRequest(res.t('missingBillingAgreementId'));
}
amzPayment.offAmazonPayments.createOrderReferenceForId({
let response = await amz.createOrderReferenceId({
Id: req.body.billingAgreementId,
IdType: 'BillingAgreement',
ConfirmNow: false
}, function(err, response){
if(err) return next(err);
if(!response.OrderReferenceDetails || !response.OrderReferenceDetails.AmazonOrderReferenceId){
return next(new Error('Missing attributes in Amazon response.'));
}
res.json({
orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId
ConfirmNow: false,
}).then(() => {
res.respond(200, {
orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId,
});
}).catch(errStr => {
throw new BadRequest(res.t(errStr));
});
},
};
exports.checkout = function(req, res, next){
if(!req.body || !req.body.orderReferenceId){
/*
api.checkout = function checkout (req, res, next) {
if (!req.body || !req.body.orderReferenceId) {
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
}
var gift = req.body.gift;
var user = res.locals.user;
var orderReferenceId = req.body.orderReferenceId;
var amount = 5;
let gift = req.body.gift;
let user = res.locals.user;
let orderReferenceId = req.body.orderReferenceId;
let amount = 5;
if(gift){
if(gift.type === 'gems'){
amount = gift.gems.amount/4;
}else if(gift.type === 'subscription'){
if (gift) {
if (gift.type === 'gems') {
amount = gift.gems.amount / 4;
} else if (gift.type === 'subscription') {
amount = shared.content.subscriptionBlocks[gift.subscription.key].price;
}
}
async.series({
setOrderReferenceDetails: function(cb){
setOrderReferenceDetails (cb) {
amzPayment.offAmazonPayments.setOrderReferenceDetails({
AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: {
OrderTotal: {
CurrencyCode: 'USD',
Amount: amount
Amount: amount,
},
SellerNote: 'HabitRPG Payment',
SellerOrderAttributes: {
SellerOrderId: shared.uuid(),
StoreName: 'HabitRPG'
}
}
StoreName: 'HabitRPG',
},
},
}, cb);
},
confirmOrderReference: function(cb){
confirmOrderReference (cb) {
amzPayment.offAmazonPayments.confirmOrderReference({
AmazonOrderReferenceId: orderReferenceId
AmazonOrderReferenceId: orderReferenceId,
}, cb);
},
authorize: function(cb){
authorize (cb) {
amzPayment.offAmazonPayments.authorize({
AmazonOrderReferenceId: orderReferenceId,
AuthorizationReferenceId: shared.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: 'USD',
Amount: amount
Amount: amount,
},
SellerAuthorizationNote: 'HabitRPG Payment',
TransactionTimeout: 0,
CaptureNow: true
}, function(err, res){
if(err) return cb(err);
CaptureNow: true,
}, function checkAuthorizationStatus (err) {
if (err) return cb(err);
if(res.AuthorizationDetails.AuthorizationStatus.State === 'Declined'){
if (res.AuthorizationDetails.AuthorizationStatus.State === 'Declined') {
return cb(new Error('The payment was not successfull.'));
}
@@ -114,64 +130,65 @@ exports.checkout = function(req, res, next){
});
},
closeOrderReference: function(cb){
closeOrderReference (cb) {
amzPayment.offAmazonPayments.closeOrderReference({
AmazonOrderReferenceId: orderReferenceId
AmazonOrderReferenceId: orderReferenceId,
}, cb);
},
executePayment: function(cb){
executePayment (cb) {
async.waterfall([
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); },
function(member, cb2){
var data = {user:user, paymentMethod:'Amazon Payments'};
var method = 'buyGems';
function findUser (cb2) {
User.findById(gift ? gift.uuid : undefined, cb2);
},
function executeAmazonPayment (member, cb2) {
let data = {user, paymentMethod: 'Amazon Payments'};
let method = 'buyGems';
if (gift){
if (gift.type == 'subscription') method = 'createSubscription';
if (gift) {
if (gift.type === 'subscription') method = 'createSubscription';
gift.member = member;
data.gift = gift;
data.paymentMethod = 'Gift';
}
payments[method](data, cb2);
}
},
], cb);
}
}, function(err, results){
if(err) return next(err);
},
}, function result (err) {
if (err) return next(err);
res.sendStatus(200);
});
};
exports.subscribe = function(req, res, next){
if(!req.body || !req.body['billingAgreementId']){
api.subscribe = function subscribe (req, res, next) {
if (!req.body || !req.body.billingAgreementId) {
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
}
var billingAgreementId = req.body.billingAgreementId;
var sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
var coupon = req.body.coupon;
var user = res.locals.user;
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){
if (!sub) {
return res.status(400).json({err: 'Subscription plan not found.'});
}
async.series({
applyDiscount: function(cb){
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(err, coupon){
if(err) return cb(err);
if(!coupon) return cb(new Error('Coupon code not found.'));
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: function(cb){
setBillingAgreementDetails (cb) {
amzPayment.offAmazonPayments.setBillingAgreementDetails({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
@@ -179,25 +196,25 @@ exports.subscribe = function(req, res, next){
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: shared.uuid(),
StoreName: 'HabitRPG',
CustomInformation: 'HabitRPG Subscription'
}
}
CustomInformation: 'HabitRPG Subscription',
},
},
}, cb);
},
confirmBillingAgreement: function(cb){
confirmBillingAgreement (cb) {
amzPayment.offAmazonPayments.confirmBillingAgreement({
AmazonBillingAgreementId: billingAgreementId
AmazonBillingAgreementId: billingAgreementId,
}, cb);
},
authorizeOnBillingAgreeement: function(cb){
authorizeOnBillingAgreement (cb) {
amzPayment.offAmazonPayments.authorizeOnBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: shared.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: 'USD',
Amount: sub.price
Amount: sub.price,
},
SellerAuthorizationNote: 'HabitRPG Subscription Payment',
TransactionTimeout: 0,
@@ -205,67 +222,70 @@ exports.subscribe = function(req, res, next){
SellerNote: 'HabitRPG Subscription Payment',
SellerOrderAttributes: {
SellerOrderId: shared.uuid(),
StoreName: 'HabitRPG'
}
}, function(err, res){
if(err) return cb(err);
StoreName: 'HabitRPG',
},
}, function billingAgreementResult (err) {
if (err) return cb(err);
if(res.AuthorizationDetails.AuthorizationStatus.State === 'Declined'){
return cb(new Error('The payment was not successfull.'));
if (res.AuthorizationDetails.AuthorizationStatus.State === 'Declined') {
return cb(new Error('The payment was not successful.'));
}
return cb();
});
},
createSubscription: function(cb){
createSubscription (cb) {
payments.createSubscription({
user: user,
user,
customerId: billingAgreementId,
paymentMethod: 'Amazon Payments',
sub: sub
sub,
}, cb);
}
}, function(err, results){
if(err) return next(err);
},
}, function subscribeResult (err) {
if (err) return next(err);
res.sendStatus(200);
});
};
exports.subscribeCancel = function(req, res, next){
var user = res.locals.user;
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'});
var billingAgreementId = user.purchased.plan.customerId;
let billingAgreementId = user.purchased.plan.customerId;
async.series({
closeBillingAgreement: function(cb){
closeBillingAgreement (cb) {
amzPayment.offAmazonPayments.closeBillingAgreement({
AmazonBillingAgreementId: billingAgreementId
AmazonBillingAgreementId: billingAgreementId,
}, cb);
},
cancelSubscription: function(cb){
var data = {
user: user,
cancelSubscription (cb) {
let data = {
user,
// Date of next bill
nextBill: moment(user.purchased.plan.lastBillingDate).add({days: 30}),
paymentMethod: 'Amazon Payments'
paymentMethod: 'Amazon Payments',
};
payments.cancelSubscription(data, cb);
}
}, function(err, results){
},
}, function subscribeCancelResult (err) {
if (err) return next(err); // don't json this, let toString() handle errors
if(req.query.noRedirect){
if (req.query.noRedirect) {
res.sendStatus(200);
}else{
} else {
res.redirect('/');
}
user = null;
});
};
*/
module.exports = api;

View File

@@ -1,67 +1,66 @@
var iap = require('in-app-purchase');
var async = require('async');
var payments = require('./index');
var nconf = require('nconf');
import iap from 'in-app-purchase';
import whatThis from 'in-app-purchase';
import payments from './index';
import nconf from 'nconf';
var inAppPurchase = require('in-app-purchase');
inAppPurchase.config({
iap.config({
// this is the path to the directory containing iap-sanbox/iap-live files
googlePublicKeyPath: nconf.get('IAP_GOOGLE_KEYDIR')
googlePublicKeyPath: nconf.get('IAP_GOOGLE_KEYDIR'),
});
// Validation ERROR Codes
var INVALID_PAYLOAD = 6778001;
var CONNECTION_FAILED = 6778002;
var PURCHASE_EXPIRED = 6778003;
const INVALID_PAYLOAD = 6778001;
/* const CONNECTION_FAILED = 6778002;
const PURCHASE_EXPIRED = 6778003; */ // These variables were never used??
exports.androidVerify = function(req, res, next) {
var iapBody = req.body;
var user = res.locals.user;
let api = {};
iap.setup(function (error) {
/*
api.androidVerify = function androidVerify (req, res) {
let iapBody = req.body;
let user = res.locals.user;
iap.setup(function googleSetupResult (error) {
if (error) {
var resObj = {
let resObj = {
ok: false,
data: 'IAP Error'
data: 'IAP Error',
};
return res.json(resObj);
}
/*
google receipt must be provided as an object
{
"data": "{stringified data object}",
"signature": "signature from google"
}
*/
var testObj = {
// google receipt must be provided as an object
// {
// "data": "{stringified data object}",
// "signature": "signature from google"
// }
let testObj = {
data: iapBody.transaction.receipt,
signature: iapBody.transaction.signature
signature: iapBody.transaction.signature,
};
// iap is ready
iap.validate(iap.GOOGLE, testObj, function (err, googleRes) {
iap.validate(iap.GOOGLE, testObj, function googleValidateResult (err, googleRes) {
if (err) {
var resObj = {
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString()
}
message: err.toString(),
},
};
return res.json(resObj);
}
if (iap.isValidated(googleRes)) {
var resObj = {
let resObj = {
ok: true,
data: googleRes
data: googleRes,
};
payments.buyGems({user:user, paymentMethod:'IAP GooglePlay', amount: 5.25});
payments.buyGems({user, paymentMethod: 'IAP GooglePlay', amount: 5.25});
return res.json(resObj);
}
@@ -69,87 +68,89 @@ exports.androidVerify = function(req, res, next) {
});
};
exports.iosVerify = function(req, res, next) {
var iapBody = req.body;
var user = res.locals.user;
exports.iosVerify = function iosVerify (req, res) {
let iapBody = req.body;
let user = res.locals.user;
iap.setup(function (error) {
iap.setup(function iosSetupResult (error) {
if (error) {
var resObj = {
let resObj = {
ok: false,
data: 'IAP Error'
data: 'IAP Error',
};
return res.json(resObj);
}
//iap is ready
iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) {
// iap is ready
iap.validate(iap.APPLE, iapBody.transaction.receipt, function iosValidateResult (err, appleRes) {
if (err) {
var resObj = {
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString()
}
message: err.toString(),
},
};
return res.json(resObj);
}
if (iap.isValidated(appleRes)) {
var purchaseDataList = iap.getPurchaseData(appleRes);
let purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length > 0) {
var correctReceipt = true;
for (var index in purchaseDataList) {
let correctReceipt = true;
for (let index of purchaseDataList) {
switch (purchaseDataList[index].productId) {
case 'com.habitrpg.ios.Habitica.4gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 1});
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1});
break;
case 'com.habitrpg.ios.Habitica.8gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 2});
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2});
break;
case 'com.habitrpg.ios.Habitica.20gems':
case 'com.habitrpg.ios.Habitica.21gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 5.25});
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25});
break;
case 'com.habitrpg.ios.Habitica.42gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 10.5});
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5});
break;
default:
correctReceipt = false;
}
}
if (correctReceipt) {
var resObj = {
let resObj = {
ok: true,
data: appleRes
data: appleRes,
};
// yay good!
return res.json(resObj);
}
}
//wrong receipt content
var resObj = {
// wrong receipt content
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: 'Incorrect receipt content'
}
message: 'Incorrect receipt content',
},
};
return res.json(resObj);
}
//invalid receipt
var resObj = {
// invalid receipt
let resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: 'Invalid receipt'
}
message: 'Invalid receipt',
},
};
return res.json(resObj);
});
});
};
*/
module.exports = api;

View File

@@ -1,207 +1,233 @@
var _ = require('lodash');
var shared = require('../../../../common');
var nconf = require('nconf');
var utils = require('./../../libs/api-v2/utils');
var moment = require('moment');
var isProduction = nconf.get("NODE_ENV") === "production";
var stripe = require('./stripe');
var paypal = require('./paypal');
var amazon = require('./amazon');
var members = require('../api-v2/members')
var async = require('async');
var iap = require('./iap');
var mongoose= require('mongoose');
var cc = require('coupon-code');
var pushNotify = require('./../api-v2/pushNotifications');
import _ from 'lodash' ;
import analytics from '../../../libs/api-v3/analyticsService';
import async from 'async';
import cc from 'coupon-code';
import {
getUserInfo,
sendTxn as txnEmail,
} from '../../../libs/api-v3/email';
import members from '../members';
import moment from 'moment';
import mongoose from 'mongoose';
import nconf from 'nconf';
import pushNotify from '../../../libs/api-v3/pushNotifications';
import shared from '../../../../../common' ;
function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) {
import amazon from './amazon';
import iap from './iap';
import paypal from './paypal';
import stripe from './stripe';
const IS_PROD = nconf.get('NODE_ENV') === 'production';
let api = {};
function revealMysteryItems (user) {
_.each(shared.content.gear.flat, function findMysteryItems (item) {
if (
item.klass === 'mystery' &&
moment().isAfter(shared.content.mystery[item.mystery].start) &&
moment().isBefore(shared.content.mystery[item.mystery].end) &&
!user.items.gear.owned[item.key] &&
!~user.purchased.plan.mysteryItems.indexOf(item.key)
user.purchased.plan.mysteryItems.indexOf(item.key) !== -1
) {
user.purchased.plan.mysteryItems.push(item.key);
}
});
}
exports.createSubscription = function(data, cb) {
var recipient = data.gift ? data.gift.member : data.user;
//if (!recipient.purchased.plan) recipient.purchased.plan = {}; // TODO double-check, this should never be the case
var p = recipient.purchased.plan;
var block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
var months = +block.months;
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];
let months = Number(block.months);
if (data.gift) {
if (p.customerId && !p.dateTerminated) { // User has active plan
p.extraMonths += months;
if (plan.customerId && !plan.dateTerminated) { // User has active plan
plan.extraMonths += months;
} else {
p.dateTerminated = moment(p.dateTerminated).add({months: months}).toDate();
if (!p.dateUpdated) p.dateUpdated = new Date();
plan.dateTerminated = moment(plan.dateTerminated).add({months}).toDate();
if (!plan.dateUpdated) plan.dateUpdated = new Date();
}
if (!p.customerId) p.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId
if (!plan.customerId) plan.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId
} else {
_(p).merge({ // override with these values
_(plan).merge({ // override with these values
planId: block.key,
customerId: data.customerId,
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: data.paymentMethod,
extraMonths: +p.extraMonths
+ +(p.dateTerminated ? moment(p.dateTerminated).diff(new Date(),'months',true) : 0),
extraMonths: Number(plan.extraMonths) +
Number(plan.dateTerminated ? moment(plan.dateTerminated).diff(new Date(), 'months', true) : 0),
dateTerminated: null,
// Specify a lastBillingDate just for Amazon Payments
// Resetted every time the subscription restarts
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? new Date() : undefined
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? new Date() : undefined,
}).defaults({ // allow non-override if a plan was previously used
dateCreated: new Date(),
mysteryItems: []
mysteryItems: [],
}).value();
}
// Block sub perks
var perks = Math.floor(months/3);
let perks = Math.floor(months / 3);
if (perks) {
p.consecutive.offset += months;
p.consecutive.gemCapExtra += perks*5;
if (p.consecutive.gemCapExtra > 25) p.consecutive.gemCapExtra = 25;
p.consecutive.trinkets += perks;
plan.consecutive.offset += months;
plan.consecutive.gemCapExtra += perks * 5;
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
plan.consecutive.trinkets += perks;
}
revealMysteryItems(recipient);
if(isProduction) {
if (!data.gift) utils.txnEmail(data.user, 'subscription-begins');
if (IS_PROD) {
if (!data.gift) txnEmail(data.user, 'subscription-begins');
var analyticsData = {
let analyticsData = {
uuid: data.user._id,
itemPurchased: 'Subscription',
sku: data.paymentMethod.toLowerCase() + '-subscription',
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: !!data.gift, // coerced into a boolean
purchaseValue: block.price
}
utils.analytics.trackPurchase(analyticsData);
gift: Boolean(data.gift),
purchaseValue: block.price,
};
analytics.trackPurchase(analyticsData);
}
data.user.purchased.txnCount++;
if (data.gift){
if (data.gift) {
members.sendMessage(data.user, data.gift.member, data.gift);
var byUserName = utils.getUserInfo(data.user, ['name']).name;
let byUserName = getUserInfo(data.user, ['name']).name;
if(data.gift.member.preferences.emailNotifications.giftedSubscription !== false){
utils.txnEmail(data.gift.member, 'gifted-subscription', [
if (data.gift.member.preferences.emailNotifications.giftedSubscription !== false) {
txnEmail(data.gift.member, 'gifted-subscription', [
{name: 'GIFTER', content: byUserName},
{name: 'X_MONTHS_SUBSCRIPTION', content: months}
{name: 'X_MONTHS_SUBSCRIPTION', content: months},
]);
}
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('giftedSubscription'), months + " months - by "+ byUserName);
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('giftedSubscription'), `${months} months - by ${byUserName}`);
}
}
async.parallel([
function(cb2){data.user.save(cb2)},
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
function saveGiftingUserData (cb2) {
data.user.save(cb2);
},
function saveRecipientUserData (cb2) {
if (data.gift) {
data.gift.member.save(cb2);
} else {
cb2(null);
}
},
], cb);
}
};
/**
* Sets their subscription to be cancelled later
*/
exports.cancelSubscription = function(data, cb) {
var p = data.user.purchased.plan,
now = moment(),
remaining = data.nextBill ? moment(data.nextBill).diff(new Date, 'days') : 30;
api.cancelSubscription = function cancelSubscription (data, cb) {
let plan = data.user.purchased.plan;
let now = moment();
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
p.dateTerminated =
moment( now.format('MM') + '/' + moment(p.dateUpdated).format('DD') + '/' + now.format('YYYY') )
plan.dateTerminated =
moment(`${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`)
.add({days: remaining}) // end their subscription 1mo from their last payment
.add({months: Math.ceil(p.extraMonths)})// plus any extra time (carry-over, gifted subscription, etc) they have. TODO: moment can't add months in fractions...
.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...
.toDate();
p.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
plan.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
data.user.save(cb);
utils.txnEmail(data.user, 'cancel-subscription');
var analyticsData = {
txnEmail(data.user, 'cancel-subscription');
let analyticsData = {
uuid: data.user._id,
gaCategory: 'commerce',
gaLabel: data.paymentMethod,
paymentMethod: data.paymentMethod
}
utils.analytics.track('unsubscribe', analyticsData);
}
paymentMethod: data.paymentMethod,
};
analytics.track('unsubscribe', analyticsData);
};
exports.buyGems = function(data, cb) {
var amt = data.amount || 5;
amt = data.gift ? data.gift.gems.amount/4 : amt;
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;
data.user.purchased.txnCount++;
if(isProduction) {
if (!data.gift) utils.txnEmail(data.user, 'donation');
if (IS_PROD) {
if (!data.gift) txnEmail(data.user, 'donation');
var analyticsData = {
let analyticsData = {
uuid: data.user._id,
itemPurchased: 'Gems',
sku: data.paymentMethod.toLowerCase() + '-checkout',
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
purchaseType: 'checkout',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: !!data.gift, // coerced into a boolean
purchaseValue: amt
}
utils.analytics.trackPurchase(analyticsData);
gift: Boolean(data.gift),
purchaseValue: amt,
};
analytics.trackPurchase(analyticsData);
}
if (data.gift){
var byUsername = utils.getUserInfo(data.user, ['name']).name;
var gemAmount = data.gift.gems.amount || 20;
if (data.gift) {
let byUsername = getUserInfo(data.user, ['name']).name;
let gemAmount = data.gift.gems.amount || 20;
members.sendMessage(data.user, data.gift.member, data.gift);
if(data.gift.member.preferences.emailNotifications.giftedGems !== false){
utils.txnEmail(data.gift.member, 'gifted-gems', [
if (data.gift.member.preferences.emailNotifications.giftedGems !== false) {
txnEmail(data.gift.member, 'gifted-gems', [
{name: 'GIFTER', content: byUsername},
{name: 'X_GEMS_GIFTED', content: gemAmount}
{name: 'X_GEMS_GIFTED', content: gemAmount},
]);
}
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);
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}`);
}
}
async.parallel([
function(cb2){data.user.save(cb2)},
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
function saveGiftingUserData (cb2) {
data.user.save(cb2);
},
function saveRecipientUserData (cb2) {
if (data.gift) {
data.gift.member.save(cb2);
} else {
cb2(null);
}
},
], cb);
}
};
exports.validCoupon = function(req, res, next){
mongoose.model('Coupon').findOne({_id:cc.validate(req.params.code), event:'google_6mo'}, function(err, coupon){
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"});
if (!coupon) return res.status(401).json({err: 'Invalid coupon code'});
return res.sendStatus(200);
});
}
};
exports.stripeCheckout = stripe.checkout;
exports.stripeSubscribeCancel = stripe.subscribeCancel;
exports.stripeSubscribeEdit = stripe.subscribeEdit;
api.stripeCheckout = stripe.checkout;
api.stripeSubscribeCancel = stripe.subscribeCancel;
api.stripeSubscribeEdit = stripe.subscribeEdit;
exports.paypalSubscribe = paypal.createBillingAgreement;
exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
exports.paypalSubscribeCancel = paypal.cancelSubscription;
exports.paypalCheckout = paypal.createPayment;
exports.paypalCheckoutSuccess = paypal.executePayment;
exports.paypalIPN = paypal.ipn;
api.paypalSubscribe = paypal.createBillingAgreement;
api.paypalSubscribeSuccess = paypal.executeBillingAgreement;
api.paypalSubscribeCancel = paypal.cancelSubscription;
api.paypalCheckout = paypal.createPayment;
api.paypalCheckoutSuccess = paypal.executePayment;
api.paypalIPN = paypal.ipn;
exports.amazonVerifyAccessToken = amazon.verifyAccessToken;
exports.amazonCreateOrderReferenceId = amazon.createOrderReferenceId;
exports.amazonCheckout = amazon.checkout;
exports.amazonSubscribe = amazon.subscribe;
exports.amazonSubscribeCancel = amazon.subscribeCancel;
api.amazonVerifyAccessToken = amazon.verifyAccessToken;
api.amazonCreateOrderReferenceId = amazon.createOrderReferenceId;
api.amazonCheckout = amazon.checkout;
api.amazonSubscribe = amazon.subscribe;
api.amazonSubscribeCancel = amazon.subscribeCancel;
exports.iapAndroidVerify = iap.androidVerify;
exports.iapIosVerify = iap.iosVerify;
api.iapAndroidVerify = iap.androidVerify;
api.iapIosVerify = iap.iosVerify;
// module.exports = api;
module.exports = {}; // @TODO HEREHERE

View File

@@ -5,10 +5,10 @@ var _ = require('lodash');
var url = require('url');
var User = require('mongoose').model('User');
var payments = require('./index');
var logger = require('../../libs/api-v2/logging');
var logger = require('../../../libs/api-v2/logging');
var ipn = require('paypal-ipn');
var paypal = require('paypal-rest-sdk');
var shared = require('../../../../common');
var shared = require('../../../../../common');
var mongoose = require('mongoose');
var cc = require('coupon-code');
@@ -31,6 +31,7 @@ var parseErr = function(res, err){
return res.status(400).json({err:error});
}
/*
exports.createBillingAgreement = function(req,res,next){
var sub = shared.content.subscriptionBlocks[req.query.sub];
async.waterfall([
@@ -190,11 +191,13 @@ exports.cancelSubscription = function(req, res, next){
user = null;
});
}
*/
/**
* 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
@@ -213,4 +216,4 @@ exports.ipn = function(req, res, next) {
}
});
};
*/

View File

@@ -1,123 +1,137 @@
var nconf = require('nconf');
var stripe = require('stripe')(nconf.get('STRIPE_API_KEY'));
var async = require('async');
var payments = require('./index');
var User = require('mongoose').model('User');
var shared = require('../../../../common');
var mongoose = require('mongoose');
var cc = require('coupon-code');
import nconf from 'nconf';
import stripeModule from 'stripe';
import async from 'async';
import payments from './index';
import { model as User } from '../../../models/user';
import shared from '../../../../../common';
import mongoose from 'mongoose';
import cc from 'coupon-code';
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
let api = {};
/*
Setup Stripe response when posting payment
*/
exports.checkout = function(req, res, next) {
var token = req.body.id;
var user = res.locals.user;
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
var sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
/*
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(cb){
function stripeCharge (cb) {
if (sub) {
async.waterfall([
function(cb2){
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);
mongoose.model('Coupon').findOne({_id: cc.validate(req.query.coupon), event: sub.key}, cb2);
},
function(coupon, cb2){
function createCustomer (coupon, cb2) {
if (sub.discount && !coupon) return cb2('Invalid coupon code.');
var customer = {
let customer = {
email: req.body.email,
metadata: {uuid: user._id},
card: token,
plan: sub.key
plan: sub.key,
};
stripe.customers.create(customer, cb2);
}
},
], cb);
} else {
let amount;
if (!gift) {
amount = '500';
} else if (gift.type === 'subscription') {
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
} else {
amount = `${gift.gems.amount / 4 * 100}`;
}
stripe.charges.create({
amount: !gift ? '500' //"500" = $5
: gift.type=='subscription' ? ''+shared.content.subscriptionBlocks[gift.subscription.key].price*100
: ''+gift.gems.amount/4*100,
amount,
currency: 'usd',
card: token
card: token,
}, cb);
}
},
function(response, cb) {
if (sub) return payments.createSubscription({user:user, customerId:response.id, paymentMethod:'Stripe', sub:sub}, cb);
function saveUserData (response, cb) {
if (sub) return payments.createSubscription({user, customerId: response.id, paymentMethod: 'Stripe', sub}, cb);
async.waterfall([
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); },
function(member, cb2){
var data = {user:user, customerId:response.id, paymentMethod:'Stripe', gift:gift};
var method = 'buyGems';
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';
if (gift.type === 'subscription') method = 'createSubscription';
data.paymentMethod = 'Gift';
}
payments[method](data, cb2);
}
},
], cb);
}
], function(err){
},
], 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;
});
};
exports.subscribeCancel = function(req, res, next) {
var user = res.locals.user;
if (!user.purchased.plan.customerId)
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({
get_cus: function(cb){
getCustomer: function getCustomer (cb) {
stripe.customers.retrieve(user.purchased.plan.customerId, cb);
},
del_cus: ['get_cus', function(cb, results){
deleteCustomer: ['getCustomer', function deleteCustomer (cb) {
stripe.customers.del(user.purchased.plan.customerId, cb);
}],
cancel_sub: ['get_cus', function(cb, results) {
var data = {
user: user,
nextBill: results.get_cus.subscription.current_period_end*1000, // timestamp is in seconds
paymentMethod: 'Stripe'
cancelSubscription: ['getCustomer', function cancelSubscription (cb, results) {
let data = {
user,
nextBill: results.get_cus.subscription.current_period_end * 1000, // timestamp is in seconds
paymentMethod: 'Stripe',
};
payments.cancelSubscription(data, cb);
}]
}, function(err, results){
}],
}, function handleResponse (err) {
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.redirect('/');
user = null;
});
};
exports.subscribeEdit = function(req, res, next) {
var token = req.body.id;
var user = res.locals.user;
var user_id = user.purchased.plan.customerId;
var sub_id;
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(cb){
stripe.customers.listSubscriptions(user_id, cb);
function listSubscriptions (cb) {
stripe.customers.listSubscriptions(userId, cb);
},
function(response, cb) {
sub_id = response.data[0].id;
console.warn(sub_id);
console.warn([user_id, sub_id, { card: token }]);
stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb);
function updateSubscription (response, cb) {
subscriptionId = response.data[0].id;
stripe.customers.updateSubscription(userId, subscriptionId, { card: token }, cb);
},
function(response, cb) {
function saveUser (response, cb) {
user.save(cb);
}
], function(err, saved){
},
], function handleResponse (err) {
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.sendStatus(200);
token = user = user_id = sub_id;
token = user = userId = subscriptionId;
});
};
*/
module.exports = api;

View File

@@ -0,0 +1,38 @@
import amazonPayments from 'amazon-payments';
import nconf from 'nconf';
import Q from 'q';
const IS_PROD = nconf.get('NODE_ENV') === 'production';
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'),
});
api.getTokenInfo = (token) => {
return new Promise((resolve, reject) => {
amzPayment.api.getTokenInfo(token, (err, tokenInfo) => {
if (err) return reject(err);
return resolve(tokenInfo);
});
});
};
api.createOrderReferenceId = (inputSet) => {
return new Promise((resolve, reject) => {
amzPayment.offAmazonPayments.createOrderReferenceForId(inputSet, (err, response) => {
if (err) return reject(err);
if (!response.OrderReferenceDetails || !response.OrderReferenceDetails.AmazonOrderReferenceId) {
return reject('missingAttributesFromAmazon');
}
return resolve(response);
});
});
};
module.exports = api;