mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
v3 payments: working IAP and Stripe, move paypalBillingSetup to its own file, closeChal is now a challenge instance method
This commit is contained in:
@@ -22,6 +22,7 @@ website/src/middlewares/apiThrottle.js
|
|||||||
website/src/middlewares/forceRefresh.js
|
website/src/middlewares/forceRefresh.js
|
||||||
|
|
||||||
debug-scripts/*
|
debug-scripts/*
|
||||||
|
scripts/*
|
||||||
tasks/*.js
|
tasks/*.js
|
||||||
gulpfile.js
|
gulpfile.js
|
||||||
Gruntfile.js
|
Gruntfile.js
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ function processChallenges (afterId) {
|
|||||||
|
|
||||||
oldTask.tags = _.map(oldTask.tags || {}, function (tagPresent, tagId) {
|
oldTask.tags = _.map(oldTask.tags || {}, function (tagPresent, tagId) {
|
||||||
return tagPresent && tagId;
|
return tagPresent && tagId;
|
||||||
|
}).filter(function (tag) {
|
||||||
|
return tag !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!oldTask.text) oldTask.text = 'task text'; // required
|
if (!oldTask.text) oldTask.text = 'task text'; // required
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ function processUsers (afterId) {
|
|||||||
if (!oldTask.text) oldTask.text = 'task text'; // required
|
if (!oldTask.text) oldTask.text = 'task text'; // required
|
||||||
oldTask.tags = _.map(oldTask.tags, function (tagPresent, tagId) {
|
oldTask.tags = _.map(oldTask.tags, function (tagPresent, tagId) {
|
||||||
return tagPresent && tagId;
|
return tagPresent && tagId;
|
||||||
|
}).filter(function (tag) {
|
||||||
|
return tag !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oldTask.type !== 'todo' || (oldTask.type === 'todo' && !oldTask.completed)) {
|
if (oldTask.type !== 'todo' || (oldTask.type === 'todo' && !oldTask.completed)) {
|
||||||
|
|||||||
94
scripts/paypalBillingSetup.js
Normal file
94
scripts/paypalBillingSetup.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
|
||||||
|
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
|
||||||
|
// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json),
|
||||||
|
// and once for any time you need to edit the plan thereafter
|
||||||
|
|
||||||
|
var path = require('path');
|
||||||
|
var nconf = require('nconf');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var paypal = require('paypal-rest-sdk');
|
||||||
|
var blocks = require('../../../../common').content.subscriptionBlocks;
|
||||||
|
var live = nconf.get('PAYPAL:mode')=='live';
|
||||||
|
|
||||||
|
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
|
||||||
|
|
||||||
|
var OP = 'create'; // list create update remove
|
||||||
|
|
||||||
|
paypal.configure({
|
||||||
|
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
|
||||||
|
'client_id': nconf.get("PAYPAL:client_id"),
|
||||||
|
'client_secret': nconf.get("PAYPAL:client_secret")
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://developer.paypal.com/docs/api/#billing-plans-and-agreements
|
||||||
|
var billingPlanTitle ="Habitica Subscription";
|
||||||
|
var billingPlanAttributes = {
|
||||||
|
"name": billingPlanTitle,
|
||||||
|
"description": billingPlanTitle,
|
||||||
|
"type": "INFINITE",
|
||||||
|
"merchant_preferences": {
|
||||||
|
"auto_bill_amount": "yes",
|
||||||
|
"cancel_url": live ? 'https://habitica.com' : 'http://localhost:3000',
|
||||||
|
"return_url": (live ? 'https://habitica.com' : 'http://localhost:3000') + '/paypal/subscribe/success'
|
||||||
|
},
|
||||||
|
payment_definitions: [{
|
||||||
|
"type": "REGULAR",
|
||||||
|
"frequency": "MONTH",
|
||||||
|
"cycles": "0"
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
_.each(blocks, function(block){
|
||||||
|
block.definition = _.cloneDeep(billingPlanAttributes);
|
||||||
|
_.merge(block.definition.payment_definitions[0], {
|
||||||
|
"name": billingPlanTitle + ' ($'+block.price+' every '+block.months+' months, recurring)',
|
||||||
|
"frequency_interval": ""+block.months,
|
||||||
|
"amount": {
|
||||||
|
"currency": "USD",
|
||||||
|
"value": ""+block.price
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
switch(OP) {
|
||||||
|
case "list":
|
||||||
|
paypal.billingPlan.list({status: 'ACTIVE'}, function(err, plans){
|
||||||
|
console.log({err:err, plans:plans});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "get":
|
||||||
|
paypal.billingPlan.get(nconf.get("PAYPAL:billing_plans:12"), function (err, plan) {
|
||||||
|
console.log({err:err, plan:plan});
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
case "update":
|
||||||
|
var update = {
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/merchant_preferences",
|
||||||
|
"value": {
|
||||||
|
"cancel_url": "https://habitica.com"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
paypal.billingPlan.update(nconf.get("PAYPAL:billing_plans:12"), update, function (err, res) {
|
||||||
|
console.log({err:err, plan:res});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "create":
|
||||||
|
paypal.billingPlan.create(blocks["google_6mo"].definition, function(err,plan){
|
||||||
|
if (err) return console.log(err);
|
||||||
|
if (plan.state == "ACTIVE")
|
||||||
|
return console.log({err:err, plan:plan});
|
||||||
|
var billingPlanUpdateAttributes = [{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/",
|
||||||
|
"value": {
|
||||||
|
"state": "ACTIVE"
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
// Activate the plan by changing status to Active
|
||||||
|
paypal.billingPlan.update(plan.id, billingPlanUpdateAttributes, function(err, response){
|
||||||
|
console.log({err:err, response:response, id:plan.id});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "remove": break;
|
||||||
|
}
|
||||||
@@ -289,8 +289,6 @@ api.update = function(req, res, next){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
import { _closeChal } from '../api-v3/challenges';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete & close
|
* Delete & close
|
||||||
*/
|
*/
|
||||||
@@ -304,7 +302,7 @@ api.delete = async function(req, res, next){
|
|||||||
if (!challenge.canModify(user)) return next(shared.i18n.t('noPermissionCloseChallenge'));
|
if (!challenge.canModify(user)) return next(shared.i18n.t('noPermissionCloseChallenge'));
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_DELETED'});
|
await challenge.closeChal({broken: 'CHALLENGE_DELETED'});
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
@@ -326,7 +324,7 @@ api.selectWinner = async function(req, res, next) {
|
|||||||
if (!winner || winner.challenges.indexOf(challenge._id) === -1) return next('Winner ' + req.query.uid + ' not found.');
|
if (!winner || winner.challenges.indexOf(challenge._id) === -1) return next('Winner ' + req.query.uid + ' not found.');
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_CLOSED', winner});
|
await challenge.closeChal({broken: 'CHALLENGE_CLOSED', winner});
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import validator from 'validator';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import setupNconf from '../../libs/api-v3/setupNconf';
|
|
||||||
setupNconf();
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
authWithHeaders,
|
authWithHeaders,
|
||||||
} from '../../middlewares/api-v3/auth';
|
} from '../../middlewares/api-v3/auth';
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ import {
|
|||||||
NotFound,
|
NotFound,
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
} from '../../libs/api-v3/errors';
|
} from '../../libs/api-v3/errors';
|
||||||
import shared from '../../../../common';
|
|
||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
import { sendTxn as txnEmail } from '../../libs/api-v3/email';
|
|
||||||
import sendPushNotification from '../../libs/api-v3/pushNotifications';
|
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import csvStringify from '../../libs/api-v3/csvStringify';
|
import csvStringify from '../../libs/api-v3/csvStringify';
|
||||||
|
|
||||||
@@ -449,64 +446,6 @@ api.updateChallenge = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO everything here should be moved to a worker
|
|
||||||
// actually even for a worker it's probably just too big and will kill mongo
|
|
||||||
// Exported because it's used in v2 controller
|
|
||||||
export async function _closeChal (challenge, broken = {}) {
|
|
||||||
let winner = broken.winner;
|
|
||||||
let brokenReason = broken.broken;
|
|
||||||
|
|
||||||
// Delete the challenge
|
|
||||||
await Challenge.remove({_id: challenge._id}).exec();
|
|
||||||
|
|
||||||
// Refund the leader if the challenge is closed and the group not the tavern
|
|
||||||
if (challenge.group !== TAVERN_ID && brokenReason === 'CHALLENGE_DELETED') {
|
|
||||||
await User.update({_id: challenge.leader}, {$inc: {balance: challenge.prize / 4}}).exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the challengeCount on the group
|
|
||||||
await Group.update({_id: challenge.group}, {$inc: {challengeCount: -1}}).exec();
|
|
||||||
|
|
||||||
// Award prize to winner and notify
|
|
||||||
if (winner) {
|
|
||||||
winner.achievements.challenges.push(challenge.name);
|
|
||||||
winner.balance += challenge.prize / 4;
|
|
||||||
let savedWinner = await winner.save();
|
|
||||||
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
|
|
||||||
txnEmail(savedWinner, 'won-challenge', [
|
|
||||||
{name: 'CHALLENGE_NAME', content: challenge.name},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPushNotification(savedWinner, shared.i18n.t('wonChallenge'), challenge.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run some operations in the background withouth blocking the thread
|
|
||||||
let backgroundTasks = [
|
|
||||||
// And it's tasks
|
|
||||||
Tasks.Task.remove({'challenge.id': challenge._id, userId: {$exists: false}}).exec(),
|
|
||||||
// Set the challenge tag to non-challenge status and remove the challenge from the user's challenges
|
|
||||||
User.update({
|
|
||||||
challenges: challenge._id,
|
|
||||||
'tags._id': challenge._id,
|
|
||||||
}, {
|
|
||||||
$set: {'tags.$.challenge': false},
|
|
||||||
$pull: {challenges: challenge._id},
|
|
||||||
}, {multi: true}).exec(),
|
|
||||||
// Break users' tasks
|
|
||||||
Tasks.Task.update({
|
|
||||||
'challenge.id': challenge._id,
|
|
||||||
}, {
|
|
||||||
$set: {
|
|
||||||
'challenge.broken': brokenReason,
|
|
||||||
'challenge.winner': winner && winner.profile.name,
|
|
||||||
},
|
|
||||||
}, {multi: true}).exec(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Q.all(backgroundTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {delete} /api/v3/challenges/:challengeId Delete a challenge
|
* @api {delete} /api/v3/challenges/:challengeId Delete a challenge
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
@@ -534,7 +473,7 @@ api.deleteChallenge = {
|
|||||||
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_DELETED'});
|
await challenge.closeChal({broken: 'CHALLENGE_DELETED'});
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -571,7 +510,7 @@ api.selectChallengeWinner = {
|
|||||||
if (!winner || winner.challenges.indexOf(challenge._id) === -1) throw new NotFound(res.t('winnerNotFound', {userId: req.params.winnerId}));
|
if (!winner || winner.challenges.indexOf(challenge._id) === -1) throw new NotFound(res.t('winnerNotFound', {userId: req.params.winnerId}));
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_CLOSED', winner});
|
await challenge.closeChal({broken: 'CHALLENGE_CLOSED', winner});
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ import { sendTxn } from '../../libs/api-v3/email';
|
|||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
|
|
||||||
import setupNconf from '../../libs/api-v3/setupNconf';
|
|
||||||
setupNconf();
|
|
||||||
|
|
||||||
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => {
|
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => {
|
||||||
return { email, canSend: true };
|
return { email, canSend: true };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
} from '../../../libs/api-v3/errors';
|
} from '../../../libs/api-v3/errors';
|
||||||
import amzLib from '../../../libs/api-v3/amazonPayments';
|
import amzLib from '../../../libs/api-v3/amazonPayments';
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +17,7 @@ import cc from 'coupon-code';
|
|||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {post} /amazon/verifyAccessToken verify access token
|
* @api {post} /amazon/verifyAccessToken verify access token
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName AmazonVerifyAccessToken
|
* @apiName AmazonVerifyAccessToken
|
||||||
@@ -23,11 +25,11 @@ let api = {};
|
|||||||
*
|
*
|
||||||
* @apiParam {string} access_token the access token
|
* @apiParam {string} access_token the access token
|
||||||
*
|
*
|
||||||
* @apiSuccess {} empty
|
* @apiSuccess {Object} data Empty object
|
||||||
**/
|
**/
|
||||||
api.verifyAccessToken = {
|
api.verifyAccessToken = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/payments/amazon/verifyAccessToken',
|
url: '/amazon/verifyAccessToken',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -40,6 +42,7 @@ api.verifyAccessToken = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {post} /amazon/createOrderReferenceId create order reference id
|
* @api {post} /amazon/createOrderReferenceId create order reference id
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName AmazonCreateOrderReferenceId
|
* @apiName AmazonCreateOrderReferenceId
|
||||||
@@ -51,7 +54,7 @@ api.verifyAccessToken = {
|
|||||||
**/
|
**/
|
||||||
api.createOrderReferenceId = {
|
api.createOrderReferenceId = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/payments/amazon/createOrderReferenceId',
|
url: '/amazon/createOrderReferenceId',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -70,6 +73,7 @@ api.createOrderReferenceId = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {post} /amazon/checkout do checkout
|
* @api {post} /amazon/checkout do checkout
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName AmazonCheckout
|
* @apiName AmazonCheckout
|
||||||
@@ -81,7 +85,7 @@ api.createOrderReferenceId = {
|
|||||||
**/
|
**/
|
||||||
api.checkout = {
|
api.checkout = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/payments/amazon/checkout',
|
url: '/amazon/checkout',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let gift = req.body.gift;
|
let gift = req.body.gift;
|
||||||
@@ -148,6 +152,7 @@ api.checkout = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {post} /amazon/subscribe Subscribe
|
* @api {post} /amazon/subscribe Subscribe
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName AmazonSubscribe
|
* @apiName AmazonSubscribe
|
||||||
@@ -161,7 +166,7 @@ api.checkout = {
|
|||||||
**/
|
**/
|
||||||
api.subscribe = {
|
api.subscribe = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/payments/amazon/subscribe',
|
url: '/amazon/subscribe',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let billingAgreementId = req.body.billingAgreementId;
|
let billingAgreementId = req.body.billingAgreementId;
|
||||||
@@ -228,22 +233,21 @@ api.subscribe = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {get} /amazon/subscribe/cancel SubscribeCancel
|
* @api {get} /amazon/subscribe/cancel SubscribeCancel
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName AmazonSubscribe
|
* @apiName AmazonSubscribe
|
||||||
* @apiGroup Payments
|
* @apiGroup Payments
|
||||||
*
|
|
||||||
* @apiSuccess {object} empty object
|
|
||||||
**/
|
**/
|
||||||
api.subscribeCancel = {
|
api.subscribeCancel = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/payments/amazon/subscribe/cancel',
|
url: '/amazon/subscribe/cancel',
|
||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let billingAgreementId = user.purchased.plan.customerId;
|
let billingAgreementId = user.purchased.plan.customerId;
|
||||||
|
|
||||||
if (!billingAgreementId) throw new BadRequest(res.t('missingSubscription'));
|
if (!billingAgreementId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await amzLib.closeBillingAgreement({
|
await amzLib.closeBillingAgreement({
|
||||||
@@ -257,7 +261,7 @@ api.subscribeCancel = {
|
|||||||
};
|
};
|
||||||
await payments.cancelSubscription(data);
|
await payments.cancelSubscription(data);
|
||||||
|
|
||||||
res.respond(200, {});
|
res.redirect('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BadRequest(error.message);
|
throw new BadRequest(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import iap from 'in-app-purchase';
|
import iap from 'in-app-purchase';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
|
import {
|
||||||
|
authWithHeaders,
|
||||||
|
authWithUrl,
|
||||||
|
} from '../../../middlewares/api-v3/auth';
|
||||||
|
import payments from '../../../libs/api-v3/payments';
|
||||||
|
|
||||||
|
// NOT PORTED TO v3
|
||||||
|
|
||||||
iap.config({
|
iap.config({
|
||||||
// this is the path to the directory containing iap-sanbox/iap-live files
|
// this is the path to the directory containing iap-sanbox/iap-live files
|
||||||
@@ -7,148 +14,178 @@ iap.config({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Validation ERROR Codes
|
// Validation ERROR Codes
|
||||||
// const INVALID_PAYLOAD = 6778001;
|
const INVALID_PAYLOAD = 6778001;
|
||||||
// const CONNECTION_FAILED = 6778002;
|
// const CONNECTION_FAILED = 6778002;
|
||||||
// const PURCHASE_EXPIRED = 6778003;
|
// const PURCHASE_EXPIRED = 6778003;
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
/*
|
/**
|
||||||
api.androidVerify = function androidVerify (req, res) {
|
* @apiIgnore Payments are considered part of the private API
|
||||||
let iapBody = req.body;
|
* @api {post} /iap/android/verify Android Verify IAP
|
||||||
let user = res.locals.user;
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName IapAndroidVerify
|
||||||
|
* @apiGroup Payments
|
||||||
|
**/
|
||||||
|
api.iapAndroidVerify = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/iap/android/verify',
|
||||||
|
middlewares: [authWithUrl],
|
||||||
|
async handler (req, res) {
|
||||||
|
let user = res.locals.user;
|
||||||
|
let iapBody = req.body;
|
||||||
|
|
||||||
iap.setup(function googleSetupResult (error) {
|
iap.setup((error) => {
|
||||||
if (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 = {
|
let resObj = {
|
||||||
ok: false,
|
ok: false,
|
||||||
data: {
|
data: 'IAP Error',
|
||||||
code: INVALID_PAYLOAD,
|
|
||||||
message: err.toString(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return res.json(resObj);
|
return res.json(resObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iap.isValidated(googleRes)) {
|
// google receipt must be provided as an object
|
||||||
let resObj = {
|
// {
|
||||||
ok: true,
|
// "data": "{stringified data object}",
|
||||||
data: googleRes,
|
// "signature": "signature from google"
|
||||||
};
|
// }
|
||||||
|
let testObj = {
|
||||||
|
data: iapBody.transaction.receipt,
|
||||||
|
signature: iapBody.transaction.signature,
|
||||||
|
};
|
||||||
|
|
||||||
payments.buyGems({user, paymentMethod: 'IAP GooglePlay', amount: 5.25});
|
// iap is ready
|
||||||
|
iap.validate(iap.GOOGLE, testObj, (err, googleRes) => {
|
||||||
|
if (err) {
|
||||||
|
let resObj = {
|
||||||
|
ok: false,
|
||||||
|
data: {
|
||||||
|
code: INVALID_PAYLOAD,
|
||||||
|
message: err.toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return res.json(resObj);
|
return res.json(resObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (iap.isValidated(googleRes)) {
|
||||||
|
let resObj = {
|
||||||
|
ok: true,
|
||||||
|
data: googleRes,
|
||||||
|
};
|
||||||
|
|
||||||
|
payments.buyGems({
|
||||||
|
user,
|
||||||
|
paymentMethod: 'IAP GooglePlay',
|
||||||
|
amount: 5.25,
|
||||||
|
}).then(() => res.json(resObj));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.iosVerify = function iosVerify (req, res) {
|
/**
|
||||||
let iapBody = req.body;
|
* @apiIgnore Payments are considered part of the private API
|
||||||
let user = res.locals.user;
|
* @api {post} /iap/ios/verify iOS Verify IAP
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName IapiOSVerify
|
||||||
|
* @apiGroup Payments
|
||||||
|
**/
|
||||||
|
api.iapiOSVerify = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/iap/android/verify',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
let iapBody = req.body;
|
||||||
|
let user = res.locals.user;
|
||||||
|
|
||||||
iap.setup(function iosSetupResult (error) {
|
iap.setup(function iosSetupResult (error) {
|
||||||
if (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 = {
|
let resObj = {
|
||||||
ok: false,
|
ok: false,
|
||||||
data: {
|
data: 'IAP Error',
|
||||||
code: INVALID_PAYLOAD,
|
|
||||||
message: err.toString(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return res.json(resObj);
|
return res.json(resObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iap.isValidated(appleRes)) {
|
// iap is ready
|
||||||
let purchaseDataList = iap.getPurchaseData(appleRes);
|
iap.validate(iap.APPLE, iapBody.transaction.receipt, (err, appleRes) => {
|
||||||
if (purchaseDataList.length > 0) {
|
if (err) {
|
||||||
let correctReceipt = true;
|
let resObj = {
|
||||||
for (let index of purchaseDataList) {
|
ok: false,
|
||||||
switch (purchaseDataList[index].productId) {
|
data: {
|
||||||
case 'com.habitrpg.ios.Habitica.4gems':
|
code: INVALID_PAYLOAD,
|
||||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1});
|
message: err.toString(),
|
||||||
break;
|
},
|
||||||
case 'com.habitrpg.ios.Habitica.8gems':
|
};
|
||||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2});
|
|
||||||
break;
|
return res.json(resObj);
|
||||||
case 'com.habitrpg.ios.Habitica.20gems':
|
}
|
||||||
case 'com.habitrpg.ios.Habitica.21gems':
|
|
||||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25});
|
if (iap.isValidated(appleRes)) {
|
||||||
break;
|
let purchaseDataList = iap.getPurchaseData(appleRes);
|
||||||
case 'com.habitrpg.ios.Habitica.42gems':
|
if (purchaseDataList.length > 0) {
|
||||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5});
|
let correctReceipt = true;
|
||||||
break;
|
|
||||||
default:
|
for (let index of purchaseDataList) {
|
||||||
correctReceipt = false;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (correctReceipt) {
|
|
||||||
let resObj = {
|
// wrong receipt content
|
||||||
ok: true,
|
let resObj = {
|
||||||
data: appleRes,
|
ok: false,
|
||||||
};
|
data: {
|
||||||
// yay good!
|
code: INVALID_PAYLOAD,
|
||||||
return res.json(resObj);
|
message: 'Incorrect receipt content',
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json(resObj);
|
||||||
}
|
}
|
||||||
// wrong receipt content
|
|
||||||
|
// invalid receipt
|
||||||
let resObj = {
|
let resObj = {
|
||||||
ok: false,
|
ok: false,
|
||||||
data: {
|
data: {
|
||||||
code: INVALID_PAYLOAD,
|
code: INVALID_PAYLOAD,
|
||||||
message: 'Incorrect receipt content',
|
message: 'Invalid receipt',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return res.json(resObj);
|
|
||||||
}
|
|
||||||
// invalid receipt
|
|
||||||
let resObj = {
|
|
||||||
ok: false,
|
|
||||||
data: {
|
|
||||||
code: INVALID_PAYLOAD,
|
|
||||||
message: 'Invalid receipt',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.json(resObj);
|
return res.json(resObj);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -17,6 +19,8 @@ import {
|
|||||||
} from '../../../libs/api-v3/errors';
|
} from '../../../libs/api-v3/errors';
|
||||||
import * as logger from '../../../libs/api-v3/logger';
|
import * as logger from '../../../libs/api-v3/logger';
|
||||||
|
|
||||||
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
|
|
||||||
// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have
|
// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have
|
||||||
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created
|
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created
|
||||||
// there, get it's plan.id and store it in config.json
|
// there, get it's plan.id and store it in config.json
|
||||||
@@ -24,8 +28,6 @@ _.each(shared.content.subscriptionBlocks, (block) => {
|
|||||||
block.paypalKey = nconf.get(`PAYPAL:billing_plans:${block.key}`);
|
block.paypalKey = nconf.get(`PAYPAL:billing_plans:${block.key}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
|
|
||||||
paypal.configure({
|
paypal.configure({
|
||||||
mode: nconf.get('PAYPAL:mode'), // sandbox or live
|
mode: nconf.get('PAYPAL:mode'), // sandbox or live
|
||||||
client_id: nconf.get('PAYPAL:client_id'),
|
client_id: nconf.get('PAYPAL:client_id'),
|
||||||
@@ -35,18 +37,18 @@ paypal.configure({
|
|||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /paypal/checkout checkout
|
* @apiIgnore Payments are considered part of the private API
|
||||||
|
* @api {get} /paypal/checkout Paypal checkout
|
||||||
|
* @apiDescription Redirects to Paypal
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName PaypalCheckout
|
* @apiName PaypalCheckout
|
||||||
* @apiGroup Payments
|
* @apiGroup Payments
|
||||||
*
|
*
|
||||||
* @apiParam {string} gift The stringified object representing the user, the gift recepient.
|
* @apiParam {string} gift Query parameter - The stringified object representing the user, the gift recepient.
|
||||||
*
|
|
||||||
* @apiSuccess {} redirect
|
|
||||||
**/
|
**/
|
||||||
api.checkout = {
|
api.checkout = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/payments/paypal/checkout',
|
url: '/paypal/checkout',
|
||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||||
@@ -68,8 +70,8 @@ api.checkout = {
|
|||||||
intent: 'sale',
|
intent: 'sale',
|
||||||
payer: { payment_method: 'Paypal' },
|
payer: { payment_method: 'Paypal' },
|
||||||
redirect_urls: {
|
redirect_urls: {
|
||||||
return_url: `${nconf.get('BASE_URL')}/paypal/checkout/success`,
|
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||||
cancel_url: `${nconf.get('BASE_URL')}`,
|
cancel_url: `${BASE_URL}`,
|
||||||
},
|
},
|
||||||
transactions: [{
|
transactions: [{
|
||||||
item_list: {
|
item_list: {
|
||||||
@@ -87,6 +89,7 @@ api.checkout = {
|
|||||||
description,
|
description,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await paypal.payment.create(createPayment);
|
let result = await paypal.payment.create(createPayment);
|
||||||
let link = _.find(result.links, { rel: 'approval_url' }).href;
|
let link = _.find(result.links, { rel: 'approval_url' }).href;
|
||||||
@@ -98,6 +101,7 @@ api.checkout = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {get} /paypal/checkout/success Paypal checkout success
|
* @api {get} /paypal/checkout/success Paypal checkout success
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName PaypalCheckoutSuccess
|
* @apiName PaypalCheckoutSuccess
|
||||||
@@ -110,7 +114,7 @@ api.checkout = {
|
|||||||
**/
|
**/
|
||||||
api.checkoutSuccess = {
|
api.checkoutSuccess = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/payments/paypal/checkout/success',
|
url: '/paypal/checkout/success',
|
||||||
middlewares: [authWithSession],
|
middlewares: [authWithSession],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let paymentId = req.query.paymentId;
|
let paymentId = req.query.paymentId;
|
||||||
@@ -144,6 +148,7 @@ api.checkoutSuccess = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {get} /paypal/subscribe Paypal subscribe
|
* @api {get} /paypal/subscribe Paypal subscribe
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName PaypalSubscribe
|
* @apiName PaypalSubscribe
|
||||||
@@ -156,7 +161,7 @@ api.checkoutSuccess = {
|
|||||||
**/
|
**/
|
||||||
api.subscribe = {
|
api.subscribe = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/payments/paypal/subscribe',
|
url: '/paypal/subscribe',
|
||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let sub = shared.content.subscriptionBlocks[req.query.sub];
|
let sub = shared.content.subscriptionBlocks[req.query.sub];
|
||||||
@@ -190,6 +195,7 @@ api.subscribe = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {get} /paypal/subscribe/success Paypal subscribe success
|
* @api {get} /paypal/subscribe/success Paypal subscribe success
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName PaypalSubscribeSuccess
|
* @apiName PaypalSubscribeSuccess
|
||||||
@@ -201,7 +207,7 @@ api.subscribe = {
|
|||||||
**/
|
**/
|
||||||
api.subscribeSuccess = {
|
api.subscribeSuccess = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/payments/paypal/subscribe/success',
|
url: '/paypal/subscribe/success',
|
||||||
middlewares: [authWithSession],
|
middlewares: [authWithSession],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -223,6 +229,7 @@ api.subscribeSuccess = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {get} /paypal/subscribe/cancel Paypal subscribe cancel
|
* @api {get} /paypal/subscribe/cancel Paypal subscribe cancel
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName PaypalSubscribeCancel
|
* @apiName PaypalSubscribeCancel
|
||||||
@@ -234,7 +241,7 @@ api.subscribeSuccess = {
|
|||||||
**/
|
**/
|
||||||
api.subscribeCancel = {
|
api.subscribeCancel = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/payments/paypal/subscribe/cancel',
|
url: '/paypal/subscribe/cancel',
|
||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -261,6 +268,7 @@ api.subscribeCancel = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {post} /paypal/ipn Paypal IPN
|
* @api {post} /paypal/ipn Paypal IPN
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName PaypalIpn
|
* @apiName PaypalIpn
|
||||||
@@ -273,7 +281,7 @@ api.subscribeCancel = {
|
|||||||
**/
|
**/
|
||||||
api.ipn = {
|
api.ipn = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/payments/paypal/ipn',
|
url: '/paypal/ipn',
|
||||||
middlewares: [],
|
middlewares: [],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
res.respond(200);
|
res.respond(200);
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
|
|
||||||
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
|
|
||||||
// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json),
|
|
||||||
// and once for any time you need to edit the plan thereafter
|
|
||||||
import path from 'path';
|
|
||||||
import nconf from 'nconf';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import paypal from 'paypal-rest-sdk';
|
|
||||||
import shared from '../../../../../common';
|
|
||||||
|
|
||||||
let blocks = shared.content.subscriptionBlocks;
|
|
||||||
const BILLING_PLAN_TITLE = 'Habitica Subscription';
|
|
||||||
const LIVE = nconf.get('PAYPAL:mode') === 'live';
|
|
||||||
const OP = 'create'; // list create update remove
|
|
||||||
|
|
||||||
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
|
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
paypal.configure({
|
|
||||||
mode: nconf.get('PAYPAL:mode'), // sandbox or live
|
|
||||||
client_id: nconf.get('PAYPAL:client_id'),
|
|
||||||
client_secret: nconf.get('PAYPAL:client_secret'),
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://developer.paypal.com/docs/api/#billing-plans-and-agreements
|
|
||||||
let billingPlanAttributes = {
|
|
||||||
name: BILLING_PLAN_TITLE,
|
|
||||||
description: BILLING_PLAN_TITLE,
|
|
||||||
type: 'INFINITE',
|
|
||||||
merchant_preferences: {
|
|
||||||
auto_bill_amount: 'yes',
|
|
||||||
cancel_url: LIVE ? 'https://habitica.com' : 'http://localhost:3000',
|
|
||||||
return_url: LIVE ? 'https://habitica.com/paypal/subscribe/success' : 'http://localhost:3000/paypal/subscribe/success',
|
|
||||||
},
|
|
||||||
payment_definitions: [{
|
|
||||||
type: 'REGULAR',
|
|
||||||
frequency: 'MONTH',
|
|
||||||
cycles: '0',
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
|
|
||||||
_.each(blocks, function defineBlock (block) {
|
|
||||||
block.definition = _.cloneDeep(billingPlanAttributes);
|
|
||||||
_.merge(block.definition.payment_definitions[0], {
|
|
||||||
name: `${BILLING_PLAN_TITLE} (\$${block.price} every ${block.months} months, recurring)`,
|
|
||||||
frequency_interval: `${block.months}`,
|
|
||||||
amount: {
|
|
||||||
currency: 'USD',
|
|
||||||
value: `${block.price}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let update = {
|
|
||||||
op: 'replace',
|
|
||||||
path: '/merchant_preferences',
|
|
||||||
value: {
|
|
||||||
cancel_url: 'https://habitica.com',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (OP) {
|
|
||||||
case 'list':
|
|
||||||
paypal.billingPlan.list({status: 'ACTIVE'}, function listPlans () {
|
|
||||||
// TODO Was a console.log statement. Need proper response output
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'get':
|
|
||||||
paypal.billingPlan.get(nconf.get('PAYPAL:billing_plans:12'), function getPlan () {
|
|
||||||
// TODO Was a console.log statement. Need proper response output
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'update':
|
|
||||||
paypal.billingPlan.update(nconf.get('PAYPAL:billing_plans:12'), update, function updatePlan () {
|
|
||||||
// TODO Was a console.log statement. Need proper response output
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'create':
|
|
||||||
paypal.billingPlan.create(blocks.google_6mo.definition, function createPlan (err, plan) {
|
|
||||||
if (err) return; // TODO Was a console.log statement. Need proper response output
|
|
||||||
if (plan.state === 'ACTIVE')
|
|
||||||
return; // TODO Was a console.log statement. Need proper response output
|
|
||||||
let billingPlanUpdateAttributes = [{
|
|
||||||
op: 'replace',
|
|
||||||
path: '/',
|
|
||||||
value: {
|
|
||||||
state: 'ACTIVE',
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
// Activate the plan by changing status to Active
|
|
||||||
paypal.billingPlan.update(plan.id, billingPlanUpdateAttributes, function activatePlan () {
|
|
||||||
// TODO Was a console.log statement. Need proper response output
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'remove': break;
|
|
||||||
}
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
@@ -2,6 +2,7 @@ import stripeModule from 'stripe';
|
|||||||
import shared from '../../../../../common';
|
import shared from '../../../../../common';
|
||||||
import {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
} from '../../../libs/api-v3/errors';
|
} from '../../../libs/api-v3/errors';
|
||||||
import { model as Coupon } from '../../../models/coupon';
|
import { model as Coupon } from '../../../models/coupon';
|
||||||
import payments from '../../../libs/api-v3/payments';
|
import payments from '../../../libs/api-v3/payments';
|
||||||
@@ -18,22 +19,23 @@ const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
|||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @apiIgnore Payments are considered part of the private API
|
||||||
* @api {post} /stripe/checkout Stripe checkout
|
* @api {post} /stripe/checkout Stripe checkout
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName StripeCheckout
|
* @apiName StripeCheckout
|
||||||
* @apiGroup Payments
|
* @apiGroup Payments
|
||||||
*
|
*
|
||||||
* @apiParam {string} id The token
|
* @apiParam {string} id Body parameter - The token
|
||||||
* @apiParam {string} gift stringified json object, gift
|
* @apiParam {string} email Body parameter - the customer email
|
||||||
* @apiParam {string} sub subscription, possible values are: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo
|
* @apiParam {string} gift Query parameter - stringified json object, gift
|
||||||
* @apiParam {string} coupon coupon for the matching subscription, required only for certain subscriptions
|
* @apiParam {string} sub Query parameter - subscription, possible values are: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo
|
||||||
* @apiParam {string} email the customer email
|
* @apiParam {string} coupon Query parameter - coupon for the matching subscription, required only for certain subscriptions
|
||||||
*
|
*
|
||||||
* @apiSuccess {} empty object
|
* @apiSuccess {Object} data Empty object
|
||||||
**/
|
**/
|
||||||
api.checkout = {
|
api.checkout = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/payments/stripe/checkout',
|
url: '/stripe/checkout',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let token = req.body.id;
|
let token = req.body.id;
|
||||||
@@ -49,15 +51,16 @@ api.checkout = {
|
|||||||
coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key});
|
coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key});
|
||||||
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
|
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
|
||||||
}
|
}
|
||||||
let customer = {
|
|
||||||
|
response = await stripe.customers.create({
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
metadata: { uuid: user._id },
|
metadata: { uuid: user._id },
|
||||||
card: token,
|
card: token,
|
||||||
plan: sub.key,
|
plan: sub.key,
|
||||||
};
|
});
|
||||||
response = await stripe.customers.create(customer);
|
|
||||||
} else {
|
} else {
|
||||||
let amount = 500; // $5
|
let amount = 500; // $5
|
||||||
|
|
||||||
if (gift) {
|
if (gift) {
|
||||||
if (gift.type === 'subscription') {
|
if (gift.type === 'subscription') {
|
||||||
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
|
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
|
||||||
@@ -65,6 +68,7 @@ api.checkout = {
|
|||||||
amount = `${gift.gems.amount / 4 * 100}`;
|
amount = `${gift.gems.amount / 4 * 100}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response = await stripe.charges.create({
|
response = await stripe.charges.create({
|
||||||
amount,
|
amount,
|
||||||
currency: 'usd',
|
currency: 'usd',
|
||||||
@@ -87,80 +91,74 @@ api.checkout = {
|
|||||||
paymentMethod: 'Stripe',
|
paymentMethod: 'Stripe',
|
||||||
gift,
|
gift,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (gift) {
|
if (gift) {
|
||||||
let member = await User.findById(gift.uuid);
|
let member = await User.findById(gift.uuid);
|
||||||
gift.member = member;
|
gift.member = member;
|
||||||
if (gift.type === 'subscription') method = 'createSubscription';
|
if (gift.type === 'subscription') method = 'createSubscription';
|
||||||
data.paymentMethod = 'Gift';
|
data.paymentMethod = 'Gift';
|
||||||
}
|
}
|
||||||
|
|
||||||
await payments[method](data);
|
await payments[method](data);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /stripe/subscribe/edit Stripe subscribeEdit
|
* @apiIgnore Payments are considered part of the private API
|
||||||
|
* @api {post} /stripe/subscribe/edit Edit Stripe subscription
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName StripeSubscribeEdit
|
* @apiName StripeSubscribeEdit
|
||||||
* @apiGroup Payments
|
* @apiGroup Payments
|
||||||
*
|
*
|
||||||
* @apiParam {string} id The token
|
* @apiParam {string} id Body parameter - The token
|
||||||
*
|
*
|
||||||
* @apiSuccess {}
|
* @apiSuccess {Object} data Empty object
|
||||||
**/
|
**/
|
||||||
api.subscribeEdit = {
|
api.subscribeEdit = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/payments/stripe/subscribe/edit',
|
url: '/stripe/subscribe/edit',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let token = req.body.id;
|
let token = req.body.id;
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let customerId = user.purchased.plan.customerId;
|
let customerId = user.purchased.plan.customerId;
|
||||||
|
|
||||||
if (!customerId) throw new BadRequest(res.t('missingSubscription'));
|
if (!customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||||
|
|
||||||
try {
|
let subscriptions = await stripe.customers.listSubscriptions(customerId);
|
||||||
let subscriptions = await stripe.customers.listSubscriptions(customerId);
|
let subscriptionId = subscriptions.data[0].id;
|
||||||
let subscriptionId = subscriptions.data[0].id;
|
await stripe.customers.updateSubscription(customerId, subscriptionId, { card: token });
|
||||||
await stripe.customers.updateSubscription(customerId, subscriptionId, { card: token });
|
res.respond(200, {});
|
||||||
res.respond(200, {});
|
|
||||||
} catch (error) {
|
|
||||||
throw new BadRequest(error.message);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /stripe/subscribe/cancel Stripe subscribeCancel
|
* @apiIgnore Payments are considered part of the private API
|
||||||
|
* @api {get} /stripe/subscribe/cancel Cancel Stripe subscription
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName StripeSubscribeCancel
|
* @apiName StripeSubscribeCancel
|
||||||
* @apiGroup Payments
|
* @apiGroup Payments
|
||||||
*
|
|
||||||
* @apiParam
|
|
||||||
*
|
|
||||||
* @apiSuccess {}
|
|
||||||
**/
|
**/
|
||||||
api.subscribeCancel = {
|
api.subscribeCancel = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/payments/stripe/subscribe/cancel',
|
url: '/stripe/subscribe/cancel',
|
||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
if (!user.purchased.plan.customerId) throw new BadRequest(res.t('missingSubscription'));
|
if (!user.purchased.plan.customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||||
try {
|
|
||||||
let customer = await stripe.customers.retrieve(user.purchased.plan.customeerId);
|
let customer = await stripe.customers.retrieve(user.purchased.plan.customeerId);
|
||||||
await stripe.customers.del(user.purchased.plan.customerId);
|
await stripe.customers.del(user.purchased.plan.customerId);
|
||||||
let data = {
|
await payments.cancelSubscriptoin({
|
||||||
user,
|
user,
|
||||||
nextBill: customer.subscription.current_period_end * 1000, // timestamp in seconds
|
nextBill: customer.subscription.current_period_end * 1000, // timestamp in seconds
|
||||||
paymentMethod: 'Stripe',
|
paymentMethod: 'Stripe',
|
||||||
};
|
});
|
||||||
await payments.cancelSubscriptoin(data);
|
|
||||||
res.respond(200, {});
|
res.redirect('/');
|
||||||
} catch (e) {
|
|
||||||
throw new BadRequest(e);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import amazonPayments from 'amazon-payments';
|
import amazonPayments from 'amazon-payments';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import common from '../../../../common';
|
import common from '../../../../common';
|
||||||
let t = common.i18n.t;
|
|
||||||
const IS_PROD = nconf.get('NODE_ENV') === 'production';
|
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
|
import {
|
||||||
|
BadRequest,
|
||||||
|
} from './errors';
|
||||||
|
|
||||||
|
const t = common.i18n.t;
|
||||||
|
const IS_PROD = nconf.get('NODE_ENV') === 'production';
|
||||||
|
|
||||||
let amzPayment = amazonPayments.connect({
|
let amzPayment = amazonPayments.connect({
|
||||||
environment: amazonPayments.Environment[IS_PROD ? 'Production' : 'Sandbox'],
|
environment: amazonPayments.Environment[IS_PROD ? 'Production' : 'Sandbox'],
|
||||||
@@ -13,10 +17,6 @@ let amzPayment = amazonPayments.connect({
|
|||||||
clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'),
|
clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* From: https://payments.amazon.com/documentation/apireference/201751670#201751670
|
|
||||||
*/
|
|
||||||
|
|
||||||
let getTokenInfo = Q.nbind(amzPayment.api.getTokenInfo, amzPayment.api);
|
let getTokenInfo = Q.nbind(amzPayment.api.getTokenInfo, amzPayment.api);
|
||||||
let createOrderReferenceId = Q.nbind(amzPayment.offAmazonPayments.createOrderReferenceForId, amzPayment.offAmazonPayments);
|
let createOrderReferenceId = Q.nbind(amzPayment.offAmazonPayments.createOrderReferenceForId, amzPayment.offAmazonPayments);
|
||||||
let setOrderReferenceDetails = Q.nbind(amzPayment.offAmazonPayments.setOrderReferenceDetails, amzPayment.offAmazonPayments);
|
let setOrderReferenceDetails = Q.nbind(amzPayment.offAmazonPayments.setOrderReferenceDetails, amzPayment.offAmazonPayments);
|
||||||
@@ -30,7 +30,7 @@ let authorizeOnBillingAgreement = (inputSet) => {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => {
|
amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(t('paymentNotSuccessful'));
|
if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(t('paymentNotSuccessful')));
|
||||||
return resolve(response);
|
return resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -40,7 +40,7 @@ let authorize = (inputSet) => {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => {
|
amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(t('paymentNotSuccessful'));
|
if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(t('paymentNotSuccessful')));
|
||||||
return resolve(response);
|
return resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import nconf from 'nconf';
|
|||||||
import pushNotify from './pushNotifications';
|
import pushNotify from './pushNotifications';
|
||||||
import shared from '../../../../common' ;
|
import shared from '../../../../common' ;
|
||||||
|
|
||||||
import iap from '../../controllers/top-level/payments/iap';
|
|
||||||
import paypal from '../../controllers/top-level/payments/paypal';
|
|
||||||
import stripe from '../../controllers/top-level/payments/stripe';
|
|
||||||
|
|
||||||
const IS_PROD = nconf.get('IS_PROD');
|
const IS_PROD = nconf.get('IS_PROD');
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
@@ -45,6 +41,7 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
plan.dateTerminated = moment(plan.dateTerminated).add({months}).toDate();
|
plan.dateTerminated = moment(plan.dateTerminated).add({months}).toDate();
|
||||||
if (!plan.dateUpdated) plan.dateUpdated = new Date();
|
if (!plan.dateUpdated) plan.dateUpdated = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!plan.customerId) plan.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 {
|
} else {
|
||||||
_(plan).merge({ // override with these values
|
_(plan).merge({ // override with these values
|
||||||
@@ -73,12 +70,13 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
|
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
|
||||||
plan.consecutive.trinkets += perks;
|
plan.consecutive.trinkets += perks;
|
||||||
}
|
}
|
||||||
|
|
||||||
revealMysteryItems(recipient);
|
revealMysteryItems(recipient);
|
||||||
|
|
||||||
if (IS_PROD) {
|
if (IS_PROD) {
|
||||||
if (!data.gift) txnEmail(data.user, 'subscription-begins');
|
if (!data.gift) txnEmail(data.user, 'subscription-begins');
|
||||||
|
|
||||||
let analyticsData = {
|
analytics.trackPurchase({
|
||||||
uuid: data.user._id,
|
uuid: data.user._id,
|
||||||
itemPurchased: 'Subscription',
|
itemPurchased: 'Subscription',
|
||||||
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
|
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
|
||||||
@@ -87,8 +85,7 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
gift: Boolean(data.gift),
|
gift: Boolean(data.gift),
|
||||||
purchaseValue: block.price,
|
purchaseValue: block.price,
|
||||||
};
|
});
|
||||||
analytics.trackPurchase(analyticsData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.user.purchased.txnCount++;
|
data.user.purchased.txnCount++;
|
||||||
@@ -114,9 +111,7 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
if (data.gift) await data.gift.member.save();
|
if (data.gift) await data.gift.member.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Sets their subscription to be cancelled later
|
||||||
* Sets their subscription to be cancelled later
|
|
||||||
*/
|
|
||||||
api.cancelSubscription = async function cancelSubscription (data) {
|
api.cancelSubscription = async function cancelSubscription (data) {
|
||||||
let plan = data.user.purchased.plan;
|
let plan = data.user.purchased.plan;
|
||||||
let now = moment();
|
let now = moment();
|
||||||
@@ -146,12 +141,14 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
|||||||
api.buyGems = async function buyGems (data) {
|
api.buyGems = async function buyGems (data) {
|
||||||
let amt = data.amount || 5;
|
let amt = data.amount || 5;
|
||||||
amt = data.gift ? data.gift.gems.amount / 4 : amt;
|
amt = data.gift ? data.gift.gems.amount / 4 : amt;
|
||||||
|
|
||||||
(data.gift ? data.gift.member : data.user).balance += amt;
|
(data.gift ? data.gift.member : data.user).balance += amt;
|
||||||
data.user.purchased.txnCount++;
|
data.user.purchased.txnCount++;
|
||||||
|
|
||||||
if (IS_PROD) {
|
if (IS_PROD) {
|
||||||
if (!data.gift) txnEmail(data.user, 'donation');
|
if (!data.gift) txnEmail(data.user, 'donation');
|
||||||
|
|
||||||
let analyticsData = {
|
analytics.trackPurchase({
|
||||||
uuid: data.user._id,
|
uuid: data.user._id,
|
||||||
itemPurchased: 'Gems',
|
itemPurchased: 'Gems',
|
||||||
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
|
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
|
||||||
@@ -160,8 +157,7 @@ api.buyGems = async function buyGems (data) {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
gift: Boolean(data.gift),
|
gift: Boolean(data.gift),
|
||||||
purchaseValue: amt,
|
purchaseValue: amt,
|
||||||
};
|
});
|
||||||
analytics.trackPurchase(analyticsData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.gift) {
|
if (data.gift) {
|
||||||
@@ -179,23 +175,11 @@ api.buyGems = async function buyGems (data) {
|
|||||||
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
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}`);
|
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await data.gift.member.save();
|
await data.gift.member.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
await data.user.save();
|
await data.user.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
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.iapAndroidVerify = iap.androidVerify;
|
|
||||||
api.iapIosVerify = iap.iosVerify;
|
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ module.exports = function errorHandler (err, req, res, next) { // eslint-disable
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Stripe Card errors errors (can be safely shown to the users)
|
||||||
|
// https://stripe.com/docs/api/node#errors
|
||||||
|
if (err.type === 'StripeCardError') {
|
||||||
|
responseErr = new BadRequest(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
if (!responseErr || responseErr.httpCode >= 500) {
|
if (!responseErr || responseErr.httpCode >= 500) {
|
||||||
// Try to identify the error...
|
// Try to identify the error...
|
||||||
// ...
|
// ...
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ v2app.use(responseHandler);
|
|||||||
|
|
||||||
// Custom Directives
|
// Custom Directives
|
||||||
v2app.use('/', require('../../routes/api-v2/auth'));
|
v2app.use('/', require('../../routes/api-v2/auth'));
|
||||||
// v2app.use('/', require('../../routes/api-v2/coupon')); // TODO REMOVE - ONLY v3
|
|
||||||
// v2app.use('/', require('../../routes/api-v2/unsubscription')); // TODO REMOVE - ONLY v3
|
|
||||||
|
|
||||||
require('../../routes/api-v2/swagger')(swagger, v2app);
|
require('../../routes/api-v2/swagger')(swagger, v2app);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import baseModel from '../libs/api-v3/baseModel';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as Tasks from './task';
|
import * as Tasks from './task';
|
||||||
import { model as User } from './user';
|
import { model as User } from './user';
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
TAVERN_ID,
|
||||||
|
} from './group';
|
||||||
import { removeFromArray } from '../libs/api-v3/collectionManipulators';
|
import { removeFromArray } from '../libs/api-v3/collectionManipulators';
|
||||||
|
import shared from '../../../common';
|
||||||
|
import { sendTxn as txnEmail } from '../libs/api-v3/email';
|
||||||
|
import sendPushNotification from '../libs/api-v3/pushNotifications';
|
||||||
|
|
||||||
let Schema = mongoose.Schema;
|
let Schema = mongoose.Schema;
|
||||||
|
|
||||||
@@ -251,6 +258,65 @@ schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO everything here should be moved to a worker
|
||||||
|
// actually even for a worker it's probably just too big and will kill mongo
|
||||||
|
schema.methods.closeChal = async function closeChal (broken = {}) {
|
||||||
|
let challenge = this;
|
||||||
|
|
||||||
|
let winner = broken.winner;
|
||||||
|
let brokenReason = broken.broken;
|
||||||
|
|
||||||
|
// Delete the challenge
|
||||||
|
await this.model('Challenge').remove({_id: challenge._id}).exec();
|
||||||
|
|
||||||
|
// Refund the leader if the challenge is closed and the group not the tavern
|
||||||
|
if (challenge.group !== TAVERN_ID && brokenReason === 'CHALLENGE_DELETED') {
|
||||||
|
await User.update({_id: challenge.leader}, {$inc: {balance: challenge.prize / 4}}).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the challengeCount on the group
|
||||||
|
await Group.update({_id: challenge.group}, {$inc: {challengeCount: -1}}).exec();
|
||||||
|
|
||||||
|
// Award prize to winner and notify
|
||||||
|
if (winner) {
|
||||||
|
winner.achievements.challenges.push(challenge.name);
|
||||||
|
winner.balance += challenge.prize / 4;
|
||||||
|
let savedWinner = await winner.save();
|
||||||
|
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
|
||||||
|
txnEmail(savedWinner, 'won-challenge', [
|
||||||
|
{name: 'CHALLENGE_NAME', content: challenge.name},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPushNotification(savedWinner, shared.i18n.t('wonChallenge'), challenge.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run some operations in the background withouth blocking the thread
|
||||||
|
let backgroundTasks = [
|
||||||
|
// And it's tasks
|
||||||
|
Tasks.Task.remove({'challenge.id': challenge._id, userId: {$exists: false}}).exec(),
|
||||||
|
// Set the challenge tag to non-challenge status and remove the challenge from the user's challenges
|
||||||
|
User.update({
|
||||||
|
challenges: challenge._id,
|
||||||
|
'tags._id': challenge._id,
|
||||||
|
}, {
|
||||||
|
$set: {'tags.$.challenge': false},
|
||||||
|
$pull: {challenges: challenge._id},
|
||||||
|
}, {multi: true}).exec(),
|
||||||
|
// Break users' tasks
|
||||||
|
Tasks.Task.update({
|
||||||
|
'challenge.id': challenge._id,
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
'challenge.broken': brokenReason,
|
||||||
|
'challenge.winner': winner && winner.profile.name,
|
||||||
|
},
|
||||||
|
}, {multi: true}).exec(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Q.all(backgroundTasks);
|
||||||
|
};
|
||||||
|
|
||||||
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the challenge model)
|
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the challenge model)
|
||||||
// These will be removed once API v2 is discontinued
|
// These will be removed once API v2 is discontinued
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user