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
|
||||
|
||||
debug-scripts/*
|
||||
scripts/*
|
||||
tasks/*.js
|
||||
gulpfile.js
|
||||
Gruntfile.js
|
||||
|
||||
@@ -143,6 +143,8 @@ function processChallenges (afterId) {
|
||||
|
||||
oldTask.tags = _.map(oldTask.tags || {}, function (tagPresent, tagId) {
|
||||
return tagPresent && tagId;
|
||||
}).filter(function (tag) {
|
||||
return tag !== false;
|
||||
});
|
||||
|
||||
if (!oldTask.text) oldTask.text = 'task text'; // required
|
||||
|
||||
@@ -165,6 +165,8 @@ function processUsers (afterId) {
|
||||
if (!oldTask.text) oldTask.text = 'task text'; // required
|
||||
oldTask.tags = _.map(oldTask.tags, function (tagPresent, tagId) {
|
||||
return tagPresent && tagId;
|
||||
}).filter(function (tag) {
|
||||
return tag !== false;
|
||||
});
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -304,7 +302,7 @@ api.delete = async function(req, res, next){
|
||||
if (!challenge.canModify(user)) return next(shared.i18n.t('noPermissionCloseChallenge'));
|
||||
|
||||
// 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);
|
||||
} catch (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.');
|
||||
|
||||
// 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, {});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
||||
@@ -2,9 +2,6 @@ import validator from 'validator';
|
||||
import moment from 'moment';
|
||||
import passport from 'passport';
|
||||
import nconf from 'nconf';
|
||||
import setupNconf from '../../libs/api-v3/setupNconf';
|
||||
setupNconf();
|
||||
|
||||
import {
|
||||
authWithHeaders,
|
||||
} from '../../middlewares/api-v3/auth';
|
||||
|
||||
@@ -14,10 +14,7 @@ import {
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
} from '../../libs/api-v3/errors';
|
||||
import shared from '../../../../common';
|
||||
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 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
|
||||
* @apiVersion 3.0.0
|
||||
@@ -534,7 +473,7 @@ api.deleteChallenge = {
|
||||
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||
|
||||
// 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, {});
|
||||
},
|
||||
};
|
||||
@@ -571,7 +510,7 @@ api.selectChallengeWinner = {
|
||||
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
|
||||
await _closeChal(challenge, {broken: 'CHALLENGE_CLOSED', winner});
|
||||
await challenge.closeChal({broken: 'CHALLENGE_CLOSED', winner});
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,9 +14,6 @@ import { sendTxn } from '../../libs/api-v3/email';
|
||||
import nconf from 'nconf';
|
||||
import Q from 'q';
|
||||
|
||||
import setupNconf from '../../libs/api-v3/setupNconf';
|
||||
setupNconf();
|
||||
|
||||
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => {
|
||||
return { email, canSend: true };
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../libs/api-v3/errors';
|
||||
import amzLib from '../../../libs/api-v3/amazonPayments';
|
||||
import {
|
||||
@@ -16,6 +17,7 @@ import cc from 'coupon-code';
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /amazon/verifyAccessToken verify access token
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonVerifyAccessToken
|
||||
@@ -23,11 +25,11 @@ let api = {};
|
||||
*
|
||||
* @apiParam {string} access_token the access token
|
||||
*
|
||||
* @apiSuccess {} empty
|
||||
* @apiSuccess {Object} data Empty object
|
||||
**/
|
||||
api.verifyAccessToken = {
|
||||
method: 'POST',
|
||||
url: '/payments/amazon/verifyAccessToken',
|
||||
url: '/amazon/verifyAccessToken',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
try {
|
||||
@@ -40,6 +42,7 @@ api.verifyAccessToken = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /amazon/createOrderReferenceId create order reference id
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonCreateOrderReferenceId
|
||||
@@ -51,7 +54,7 @@ api.verifyAccessToken = {
|
||||
**/
|
||||
api.createOrderReferenceId = {
|
||||
method: 'POST',
|
||||
url: '/payments/amazon/createOrderReferenceId',
|
||||
url: '/amazon/createOrderReferenceId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
try {
|
||||
@@ -70,6 +73,7 @@ api.createOrderReferenceId = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /amazon/checkout do checkout
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonCheckout
|
||||
@@ -81,7 +85,7 @@ api.createOrderReferenceId = {
|
||||
**/
|
||||
api.checkout = {
|
||||
method: 'POST',
|
||||
url: '/payments/amazon/checkout',
|
||||
url: '/amazon/checkout',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
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
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonSubscribe
|
||||
@@ -161,7 +166,7 @@ api.checkout = {
|
||||
**/
|
||||
api.subscribe = {
|
||||
method: 'POST',
|
||||
url: '/payments/amazon/subscribe',
|
||||
url: '/amazon/subscribe',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
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
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonSubscribe
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiSuccess {object} empty object
|
||||
**/
|
||||
api.subscribeCancel = {
|
||||
method: 'GET',
|
||||
url: '/payments/amazon/subscribe/cancel',
|
||||
url: '/amazon/subscribe/cancel',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
if (!billingAgreementId) throw new BadRequest(res.t('missingSubscription'));
|
||||
if (!billingAgreementId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||
|
||||
try {
|
||||
await amzLib.closeBillingAgreement({
|
||||
@@ -257,7 +261,7 @@ api.subscribeCancel = {
|
||||
};
|
||||
await payments.cancelSubscription(data);
|
||||
|
||||
res.respond(200, {});
|
||||
res.redirect('/');
|
||||
} catch (error) {
|
||||
throw new BadRequest(error.message);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import iap from 'in-app-purchase';
|
||||
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({
|
||||
// this is the path to the directory containing iap-sanbox/iap-live files
|
||||
@@ -7,18 +14,28 @@ iap.config({
|
||||
});
|
||||
|
||||
// Validation ERROR Codes
|
||||
// const INVALID_PAYLOAD = 6778001;
|
||||
const INVALID_PAYLOAD = 6778001;
|
||||
// const CONNECTION_FAILED = 6778002;
|
||||
// const PURCHASE_EXPIRED = 6778003;
|
||||
|
||||
let api = {};
|
||||
|
||||
/*
|
||||
api.androidVerify = function androidVerify (req, res) {
|
||||
let iapBody = req.body;
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /iap/android/verify Android Verify IAP
|
||||
* @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) {
|
||||
let resObj = {
|
||||
ok: false,
|
||||
@@ -39,7 +56,7 @@ api.androidVerify = function androidVerify (req, res) {
|
||||
};
|
||||
|
||||
// iap is ready
|
||||
iap.validate(iap.GOOGLE, testObj, function googleValidateResult (err, googleRes) {
|
||||
iap.validate(iap.GOOGLE, testObj, (err, googleRes) => {
|
||||
if (err) {
|
||||
let resObj = {
|
||||
ok: false,
|
||||
@@ -58,15 +75,29 @@ api.androidVerify = function androidVerify (req, res) {
|
||||
data: googleRes,
|
||||
};
|
||||
|
||||
payments.buyGems({user, paymentMethod: 'IAP GooglePlay', amount: 5.25});
|
||||
|
||||
return res.json(resObj);
|
||||
payments.buyGems({
|
||||
user,
|
||||
paymentMethod: 'IAP GooglePlay',
|
||||
amount: 5.25,
|
||||
}).then(() => res.json(resObj));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
exports.iosVerify = function iosVerify (req, res) {
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @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;
|
||||
|
||||
@@ -81,7 +112,7 @@ exports.iosVerify = function iosVerify (req, res) {
|
||||
}
|
||||
|
||||
// iap is ready
|
||||
iap.validate(iap.APPLE, iapBody.transaction.receipt, function iosValidateResult (err, appleRes) {
|
||||
iap.validate(iap.APPLE, iapBody.transaction.receipt, (err, appleRes) => {
|
||||
if (err) {
|
||||
let resObj = {
|
||||
ok: false,
|
||||
@@ -98,6 +129,7 @@ exports.iosVerify = function iosVerify (req, res) {
|
||||
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':
|
||||
@@ -117,15 +149,18 @@ exports.iosVerify = function iosVerify (req, res) {
|
||||
correctReceipt = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (correctReceipt) {
|
||||
let resObj = {
|
||||
ok: true,
|
||||
data: appleRes,
|
||||
};
|
||||
|
||||
// yay good!
|
||||
return res.json(resObj);
|
||||
}
|
||||
}
|
||||
|
||||
// wrong receipt content
|
||||
let resObj = {
|
||||
ok: false,
|
||||
@@ -134,8 +169,10 @@ exports.iosVerify = function iosVerify (req, res) {
|
||||
message: 'Incorrect receipt content',
|
||||
},
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
// invalid receipt
|
||||
let resObj = {
|
||||
ok: false,
|
||||
@@ -148,7 +185,7 @@ exports.iosVerify = function iosVerify (req, res) {
|
||||
return res.json(resObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
*/
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
@@ -17,6 +19,8 @@ import {
|
||||
} from '../../../libs/api-v3/errors';
|
||||
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
|
||||
// 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
|
||||
@@ -24,8 +28,6 @@ _.each(shared.content.subscriptionBlocks, (block) => {
|
||||
block.paypalKey = nconf.get(`PAYPAL:billing_plans:${block.key}`);
|
||||
});
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
paypal.configure({
|
||||
mode: nconf.get('PAYPAL:mode'), // sandbox or live
|
||||
client_id: nconf.get('PAYPAL:client_id'),
|
||||
@@ -35,18 +37,18 @@ paypal.configure({
|
||||
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
|
||||
* @apiName PaypalCheckout
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiParam {string} gift The stringified object representing the user, the gift recepient.
|
||||
*
|
||||
* @apiSuccess {} redirect
|
||||
* @apiParam {string} gift Query parameter - The stringified object representing the user, the gift recepient.
|
||||
**/
|
||||
api.checkout = {
|
||||
method: 'GET',
|
||||
url: '/payments/paypal/checkout',
|
||||
url: '/paypal/checkout',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||
@@ -68,8 +70,8 @@ api.checkout = {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
redirect_urls: {
|
||||
return_url: `${nconf.get('BASE_URL')}/paypal/checkout/success`,
|
||||
cancel_url: `${nconf.get('BASE_URL')}`,
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
},
|
||||
transactions: [{
|
||||
item_list: {
|
||||
@@ -87,6 +89,7 @@ api.checkout = {
|
||||
description,
|
||||
}],
|
||||
};
|
||||
|
||||
try {
|
||||
let result = await paypal.payment.create(createPayment);
|
||||
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
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalCheckoutSuccess
|
||||
@@ -110,7 +114,7 @@ api.checkout = {
|
||||
**/
|
||||
api.checkoutSuccess = {
|
||||
method: 'GET',
|
||||
url: '/payments/paypal/checkout/success',
|
||||
url: '/paypal/checkout/success',
|
||||
middlewares: [authWithSession],
|
||||
async handler (req, res) {
|
||||
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
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalSubscribe
|
||||
@@ -156,7 +161,7 @@ api.checkoutSuccess = {
|
||||
**/
|
||||
api.subscribe = {
|
||||
method: 'GET',
|
||||
url: '/payments/paypal/subscribe',
|
||||
url: '/paypal/subscribe',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
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
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalSubscribeSuccess
|
||||
@@ -201,7 +207,7 @@ api.subscribe = {
|
||||
**/
|
||||
api.subscribeSuccess = {
|
||||
method: 'GET',
|
||||
url: '/payments/paypal/subscribe/success',
|
||||
url: '/paypal/subscribe/success',
|
||||
middlewares: [authWithSession],
|
||||
async handler (req, res) {
|
||||
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
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalSubscribeCancel
|
||||
@@ -234,7 +241,7 @@ api.subscribeSuccess = {
|
||||
**/
|
||||
api.subscribeCancel = {
|
||||
method: 'GET',
|
||||
url: '/payments/paypal/subscribe/cancel',
|
||||
url: '/paypal/subscribe/cancel',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
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
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalIpn
|
||||
@@ -273,7 +281,7 @@ api.subscribeCancel = {
|
||||
**/
|
||||
api.ipn = {
|
||||
method: 'POST',
|
||||
url: '/payments/paypal/ipn',
|
||||
url: '/paypal/ipn',
|
||||
middlewares: [],
|
||||
async handler (req, res) {
|
||||
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 {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../libs/api-v3/errors';
|
||||
import { model as Coupon } from '../../../models/coupon';
|
||||
import payments from '../../../libs/api-v3/payments';
|
||||
@@ -18,22 +19,23 @@ const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /stripe/checkout Stripe checkout
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName StripeCheckout
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiParam {string} id The token
|
||||
* @apiParam {string} gift stringified json object, gift
|
||||
* @apiParam {string} sub subscription, possible values are: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo
|
||||
* @apiParam {string} coupon coupon for the matching subscription, required only for certain subscriptions
|
||||
* @apiParam {string} email the customer email
|
||||
* @apiParam {string} id Body parameter - The token
|
||||
* @apiParam {string} email Body parameter - the customer email
|
||||
* @apiParam {string} gift Query parameter - stringified json object, gift
|
||||
* @apiParam {string} sub Query parameter - subscription, possible values are: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo
|
||||
* @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 = {
|
||||
method: 'POST',
|
||||
url: '/payments/stripe/checkout',
|
||||
url: '/stripe/checkout',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let token = req.body.id;
|
||||
@@ -49,15 +51,16 @@ api.checkout = {
|
||||
coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key});
|
||||
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
|
||||
}
|
||||
let customer = {
|
||||
|
||||
response = await stripe.customers.create({
|
||||
email: req.body.email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
};
|
||||
response = await stripe.customers.create(customer);
|
||||
});
|
||||
} else {
|
||||
let amount = 500; // $5
|
||||
|
||||
if (gift) {
|
||||
if (gift.type === 'subscription') {
|
||||
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
|
||||
@@ -65,6 +68,7 @@ api.checkout = {
|
||||
amount = `${gift.gems.amount / 4 * 100}`;
|
||||
}
|
||||
}
|
||||
|
||||
response = await stripe.charges.create({
|
||||
amount,
|
||||
currency: 'usd',
|
||||
@@ -87,80 +91,74 @@ api.checkout = {
|
||||
paymentMethod: 'Stripe',
|
||||
gift,
|
||||
};
|
||||
|
||||
if (gift) {
|
||||
let member = await User.findById(gift.uuid);
|
||||
gift.member = member;
|
||||
if (gift.type === 'subscription') method = 'createSubscription';
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
|
||||
await payments[method](data);
|
||||
}
|
||||
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /stripe/subscribe/edit Stripe subscribeEdit
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /stripe/subscribe/edit Edit Stripe subscription
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName StripeSubscribeEdit
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiParam {string} id The token
|
||||
* @apiParam {string} id Body parameter - The token
|
||||
*
|
||||
* @apiSuccess {}
|
||||
* @apiSuccess {Object} data Empty object
|
||||
**/
|
||||
api.subscribeEdit = {
|
||||
method: 'POST',
|
||||
url: '/payments/stripe/subscribe/edit',
|
||||
url: '/stripe/subscribe/edit',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let token = req.body.id;
|
||||
let user = res.locals.user;
|
||||
let customerId = user.purchased.plan.customerId;
|
||||
|
||||
if (!customerId) throw new BadRequest(res.t('missingSubscription'));
|
||||
if (!customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||
|
||||
try {
|
||||
let subscriptions = await stripe.customers.listSubscriptions(customerId);
|
||||
let subscriptionId = subscriptions.data[0].id;
|
||||
await stripe.customers.updateSubscription(customerId, subscriptionId, { card: token });
|
||||
res.respond(200, {});
|
||||
} catch (error) {
|
||||
throw new BadRequest(error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /stripe/subscribe/cancel Stripe subscribeCancel
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {get} /stripe/subscribe/cancel Cancel Stripe subscription
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName StripeSubscribeCancel
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiParam
|
||||
*
|
||||
* @apiSuccess {}
|
||||
**/
|
||||
api.subscribeCancel = {
|
||||
method: 'GET',
|
||||
url: '/payments/stripe/subscribe/cancel',
|
||||
url: '/stripe/subscribe/cancel',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
if (!user.purchased.plan.customerId) throw new BadRequest(res.t('missingSubscription'));
|
||||
try {
|
||||
if (!user.purchased.plan.customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||
|
||||
let customer = await stripe.customers.retrieve(user.purchased.plan.customeerId);
|
||||
await stripe.customers.del(user.purchased.plan.customerId);
|
||||
let data = {
|
||||
await payments.cancelSubscriptoin({
|
||||
user,
|
||||
nextBill: customer.subscription.current_period_end * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
};
|
||||
await payments.cancelSubscriptoin(data);
|
||||
res.respond(200, {});
|
||||
} catch (e) {
|
||||
throw new BadRequest(e);
|
||||
}
|
||||
});
|
||||
|
||||
res.redirect('/');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import amazonPayments from 'amazon-payments';
|
||||
import nconf from 'nconf';
|
||||
import common from '../../../../common';
|
||||
let t = common.i18n.t;
|
||||
const IS_PROD = nconf.get('NODE_ENV') === 'production';
|
||||
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({
|
||||
environment: amazonPayments.Environment[IS_PROD ? 'Production' : 'Sandbox'],
|
||||
@@ -13,10 +17,6 @@ let amzPayment = amazonPayments.connect({
|
||||
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 createOrderReferenceId = Q.nbind(amzPayment.offAmazonPayments.createOrderReferenceForId, amzPayment.offAmazonPayments);
|
||||
let setOrderReferenceDetails = Q.nbind(amzPayment.offAmazonPayments.setOrderReferenceDetails, amzPayment.offAmazonPayments);
|
||||
@@ -30,7 +30,7 @@ let authorizeOnBillingAgreement = (inputSet) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,7 @@ let authorize = (inputSet) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,6 @@ import nconf from 'nconf';
|
||||
import pushNotify from './pushNotifications';
|
||||
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');
|
||||
|
||||
let api = {};
|
||||
@@ -45,6 +41,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
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
|
||||
@@ -73,12 +70,13 @@ api.createSubscription = async function createSubscription (data) {
|
||||
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 = {
|
||||
analytics.trackPurchase({
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
|
||||
@@ -87,8 +85,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
quantity: 1,
|
||||
gift: Boolean(data.gift),
|
||||
purchaseValue: block.price,
|
||||
};
|
||||
analytics.trackPurchase(analyticsData);
|
||||
});
|
||||
}
|
||||
|
||||
data.user.purchased.txnCount++;
|
||||
@@ -114,9 +111,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
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) {
|
||||
let plan = data.user.purchased.plan;
|
||||
let now = moment();
|
||||
@@ -146,12 +141,14 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
api.buyGems = async function buyGems (data) {
|
||||
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 = {
|
||||
analytics.trackPurchase({
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Gems',
|
||||
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
|
||||
@@ -160,8 +157,7 @@ api.buyGems = async function buyGems (data) {
|
||||
quantity: 1,
|
||||
gift: Boolean(data.gift),
|
||||
purchaseValue: amt,
|
||||
};
|
||||
analytics.trackPurchase(analyticsData);
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
|
||||
}
|
||||
|
||||
await data.gift.member.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;
|
||||
|
||||
@@ -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) {
|
||||
// Try to identify the error...
|
||||
// ...
|
||||
|
||||
@@ -19,8 +19,6 @@ v2app.use(responseHandler);
|
||||
|
||||
// Custom Directives
|
||||
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);
|
||||
|
||||
|
||||
@@ -5,7 +5,14 @@ import baseModel from '../libs/api-v3/baseModel';
|
||||
import _ from 'lodash';
|
||||
import * as Tasks from './task';
|
||||
import { model as User } from './user';
|
||||
import {
|
||||
model as Group,
|
||||
TAVERN_ID,
|
||||
} from './group';
|
||||
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;
|
||||
|
||||
@@ -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)
|
||||
// These will be removed once API v2 is discontinued
|
||||
|
||||
|
||||
Reference in New Issue
Block a user