From 626d8d6e73b886182bed4b1b38478c7f3fecfb0b Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 6 Apr 2016 21:18:36 +0000 Subject: [PATCH 1/5] WIP(payments): lint Amazon Payments file --- .../src/controllers/api-v3/payments/amazon.js | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 website/src/controllers/api-v3/payments/amazon.js diff --git a/website/src/controllers/api-v3/payments/amazon.js b/website/src/controllers/api-v3/payments/amazon.js new file mode 100644 index 0000000000..c1056c1224 --- /dev/null +++ b/website/src/controllers/api-v3/payments/amazon.js @@ -0,0 +1,277 @@ +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; From c5549787b44edd8bf5e72c90166e63c316ebe326 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 7 Apr 2016 20:51:40 +0000 Subject: [PATCH 2/5] refactor(payments): index.js lint pass --- .../src/controllers/api-v3/payments/amazon.js | 2 +- .../src/controllers/api-v3/payments/index.js | 232 ++++++++++++++++++ 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 website/src/controllers/api-v3/payments/index.js diff --git a/website/src/controllers/api-v3/payments/amazon.js b/website/src/controllers/api-v3/payments/amazon.js index c1056c1224..bc8e3b5177 100644 --- a/website/src/controllers/api-v3/payments/amazon.js +++ b/website/src/controllers/api-v3/payments/amazon.js @@ -5,7 +5,7 @@ import mongoose from 'mongoose'; import moment from 'moment'; import nconf from 'nconf'; import payments from './index'; -import shared from '../../../../common'; +import shared from '../../../../../common'; import { model as User } from '../../models/user'; const IS_PROD = nconf.get('NODE_ENV') === 'production'; diff --git a/website/src/controllers/api-v3/payments/index.js b/website/src/controllers/api-v3/payments/index.js new file mode 100644 index 0000000000..f6e0a8ebe3 --- /dev/null +++ b/website/src/controllers/api-v3/payments/index.js @@ -0,0 +1,232 @@ +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; From 71e0792da88a262729a6e27a5c68ea5bd6228c80 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 12 Apr 2016 21:15:20 +0000 Subject: [PATCH 3/5] refactor(payments): IAP linting pass --- .../src/controllers/api-v3/payments/iap.js | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 website/src/controllers/api-v3/payments/iap.js diff --git a/website/src/controllers/api-v3/payments/iap.js b/website/src/controllers/api-v3/payments/iap.js new file mode 100644 index 0000000000..94cf21fcca --- /dev/null +++ b/website/src/controllers/api-v3/payments/iap.js @@ -0,0 +1,158 @@ +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; From da84f631e9f89ab2537621e29d4e7c0a001a670d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 13 Apr 2016 19:29:13 +0000 Subject: [PATCH 4/5] refactor(payments): Stripe linting pass --- .../src/controllers/api-v3/payments/stripe.js | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 website/src/controllers/api-v3/payments/stripe.js diff --git a/website/src/controllers/api-v3/payments/stripe.js b/website/src/controllers/api-v3/payments/stripe.js new file mode 100644 index 0000000000..5582d33ca1 --- /dev/null +++ b/website/src/controllers/api-v3/payments/stripe.js @@ -0,0 +1,135 @@ +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; From c9e3e0e68c5689b00aa508be3ba08678425f072f Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 14 Apr 2016 18:18:57 +0200 Subject: [PATCH 5/5] move payments to /top-level --- website/src/controllers/{ => top-level}/payments/amazon.js | 0 website/src/controllers/{ => top-level}/payments/iap.js | 0 website/src/controllers/{ => top-level}/payments/index.js | 0 website/src/controllers/{ => top-level}/payments/paypal.js | 0 .../controllers/{ => top-level}/payments/paypalBillingSetup.js | 0 website/src/controllers/{ => top-level}/payments/stripe.js | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename website/src/controllers/{ => top-level}/payments/amazon.js (100%) rename website/src/controllers/{ => top-level}/payments/iap.js (100%) rename website/src/controllers/{ => top-level}/payments/index.js (100%) rename website/src/controllers/{ => top-level}/payments/paypal.js (100%) rename website/src/controllers/{ => top-level}/payments/paypalBillingSetup.js (100%) rename website/src/controllers/{ => top-level}/payments/stripe.js (100%) diff --git a/website/src/controllers/payments/amazon.js b/website/src/controllers/top-level/payments/amazon.js similarity index 100% rename from website/src/controllers/payments/amazon.js rename to website/src/controllers/top-level/payments/amazon.js diff --git a/website/src/controllers/payments/iap.js b/website/src/controllers/top-level/payments/iap.js similarity index 100% rename from website/src/controllers/payments/iap.js rename to website/src/controllers/top-level/payments/iap.js diff --git a/website/src/controllers/payments/index.js b/website/src/controllers/top-level/payments/index.js similarity index 100% rename from website/src/controllers/payments/index.js rename to website/src/controllers/top-level/payments/index.js diff --git a/website/src/controllers/payments/paypal.js b/website/src/controllers/top-level/payments/paypal.js similarity index 100% rename from website/src/controllers/payments/paypal.js rename to website/src/controllers/top-level/payments/paypal.js diff --git a/website/src/controllers/payments/paypalBillingSetup.js b/website/src/controllers/top-level/payments/paypalBillingSetup.js similarity index 100% rename from website/src/controllers/payments/paypalBillingSetup.js rename to website/src/controllers/top-level/payments/paypalBillingSetup.js diff --git a/website/src/controllers/payments/stripe.js b/website/src/controllers/top-level/payments/stripe.js similarity index 100% rename from website/src/controllers/payments/stripe.js rename to website/src/controllers/top-level/payments/stripe.js