diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 7af8dc9af0..b68a72b936 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -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" } diff --git a/tasks/gulp-tests.js b/tasks/gulp-tests.js index d8a9917eef..fba9d85721 100644 --- a/tasks/gulp-tests.js +++ b/tasks/gulp-tests.js @@ -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'), diff --git a/test/api/v3/integration/payments/POST-payments_amazon_verify_access_token.test.js b/test/api/v3/integration/payments/POST-payments_amazon_verify_access_token.test.js new file mode 100644 index 0000000000..494c387f14 --- /dev/null +++ b/test/api/v3/integration/payments/POST-payments_amazon_verify_access_token.test.js @@ -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'), + }); + }); +}); diff --git a/test/api/v3/unit/libs/amazonPayments.test.js b/test/api/v3/unit/libs/amazonPayments.test.js new file mode 100644 index 0000000000..a60a8b4633 --- /dev/null +++ b/test/api/v3/unit/libs/amazonPayments.test.js @@ -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 + }); + }); +}); diff --git a/website/src/controllers/api-v3/payments/amazon.js b/website/src/controllers/api-v3/payments/amazon.js deleted file mode 100644 index bc8e3b5177..0000000000 --- a/website/src/controllers/api-v3/payments/amazon.js +++ /dev/null @@ -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; diff --git a/website/src/controllers/api-v3/payments/iap.js b/website/src/controllers/api-v3/payments/iap.js deleted file mode 100644 index 94cf21fcca..0000000000 --- a/website/src/controllers/api-v3/payments/iap.js +++ /dev/null @@ -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; diff --git a/website/src/controllers/api-v3/payments/index.js b/website/src/controllers/api-v3/payments/index.js deleted file mode 100644 index f6e0a8ebe3..0000000000 --- a/website/src/controllers/api-v3/payments/index.js +++ /dev/null @@ -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; diff --git a/website/src/controllers/api-v3/payments/stripe.js b/website/src/controllers/api-v3/payments/stripe.js deleted file mode 100644 index 5582d33ca1..0000000000 --- a/website/src/controllers/api-v3/payments/stripe.js +++ /dev/null @@ -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; diff --git a/website/src/controllers/top-level/payments/amazon.js b/website/src/controllers/top-level/payments/amazon.js index 8c01663c10..63b589f6fc 100644 --- a/website/src/controllers/top-level/payments/amazon.js +++ b/website/src/controllers/top-level/payments/amazon.js @@ -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.'}); - } - - amzPayment.offAmazonPayments.createOrderReferenceForId({ - 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.')); +/** + * @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')); } - res.json({ - orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId + let response = await amz.createOrderReferenceId({ + Id: req.body.billingAgreementId, + IdType: 'BillingAgreement', + 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; diff --git a/website/src/controllers/top-level/payments/iap.js b/website/src/controllers/top-level/payments/iap.js index 829482ed67..5de66b0452 100644 --- a/website/src/controllers/top-level/payments/iap.js +++ b/website/src/controllers/top-level/payments/iap.js @@ -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; diff --git a/website/src/controllers/top-level/payments/index.js b/website/src/controllers/top-level/payments/index.js index 1652e05b8d..5e413fd47c 100644 --- a/website/src/controllers/top-level/payments/index.js +++ b/website/src/controllers/top-level/payments/index.js @@ -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 diff --git a/website/src/controllers/top-level/payments/paypal.js b/website/src/controllers/top-level/payments/paypal.js index 766ee85139..046a6f52cc 100644 --- a/website/src/controllers/top-level/payments/paypal.js +++ b/website/src/controllers/top-level/payments/paypal.js @@ -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) { } }); }; - +*/ diff --git a/website/src/controllers/top-level/payments/stripe.js b/website/src/controllers/top-level/payments/stripe.js index 1a1085227c..765ccba8f6 100644 --- a/website/src/controllers/top-level/payments/stripe.js +++ b/website/src/controllers/top-level/payments/stripe.js @@ -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; diff --git a/website/src/libs/api-v3/amazonPayments.js b/website/src/libs/api-v3/amazonPayments.js new file mode 100644 index 0000000000..9cff7cc763 --- /dev/null +++ b/website/src/libs/api-v3/amazonPayments.js @@ -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;