mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Merge pull request #7030 from HabitRPG/sabrecat/v3-payments
[API v3] Payments refactor
This commit is contained in:
@@ -20,9 +20,9 @@ website/src/routes/payments.js
|
||||
website/src/routes/pages.js
|
||||
website/src/middlewares/apiThrottle.js
|
||||
website/src/middlewares/forceRefresh.js
|
||||
website/src/controllers/payments/
|
||||
|
||||
debug-scripts/*
|
||||
scripts/*
|
||||
tasks/*.js
|
||||
gulpfile.js
|
||||
Gruntfile.js
|
||||
|
||||
@@ -2,5 +2,8 @@
|
||||
"extends": [
|
||||
"habitrpg/server",
|
||||
"habitrpg/babel"
|
||||
]
|
||||
],
|
||||
"globals": {
|
||||
"Promise": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"missingAuthHeaders": "Missing authentication headers.",
|
||||
"missingAuthParams": "Missing authentication parameters.",
|
||||
"missingUsernameEmail": "Missing username or email.",
|
||||
"missingEmail": "Missing email.",
|
||||
"missingUsername": "Missing username.",
|
||||
@@ -100,6 +101,8 @@
|
||||
"noAdminAccess": "You don't have admin access.",
|
||||
"pageMustBeNumber": "req.query.page must be a number",
|
||||
"missingUnsubscriptionCode": "Missing unsubscription code.",
|
||||
"missingSubscription": "User does not have a plan subscription",
|
||||
"missingSubscriptionCode": "Missing subscription code. Possible values: basic_earned, basic_3mo, basic_6mo, google_6mo, basic_12mo.",
|
||||
"userNotFound": "User not found.",
|
||||
"spellNotFound": "Spell \"<%= spellId %>\" not found.",
|
||||
"partyNotFound": "Party not found",
|
||||
@@ -171,5 +174,8 @@
|
||||
"pushDeviceAlreadyAdded": "The user already has the push device",
|
||||
"resetComplete": "Reset completed",
|
||||
"lvl10ChangeClass": "To change class you must be at least level 10.",
|
||||
"equipmentAlreadyOwned": "You already own that piece of equipment"
|
||||
"equipmentAlreadyOwned": "You already own that piece of equipment",
|
||||
"paymentNotSuccessful": "The payment was not successful",
|
||||
"planNotActive": "The plan hasn't activated yet (due to a PayPal bug). It will begin <%= nextBillingDate %>, after which you can cancel to retain your full benefits",
|
||||
"cancelingSubscription": "Canceling the subscription"
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
// 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');
|
||||
_ = require('lodash');
|
||||
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
|
||||
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({
|
||||
@@ -358,6 +358,10 @@ gulp.task('test:api-v3:unit', (done) => {
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:api-v3:unit:watch', () => {
|
||||
gulp.watch(['website/src/libs/api-v3/*', 'test/api/v3/unit/**/*', 'website/src/controllers/**/*'], ['test:api-v3:unit']);
|
||||
});
|
||||
|
||||
gulp.task('test:api-v3:integration', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive'),
|
||||
@@ -369,7 +373,8 @@ gulp.task('test:api-v3:integration', (done) => {
|
||||
});
|
||||
|
||||
gulp.task('test:api-v3:integration:watch', () => {
|
||||
gulp.watch(['website/src/controllers/api-v3/**/*', 'test/api/v3/integration/**/*', 'common/script/ops/*'], ['test:api-v3:integration']);
|
||||
gulp.watch(['website/src/controllers/api-v3/**/*', 'common/script/ops/*', 'website/src/libs/api-v3/*.js',
|
||||
'test/api/v3/integration/**/*'], ['test:api-v3:integration']);
|
||||
});
|
||||
|
||||
gulp.task('test:api-v3:integration:separate-server', (done) => {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments : amazon #subscribeCancel', () => {
|
||||
let endpoint = '/amazon/subscribe/cancel';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies subscription', async () => {
|
||||
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingAuthParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments : paypal #checkout', () => {
|
||||
let endpoint = '/paypal/checkout';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies subscription', async () => {
|
||||
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingAuthParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments : paypal #checkoutSuccess', () => {
|
||||
let endpoint = '/paypal/checkout/success';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies subscription', async () => {
|
||||
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('invalidCredentials'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments : paypal #subscribe', () => {
|
||||
let endpoint = '/paypal/subscribe';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingAuthParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments : paypal #subscribeCancel', () => {
|
||||
let endpoint = '/paypal/subscribe/cancel';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingAuthParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments : paypal #subscribeSuccess', () => {
|
||||
let endpoint = '/paypal/subscribe/success';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('invalidCredentials'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments - stripe - #subscribeCancel', () => {
|
||||
let endpoint = '/stripe/subscribe/cancel';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingAuthParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments - amazon - #checkout', () => {
|
||||
let endpoint = '/amazon/checkout';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Missing req.body.orderReferenceId',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments - amazon - #createOrderReferenceId', () => {
|
||||
let endpoint = '/amazon/createOrderReferenceId';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies billingAgreementId', async (done) => {
|
||||
try {
|
||||
await user.post(endpoint);
|
||||
} catch (e) {
|
||||
// Parameter AWSAccessKeyId cannot be empty.
|
||||
expect(e.error).to.eql('BadRequest');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments - amazon - #subscribe', () => {
|
||||
let endpoint = '/amazon/subscribe';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies subscription code', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments : amazon', () => {
|
||||
let endpoint = '/amazon/verifyAccessToken';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies access token', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Missing req.body.access_token',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments - paypal - #ipn', () => {
|
||||
let endpoint = '/paypal/ipn';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
let result = await user.post(endpoint);
|
||||
expect(result).to.eql('OK');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments - stripe - #checkout', () => {
|
||||
let endpoint = '/stripe/checkout';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'Error',
|
||||
message: 'Invalid API Key provided: ****************************1111',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('payments - stripe - #subscribeEdit', () => {
|
||||
let endpoint = '/stripe/subscribe/edit';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
});
|
||||
72
test/api/v3/unit/libs/payments.test.js
Normal file
72
test/api/v3/unit/libs/payments.test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as sender from '../../../../../website/src/libs/api-v3/email';
|
||||
import * as api from '../../../../../website/src/libs/api-v3/payments';
|
||||
import { model as User } from '../../../../../website/src/models/user';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let fakeSend;
|
||||
let data;
|
||||
let user;
|
||||
|
||||
describe('#createSubscription', () => {
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
it('succeeds', async () => {
|
||||
data = { user, sub: { key: 'basic_3mo' } };
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#cancelSubscription', () => {
|
||||
beforeEach(() => {
|
||||
fakeSend = sinon.spy(sender, 'sendTxn');
|
||||
data = { user: new User() };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fakeSend.restore();
|
||||
});
|
||||
|
||||
it('plan.extraMonths is defined', () => {
|
||||
api.cancelSubscription(data);
|
||||
let terminated = data.user.purchased.plan.dateTerminated;
|
||||
data.user.purchased.plan.extraMonths = 2;
|
||||
api.cancelSubscription(data);
|
||||
let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days'));
|
||||
expect(difference - 60).to.be.lessThan(3); // the difference is approximately two months, +/- 2 days
|
||||
});
|
||||
|
||||
it('plan.extraMonth is a fraction', () => {
|
||||
api.cancelSubscription(data);
|
||||
let terminated = data.user.purchased.plan.dateTerminated;
|
||||
data.user.purchased.plan.extraMonths = 0.3;
|
||||
api.cancelSubscription(data);
|
||||
let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days'));
|
||||
expect(difference - 10).to.be.lessThan(3); // the difference should be 10 days.
|
||||
});
|
||||
|
||||
it('nextBill is defined', () => {
|
||||
api.cancelSubscription(data);
|
||||
let terminated = data.user.purchased.plan.dateTerminated;
|
||||
data.nextBill = moment().add({ days: 25 });
|
||||
api.cancelSubscription(data);
|
||||
let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days'));
|
||||
expect(difference - 5).to.be.lessThan(2); // the difference should be 5 days, +/- 1 day
|
||||
});
|
||||
|
||||
it('saves the canceled subscription for the user', () => {
|
||||
expect(data.user.purchased.plan.dateTerminated).to.not.exist;
|
||||
api.cancelSubscription(data);
|
||||
expect(data.user.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
|
||||
it('sends a text', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
sinon.assert.called(fakeSend);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,7 @@ function _requestMaker (user, method, additionalSets = {}) {
|
||||
let url = `http://localhost:${API_TEST_SERVER_PORT}`;
|
||||
|
||||
// do not prefix with api/apiVersion requests to top level routes like dataexport and payments
|
||||
if (route.indexOf('/export') === 0 || route.indexOf('/payments') === 0) {
|
||||
if (route.indexOf('/export') === 0 || route.indexOf('/paypal') === 0 || route.indexOf('/amazon') === 0 || route.indexOf('/stripe') === 0) {
|
||||
url += `${route}`;
|
||||
} else {
|
||||
url += `/api/${apiVersion}${route}`;
|
||||
|
||||
@@ -37,7 +37,7 @@ function($rootScope, User, $http, Content) {
|
||||
$http.post(url, res).success(function() {
|
||||
window.location.reload(true);
|
||||
}).error(function(res) {
|
||||
alert(res.err);
|
||||
alert(res.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -55,7 +55,7 @@ function($rootScope, User, $http, Content) {
|
||||
$http.post(url, data).success(function() {
|
||||
window.location.reload(true);
|
||||
}).error(function(data) {
|
||||
alert(data.err);
|
||||
alert(data.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -127,12 +127,12 @@ function($rootScope, User, $http, Content) {
|
||||
var url = '/amazon/createOrderReferenceId'
|
||||
$http.post(url, {
|
||||
billingAgreementId: Payments.amazonPayments.billingAgreementId
|
||||
}).success(function(data){
|
||||
}).success(function(res){
|
||||
Payments.amazonPayments.loggedIn = true;
|
||||
Payments.amazonPayments.orderReferenceId = data.orderReferenceId;
|
||||
Payments.amazonPayments.orderReferenceId = res.data.orderReferenceId;
|
||||
Payments.amazonPayments.initWidgets();
|
||||
}).error(function(res){
|
||||
alert(res.err);
|
||||
alert(res.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -146,7 +146,7 @@ function($rootScope, User, $http, Content) {
|
||||
|
||||
var url = '/amazon/verifyAccessToken'
|
||||
$http.post(url, response).error(function(res){
|
||||
alert(res.err);
|
||||
alert(res.message);
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -232,7 +232,7 @@ function($rootScope, User, $http, Content) {
|
||||
Payments.amazonPayments.reset();
|
||||
window.location.reload(true);
|
||||
}).error(function(res){
|
||||
alert(res.err);
|
||||
alert(res.message);
|
||||
Payments.amazonPayments.reset();
|
||||
});
|
||||
}else if(Payments.amazonPayments.type === 'subscription'){
|
||||
@@ -246,7 +246,7 @@ function($rootScope, User, $http, Content) {
|
||||
Payments.amazonPayments.reset();
|
||||
window.location.reload(true);
|
||||
}).error(function(res){
|
||||
alert(res.err);
|
||||
alert(res.message);
|
||||
Payments.amazonPayments.reset();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
var amazonPayments = require('amazon-payments');
|
||||
var mongoose = require('mongoose');
|
||||
var moment = require('moment');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var User = require('mongoose').model('User');
|
||||
var shared = require('../../../../common');
|
||||
var payments = require('./index');
|
||||
var cc = require('coupon-code');
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
|
||||
var amzPayment = amazonPayments.connect({
|
||||
environment: amazonPayments.Environment[isProd ? 'Production' : 'Sandbox'],
|
||||
sellerId: nconf.get('AMAZON_PAYMENTS:SELLER_ID'),
|
||||
mwsAccessKey: nconf.get('AMAZON_PAYMENTS:MWS_KEY'),
|
||||
mwsSecretKey: nconf.get('AMAZON_PAYMENTS:MWS_SECRET'),
|
||||
clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID')
|
||||
});
|
||||
|
||||
exports.verifyAccessToken = function(req, res, next){
|
||||
if(!req.body || !req.body['access_token']){
|
||||
return res.status(400).json({err: 'Access token not supplied.'});
|
||||
}
|
||||
|
||||
amzPayment.api.getTokenInfo(req.body['access_token'], function(err, tokenInfo){
|
||||
if(err) return res.status(400).json({err:err});
|
||||
|
||||
res.sendStatus(200);
|
||||
});
|
||||
};
|
||||
|
||||
exports.createOrderReferenceId = function(req, res, next){
|
||||
if(!req.body || !req.body.billingAgreementId){
|
||||
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
|
||||
}
|
||||
|
||||
amzPayment.offAmazonPayments.createOrderReferenceForId({
|
||||
Id: req.body.billingAgreementId,
|
||||
IdType: 'BillingAgreement',
|
||||
ConfirmNow: false
|
||||
}, function(err, response){
|
||||
if(err) return next(err);
|
||||
if(!response.OrderReferenceDetails || !response.OrderReferenceDetails.AmazonOrderReferenceId){
|
||||
return next(new Error('Missing attributes in Amazon response.'));
|
||||
}
|
||||
|
||||
res.json({
|
||||
orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.checkout = function(req, res, next){
|
||||
if(!req.body || !req.body.orderReferenceId){
|
||||
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
|
||||
}
|
||||
|
||||
var gift = req.body.gift;
|
||||
var user = res.locals.user;
|
||||
var orderReferenceId = req.body.orderReferenceId;
|
||||
var amount = 5;
|
||||
|
||||
if(gift){
|
||||
if(gift.type === 'gems'){
|
||||
amount = gift.gems.amount/4;
|
||||
}else if(gift.type === 'subscription'){
|
||||
amount = shared.content.subscriptionBlocks[gift.subscription.key].price;
|
||||
}
|
||||
}
|
||||
|
||||
async.series({
|
||||
setOrderReferenceDetails: function(cb){
|
||||
amzPayment.offAmazonPayments.setOrderReferenceDetails({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: amount
|
||||
},
|
||||
SellerNote: 'HabitRPG Payment',
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: shared.uuid(),
|
||||
StoreName: 'HabitRPG'
|
||||
}
|
||||
}
|
||||
}, cb);
|
||||
},
|
||||
|
||||
confirmOrderReference: function(cb){
|
||||
amzPayment.offAmazonPayments.confirmOrderReference({
|
||||
AmazonOrderReferenceId: orderReferenceId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
authorize: function(cb){
|
||||
amzPayment.offAmazonPayments.authorize({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: shared.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: amount
|
||||
},
|
||||
SellerAuthorizationNote: 'HabitRPG Payment',
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true
|
||||
}, function(err, res){
|
||||
if(err) return cb(err);
|
||||
|
||||
if(res.AuthorizationDetails.AuthorizationStatus.State === 'Declined'){
|
||||
return cb(new Error('The payment was not successfull.'));
|
||||
}
|
||||
|
||||
return cb();
|
||||
});
|
||||
},
|
||||
|
||||
closeOrderReference: function(cb){
|
||||
amzPayment.offAmazonPayments.closeOrderReference({
|
||||
AmazonOrderReferenceId: orderReferenceId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
executePayment: function(cb){
|
||||
async.waterfall([
|
||||
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); },
|
||||
function(member, cb2){
|
||||
var data = {user:user, paymentMethod:'Amazon Payments'};
|
||||
var method = 'buyGems';
|
||||
|
||||
if (gift){
|
||||
if (gift.type == 'subscription') method = 'createSubscription';
|
||||
gift.member = member;
|
||||
data.gift = gift;
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
|
||||
payments[method](data, cb2);
|
||||
}
|
||||
], cb);
|
||||
}
|
||||
}, function(err, results){
|
||||
if(err) return next(err);
|
||||
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
exports.subscribe = function(req, res, next){
|
||||
if(!req.body || !req.body['billingAgreementId']){
|
||||
return res.status(400).json({err: 'Billing Agreement Id not supplied.'});
|
||||
}
|
||||
|
||||
var billingAgreementId = req.body.billingAgreementId;
|
||||
var sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
|
||||
var coupon = req.body.coupon;
|
||||
var user = res.locals.user;
|
||||
|
||||
if(!sub){
|
||||
return res.status(400).json({err: 'Subscription plan not found.'});
|
||||
}
|
||||
|
||||
async.series({
|
||||
applyDiscount: function(cb){
|
||||
if (!sub.discount) return cb();
|
||||
if (!coupon) return cb(new Error('Please provide a coupon code for this plan.'));
|
||||
mongoose.model('Coupon').findOne({_id:cc.validate(coupon), event:sub.key}, function(err, coupon){
|
||||
if(err) return cb(err);
|
||||
if(!coupon) return cb(new Error('Coupon code not found.'));
|
||||
cb();
|
||||
});
|
||||
},
|
||||
|
||||
setBillingAgreementDetails: function(cb){
|
||||
amzPayment.offAmazonPayments.setBillingAgreementDetails({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: 'HabitRPG Subscription',
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: shared.uuid(),
|
||||
StoreName: 'HabitRPG',
|
||||
CustomInformation: 'HabitRPG Subscription'
|
||||
}
|
||||
}
|
||||
}, cb);
|
||||
},
|
||||
|
||||
confirmBillingAgreement: function(cb){
|
||||
amzPayment.offAmazonPayments.confirmBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
authorizeOnBillingAgreeement: function(cb){
|
||||
amzPayment.offAmazonPayments.authorizeOnBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: shared.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: sub.price
|
||||
},
|
||||
SellerAuthorizationNote: 'HabitRPG Subscription Payment',
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: 'HabitRPG Subscription Payment',
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: shared.uuid(),
|
||||
StoreName: 'HabitRPG'
|
||||
}
|
||||
}, function(err, res){
|
||||
if(err) return cb(err);
|
||||
|
||||
if(res.AuthorizationDetails.AuthorizationStatus.State === 'Declined'){
|
||||
return cb(new Error('The payment was not successfull.'));
|
||||
}
|
||||
|
||||
return cb();
|
||||
});
|
||||
},
|
||||
|
||||
createSubscription: function(cb){
|
||||
payments.createSubscription({
|
||||
user: user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: 'Amazon Payments',
|
||||
sub: sub
|
||||
}, cb);
|
||||
}
|
||||
}, function(err, results){
|
||||
if(err) return next(err);
|
||||
|
||||
res.sendStatus(200);
|
||||
});
|
||||
};
|
||||
|
||||
exports.subscribeCancel = function(req, res, next){
|
||||
var user = res.locals.user;
|
||||
if (!user.purchased.plan.customerId)
|
||||
return res.status(401).json({err: 'User does not have a plan subscription'});
|
||||
|
||||
var billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
async.series({
|
||||
closeBillingAgreement: function(cb){
|
||||
amzPayment.offAmazonPayments.closeBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
cancelSubscription: function(cb){
|
||||
var data = {
|
||||
user: user,
|
||||
// Date of next bill
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({days: 30}),
|
||||
paymentMethod: 'Amazon Payments'
|
||||
};
|
||||
|
||||
payments.cancelSubscription(data, cb);
|
||||
}
|
||||
}, function(err, results){
|
||||
if (err) return next(err); // don't json this, let toString() handle errors
|
||||
|
||||
if(req.query.noRedirect){
|
||||
res.sendStatus(200);
|
||||
}else{
|
||||
res.redirect('/');
|
||||
}
|
||||
|
||||
user = null;
|
||||
});
|
||||
};
|
||||
@@ -1,155 +0,0 @@
|
||||
var iap = require('in-app-purchase');
|
||||
var async = require('async');
|
||||
var payments = require('./index');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var inAppPurchase = require('in-app-purchase');
|
||||
inAppPurchase.config({
|
||||
// this is the path to the directory containing iap-sanbox/iap-live files
|
||||
googlePublicKeyPath: nconf.get('IAP_GOOGLE_KEYDIR')
|
||||
});
|
||||
|
||||
// Validation ERROR Codes
|
||||
var INVALID_PAYLOAD = 6778001;
|
||||
var CONNECTION_FAILED = 6778002;
|
||||
var PURCHASE_EXPIRED = 6778003;
|
||||
|
||||
exports.androidVerify = function(req, res, next) {
|
||||
var iapBody = req.body;
|
||||
var user = res.locals.user;
|
||||
|
||||
iap.setup(function (error) {
|
||||
if (error) {
|
||||
var 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"
|
||||
}
|
||||
*/
|
||||
var testObj = {
|
||||
data: iapBody.transaction.receipt,
|
||||
signature: iapBody.transaction.signature
|
||||
};
|
||||
|
||||
// iap is ready
|
||||
iap.validate(iap.GOOGLE, testObj, function (err, googleRes) {
|
||||
if (err) {
|
||||
var resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: err.toString()
|
||||
}
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
if (iap.isValidated(googleRes)) {
|
||||
var resObj = {
|
||||
ok: true,
|
||||
data: googleRes
|
||||
};
|
||||
|
||||
payments.buyGems({user:user, paymentMethod:'IAP GooglePlay', amount: 5.25});
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.iosVerify = function(req, res, next) {
|
||||
var iapBody = req.body;
|
||||
var user = res.locals.user;
|
||||
|
||||
iap.setup(function (error) {
|
||||
if (error) {
|
||||
var resObj = {
|
||||
ok: false,
|
||||
data: 'IAP Error'
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
|
||||
}
|
||||
|
||||
//iap is ready
|
||||
iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) {
|
||||
if (err) {
|
||||
var resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: err.toString()
|
||||
}
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
if (iap.isValidated(appleRes)) {
|
||||
var purchaseDataList = iap.getPurchaseData(appleRes);
|
||||
if (purchaseDataList.length > 0) {
|
||||
var correctReceipt = true;
|
||||
for (var index in purchaseDataList) {
|
||||
switch (purchaseDataList[index].productId) {
|
||||
case 'com.habitrpg.ios.Habitica.4gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 1});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.8gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 2});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.20gems':
|
||||
case 'com.habitrpg.ios.Habitica.21gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 5.25});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.42gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 10.5});
|
||||
break;
|
||||
default:
|
||||
correctReceipt = false;
|
||||
}
|
||||
}
|
||||
if (correctReceipt) {
|
||||
var resObj = {
|
||||
ok: true,
|
||||
data: appleRes
|
||||
};
|
||||
// yay good!
|
||||
return res.json(resObj);
|
||||
}
|
||||
}
|
||||
//wrong receipt content
|
||||
var resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: 'Incorrect receipt content'
|
||||
}
|
||||
};
|
||||
return res.json(resObj);
|
||||
}
|
||||
//invalid receipt
|
||||
var resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: 'Invalid receipt'
|
||||
}
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,207 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var shared = require('../../../../common');
|
||||
var nconf = require('nconf');
|
||||
var utils = require('./../../libs/api-v2/utils');
|
||||
var moment = require('moment');
|
||||
var isProduction = nconf.get("NODE_ENV") === "production";
|
||||
var stripe = require('./stripe');
|
||||
var paypal = require('./paypal');
|
||||
var amazon = require('./amazon');
|
||||
var members = require('../api-v2/members')
|
||||
var async = require('async');
|
||||
var iap = require('./iap');
|
||||
var mongoose= require('mongoose');
|
||||
var cc = require('coupon-code');
|
||||
var pushNotify = require('./../api-v2/pushNotifications');
|
||||
|
||||
function revealMysteryItems(user) {
|
||||
_.each(shared.content.gear.flat, function(item) {
|
||||
if (
|
||||
item.klass === 'mystery' &&
|
||||
moment().isAfter(shared.content.mystery[item.mystery].start) &&
|
||||
moment().isBefore(shared.content.mystery[item.mystery].end) &&
|
||||
!user.items.gear.owned[item.key] &&
|
||||
!~user.purchased.plan.mysteryItems.indexOf(item.key)
|
||||
) {
|
||||
user.purchased.plan.mysteryItems.push(item.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.createSubscription = function(data, cb) {
|
||||
var recipient = data.gift ? data.gift.member : data.user;
|
||||
//if (!recipient.purchased.plan) recipient.purchased.plan = {}; // TODO double-check, this should never be the case
|
||||
var p = recipient.purchased.plan;
|
||||
var block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
|
||||
var months = +block.months;
|
||||
|
||||
if (data.gift) {
|
||||
if (p.customerId && !p.dateTerminated) { // User has active plan
|
||||
p.extraMonths += months;
|
||||
} else {
|
||||
p.dateTerminated = moment(p.dateTerminated).add({months: months}).toDate();
|
||||
if (!p.dateUpdated) p.dateUpdated = new Date();
|
||||
}
|
||||
if (!p.customerId) p.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId
|
||||
} else {
|
||||
_(p).merge({ // override with these values
|
||||
planId: block.key,
|
||||
customerId: data.customerId,
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: data.paymentMethod,
|
||||
extraMonths: +p.extraMonths
|
||||
+ +(p.dateTerminated ? moment(p.dateTerminated).diff(new Date(),'months',true) : 0),
|
||||
dateTerminated: null,
|
||||
// Specify a lastBillingDate just for Amazon Payments
|
||||
// Resetted every time the subscription restarts
|
||||
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? new Date() : undefined
|
||||
}).defaults({ // allow non-override if a plan was previously used
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: []
|
||||
}).value();
|
||||
}
|
||||
|
||||
// Block sub perks
|
||||
var perks = Math.floor(months/3);
|
||||
if (perks) {
|
||||
p.consecutive.offset += months;
|
||||
p.consecutive.gemCapExtra += perks*5;
|
||||
if (p.consecutive.gemCapExtra > 25) p.consecutive.gemCapExtra = 25;
|
||||
p.consecutive.trinkets += perks;
|
||||
}
|
||||
revealMysteryItems(recipient);
|
||||
if(isProduction) {
|
||||
if (!data.gift) utils.txnEmail(data.user, 'subscription-begins');
|
||||
|
||||
var analyticsData = {
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: data.paymentMethod.toLowerCase() + '-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: !!data.gift, // coerced into a boolean
|
||||
purchaseValue: block.price
|
||||
}
|
||||
utils.analytics.trackPurchase(analyticsData);
|
||||
}
|
||||
data.user.purchased.txnCount++;
|
||||
if (data.gift){
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
|
||||
var byUserName = utils.getUserInfo(data.user, ['name']).name;
|
||||
|
||||
if(data.gift.member.preferences.emailNotifications.giftedSubscription !== false){
|
||||
utils.txnEmail(data.gift.member, 'gifted-subscription', [
|
||||
{name: 'GIFTER', content: byUserName},
|
||||
{name: 'X_MONTHS_SUBSCRIPTION', content: months}
|
||||
]);
|
||||
}
|
||||
|
||||
if (data.gift.member._id != data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedSubscription'), months + " months - by "+ byUserName);
|
||||
}
|
||||
}
|
||||
async.parallel([
|
||||
function(cb2){data.user.save(cb2)},
|
||||
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
|
||||
], cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets their subscription to be cancelled later
|
||||
*/
|
||||
exports.cancelSubscription = function(data, cb) {
|
||||
var p = data.user.purchased.plan,
|
||||
now = moment(),
|
||||
remaining = data.nextBill ? moment(data.nextBill).diff(new Date, 'days') : 30;
|
||||
|
||||
p.dateTerminated =
|
||||
moment( now.format('MM') + '/' + moment(p.dateUpdated).format('DD') + '/' + now.format('YYYY') )
|
||||
.add({days: remaining}) // end their subscription 1mo from their last payment
|
||||
.add({months: Math.ceil(p.extraMonths)})// plus any extra time (carry-over, gifted subscription, etc) they have. TODO: moment can't add months in fractions...
|
||||
.toDate();
|
||||
p.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
|
||||
|
||||
data.user.save(cb);
|
||||
utils.txnEmail(data.user, 'cancel-subscription');
|
||||
var analyticsData = {
|
||||
uuid: data.user._id,
|
||||
gaCategory: 'commerce',
|
||||
gaLabel: data.paymentMethod,
|
||||
paymentMethod: data.paymentMethod
|
||||
}
|
||||
utils.analytics.track('unsubscribe', analyticsData);
|
||||
}
|
||||
|
||||
exports.buyGems = function(data, cb) {
|
||||
var amt = data.amount || 5;
|
||||
amt = data.gift ? data.gift.gems.amount/4 : amt;
|
||||
(data.gift ? data.gift.member : data.user).balance += amt;
|
||||
data.user.purchased.txnCount++;
|
||||
if(isProduction) {
|
||||
if (!data.gift) utils.txnEmail(data.user, 'donation');
|
||||
|
||||
var analyticsData = {
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Gems',
|
||||
sku: data.paymentMethod.toLowerCase() + '-checkout',
|
||||
purchaseType: 'checkout',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: !!data.gift, // coerced into a boolean
|
||||
purchaseValue: amt
|
||||
}
|
||||
utils.analytics.trackPurchase(analyticsData);
|
||||
}
|
||||
|
||||
if (data.gift){
|
||||
var byUsername = utils.getUserInfo(data.user, ['name']).name;
|
||||
var gemAmount = data.gift.gems.amount || 20;
|
||||
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
if(data.gift.member.preferences.emailNotifications.giftedGems !== false){
|
||||
utils.txnEmail(data.gift.member, 'gifted-gems', [
|
||||
{name: 'GIFTER', content: byUsername},
|
||||
{name: 'X_GEMS_GIFTED', content: gemAmount}
|
||||
]);
|
||||
}
|
||||
|
||||
if (data.gift.member._id != data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), gemAmount + ' Gems - by '+byUsername);
|
||||
}
|
||||
}
|
||||
async.parallel([
|
||||
function(cb2){data.user.save(cb2)},
|
||||
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
|
||||
], cb);
|
||||
}
|
||||
|
||||
exports.validCoupon = function(req, res, next){
|
||||
mongoose.model('Coupon').findOne({_id:cc.validate(req.params.code), event:'google_6mo'}, function(err, coupon){
|
||||
if (err) return next(err);
|
||||
if (!coupon) return res.status(401).json({err:"Invalid coupon code"});
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
}
|
||||
|
||||
exports.stripeCheckout = stripe.checkout;
|
||||
exports.stripeSubscribeCancel = stripe.subscribeCancel;
|
||||
exports.stripeSubscribeEdit = stripe.subscribeEdit;
|
||||
|
||||
exports.paypalSubscribe = paypal.createBillingAgreement;
|
||||
exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
|
||||
exports.paypalSubscribeCancel = paypal.cancelSubscription;
|
||||
exports.paypalCheckout = paypal.createPayment;
|
||||
exports.paypalCheckoutSuccess = paypal.executePayment;
|
||||
exports.paypalIPN = paypal.ipn;
|
||||
|
||||
exports.amazonVerifyAccessToken = amazon.verifyAccessToken;
|
||||
exports.amazonCreateOrderReferenceId = amazon.createOrderReferenceId;
|
||||
exports.amazonCheckout = amazon.checkout;
|
||||
exports.amazonSubscribe = amazon.subscribe;
|
||||
exports.amazonSubscribeCancel = amazon.subscribeCancel;
|
||||
|
||||
exports.iapAndroidVerify = iap.androidVerify;
|
||||
exports.iapIosVerify = iap.iosVerify;
|
||||
@@ -1,216 +0,0 @@
|
||||
var nconf = require('nconf');
|
||||
var moment = require('moment');
|
||||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var url = require('url');
|
||||
var User = require('mongoose').model('User');
|
||||
var payments = require('./index');
|
||||
var logger = require('../../libs/api-v2/logging');
|
||||
var ipn = require('paypal-ipn');
|
||||
var paypal = require('paypal-rest-sdk');
|
||||
var shared = require('../../../../common');
|
||||
var mongoose = require('mongoose');
|
||||
var cc = require('coupon-code');
|
||||
|
||||
// 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
|
||||
_.each(shared.content.subscriptionBlocks, function(block){
|
||||
block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.key);
|
||||
});
|
||||
|
||||
paypal.configure({
|
||||
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
|
||||
'client_id': nconf.get("PAYPAL:client_id"),
|
||||
'client_secret': nconf.get("PAYPAL:client_secret")
|
||||
});
|
||||
|
||||
var parseErr = function(res, err){
|
||||
//var error = err.response ? err.response.message || err.response.details[0].issue : err;
|
||||
var error = JSON.stringify(err);
|
||||
return res.status(400).json({err:error});
|
||||
}
|
||||
|
||||
exports.createBillingAgreement = function(req,res,next){
|
||||
var sub = shared.content.subscriptionBlocks[req.query.sub];
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
if (!sub.discount) return cb(null, null);
|
||||
if (!req.query.coupon) return cb('Please provide a coupon code for this plan.');
|
||||
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb);
|
||||
},
|
||||
function(coupon, cb){
|
||||
if (sub.discount && !coupon) return cb('Invalid coupon code.');
|
||||
var billingPlanTitle = "HabitRPG Subscription" + ' ($'+sub.price+' every '+sub.months+' months, recurring)';
|
||||
var billingAgreementAttributes = {
|
||||
"name": billingPlanTitle,
|
||||
"description": billingPlanTitle,
|
||||
"start_date": moment().add({minutes:5}).format(),
|
||||
"plan": {
|
||||
"id": sub.paypalKey
|
||||
},
|
||||
"payer": {
|
||||
"payment_method": "paypal"
|
||||
}
|
||||
};
|
||||
paypal.billingAgreement.create(billingAgreementAttributes, cb);
|
||||
}
|
||||
], function(err, billingAgreement){
|
||||
if (err) return parseErr(res, err);
|
||||
// For approving subscription via Paypal, first redirect user to: approval_url
|
||||
req.session.paypalBlock = req.query.sub;
|
||||
var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href;
|
||||
res.redirect(approval_url);
|
||||
});
|
||||
}
|
||||
|
||||
exports.executeBillingAgreement = function(req,res,next){
|
||||
var block = shared.content.subscriptionBlocks[req.session.paypalBlock];
|
||||
delete req.session.paypalBlock;
|
||||
async.auto({
|
||||
exec: function (cb) {
|
||||
paypal.billingAgreement.execute(req.query.token, {}, cb);
|
||||
},
|
||||
get_user: function (cb) {
|
||||
User.findById(req.session.userId, cb);
|
||||
},
|
||||
create_sub: ['exec', 'get_user', function (cb, results) {
|
||||
payments.createSubscription({
|
||||
user: results.get_user,
|
||||
customerId: results.exec.id,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block
|
||||
}, cb);
|
||||
}]
|
||||
},function(err){
|
||||
if (err) return parseErr(res, err);
|
||||
res.redirect('/');
|
||||
})
|
||||
}
|
||||
|
||||
exports.createPayment = function(req, res) {
|
||||
// if we're gifting to a user, put it in session for the `execute()`
|
||||
req.session.gift = req.query.gift || undefined;
|
||||
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||
var price = !gift ? 5.00
|
||||
: gift.type=='gems' ? Number(gift.gems.amount/4).toFixed(2)
|
||||
: Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
|
||||
var description = !gift ? "HabitRPG Gems"
|
||||
: gift.type=='gems' ? "HabitRPG Gems (Gift)"
|
||||
: shared.content.subscriptionBlocks[gift.subscription.key].months + "mo. HabitRPG Subscription (Gift)";
|
||||
var create_payment = {
|
||||
"intent": "sale",
|
||||
"payer": {
|
||||
"payment_method": "paypal"
|
||||
},
|
||||
"redirect_urls": {
|
||||
"return_url": nconf.get('BASE_URL') + '/paypal/checkout/success',
|
||||
"cancel_url": nconf.get('BASE_URL')
|
||||
},
|
||||
"transactions": [{
|
||||
"item_list": {
|
||||
"items": [{
|
||||
"name": description,
|
||||
//"sku": "1",
|
||||
"price": price,
|
||||
"currency": "USD",
|
||||
"quantity": 1
|
||||
}]
|
||||
},
|
||||
"amount": {
|
||||
"currency": "USD",
|
||||
"total": price
|
||||
},
|
||||
"description": description
|
||||
}]
|
||||
};
|
||||
paypal.payment.create(create_payment, function (err, payment) {
|
||||
if (err) return parseErr(res, err);
|
||||
var link = _.find(payment.links, {rel: 'approval_url'}).href;
|
||||
res.redirect(link);
|
||||
});
|
||||
}
|
||||
|
||||
exports.executePayment = function(req, res) {
|
||||
var paymentId = req.query.paymentId,
|
||||
PayerID = req.query.PayerID,
|
||||
gift = req.session.gift ? JSON.parse(req.session.gift) : undefined;
|
||||
delete req.session.gift;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
paypal.payment.execute(paymentId, {payer_id: PayerID}, cb);
|
||||
},
|
||||
function(payment, cb){
|
||||
async.parallel([
|
||||
function(cb2){ User.findById(req.session.userId, cb2); },
|
||||
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); }
|
||||
], cb);
|
||||
},
|
||||
function(results, cb){
|
||||
if (_.isEmpty(results[0])) return cb("User not found when completing paypal transaction");
|
||||
var data = {user:results[0], customerId:PayerID, paymentMethod:'Paypal', gift:gift}
|
||||
var method = 'buyGems';
|
||||
if (gift) {
|
||||
gift.member = results[1];
|
||||
if (gift.type=='subscription') method = 'createSubscription';
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
payments[method](data, cb);
|
||||
}
|
||||
],function(err){
|
||||
if (err) return parseErr(res, err);
|
||||
res.redirect('/');
|
||||
})
|
||||
}
|
||||
|
||||
exports.cancelSubscription = function(req, res, next){
|
||||
var user = res.locals.user;
|
||||
if (!user.purchased.plan.customerId)
|
||||
return res.status(401).json({err: "User does not have a plan subscription"});
|
||||
async.auto({
|
||||
get_cus: function(cb){
|
||||
paypal.billingAgreement.get(user.purchased.plan.customerId, cb);
|
||||
},
|
||||
verify_cus: ['get_cus', function(cb, results){
|
||||
var hasntBilledYet = results.get_cus.agreement_details.cycles_completed == "0";
|
||||
if (hasntBilledYet)
|
||||
return cb("The plan hasn't activated yet (due to a PayPal bug). It will begin "+results.get_cus.agreement_details.next_billing_date+", after which you can cancel to retain your full benefits");
|
||||
cb();
|
||||
}],
|
||||
del_cus: ['verify_cus', function(cb, results){
|
||||
paypal.billingAgreement.cancel(user.purchased.plan.customerId, {note: "Canceling the subscription"}, cb);
|
||||
}],
|
||||
cancel_sub: ['get_cus', 'verify_cus', function(cb, results){
|
||||
var data = {user: user, paymentMethod: 'Paypal', nextBill: results.get_cus.agreement_details.next_billing_date};
|
||||
payments.cancelSubscription(data, cb)
|
||||
}]
|
||||
}, function(err){
|
||||
if (err) return parseErr(res, err);
|
||||
res.redirect('/');
|
||||
user = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* General IPN handler. We catch cancelled HabitRPG subscriptions for users who manually cancel their
|
||||
* recurring paypal payments in their paypal dashboard. Remove this when we can move to webhooks or some other solution
|
||||
*/
|
||||
exports.ipn = function(req, res, next) {
|
||||
console.log('IPN Called');
|
||||
res.sendStatus(200); // Must respond to PayPal IPN request with an empty 200 first
|
||||
ipn.verify(req.body, function(err, msg) {
|
||||
if (err) return logger.error(msg);
|
||||
switch (req.body.txn_type) {
|
||||
// TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead...
|
||||
case 'recurring_payment_profile_cancel':
|
||||
case 'subscr_cancel':
|
||||
User.findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){
|
||||
if (err) return logger.error(err);
|
||||
if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel)
|
||||
payments.cancelSubscription({user:user, paymentMethod: 'Paypal'});
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
var nconf = require('nconf');
|
||||
var stripe = require('stripe')(nconf.get('STRIPE_API_KEY'));
|
||||
var async = require('async');
|
||||
var payments = require('./index');
|
||||
var User = require('mongoose').model('User');
|
||||
var shared = require('../../../../common');
|
||||
var mongoose = require('mongoose');
|
||||
var cc = require('coupon-code');
|
||||
|
||||
/*
|
||||
Setup Stripe response when posting payment
|
||||
*/
|
||||
exports.checkout = function(req, res, next) {
|
||||
var token = req.body.id;
|
||||
var user = res.locals.user;
|
||||
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||
var sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
if (sub) {
|
||||
async.waterfall([
|
||||
function(cb2){
|
||||
if (!sub.discount) return cb2(null, null);
|
||||
if (!req.query.coupon) return cb2('Please provide a coupon code for this plan.');
|
||||
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb2);
|
||||
},
|
||||
function(coupon, cb2){
|
||||
if (sub.discount && !coupon) return cb2('Invalid coupon code.');
|
||||
var customer = {
|
||||
email: req.body.email,
|
||||
metadata: {uuid: user._id},
|
||||
card: token,
|
||||
plan: sub.key
|
||||
};
|
||||
stripe.customers.create(customer, cb2);
|
||||
}
|
||||
], cb);
|
||||
} else {
|
||||
stripe.charges.create({
|
||||
amount: !gift ? '500' //"500" = $5
|
||||
: gift.type=='subscription' ? ''+shared.content.subscriptionBlocks[gift.subscription.key].price*100
|
||||
: ''+gift.gems.amount/4*100,
|
||||
currency: 'usd',
|
||||
card: token
|
||||
}, cb);
|
||||
}
|
||||
},
|
||||
function(response, cb) {
|
||||
if (sub) return payments.createSubscription({user:user, customerId:response.id, paymentMethod:'Stripe', sub:sub}, cb);
|
||||
async.waterfall([
|
||||
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); },
|
||||
function(member, cb2){
|
||||
var data = {user:user, customerId:response.id, paymentMethod:'Stripe', gift:gift};
|
||||
var method = 'buyGems';
|
||||
if (gift) {
|
||||
gift.member = member;
|
||||
if (gift.type=='subscription') method = 'createSubscription';
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
payments[method](data, cb2);
|
||||
}
|
||||
], cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
|
||||
res.sendStatus(200);
|
||||
user = token = null;
|
||||
});
|
||||
};
|
||||
|
||||
exports.subscribeCancel = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if (!user.purchased.plan.customerId)
|
||||
return res.status(401).json({err: 'User does not have a plan subscription'});
|
||||
|
||||
async.auto({
|
||||
get_cus: function(cb){
|
||||
stripe.customers.retrieve(user.purchased.plan.customerId, cb);
|
||||
},
|
||||
del_cus: ['get_cus', function(cb, results){
|
||||
stripe.customers.del(user.purchased.plan.customerId, cb);
|
||||
}],
|
||||
cancel_sub: ['get_cus', function(cb, results) {
|
||||
var data = {
|
||||
user: user,
|
||||
nextBill: results.get_cus.subscription.current_period_end*1000, // timestamp is in seconds
|
||||
paymentMethod: 'Stripe'
|
||||
};
|
||||
payments.cancelSubscription(data, cb);
|
||||
}]
|
||||
}, function(err, results){
|
||||
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
|
||||
res.redirect('/');
|
||||
user = null;
|
||||
});
|
||||
};
|
||||
|
||||
exports.subscribeEdit = function(req, res, next) {
|
||||
var token = req.body.id;
|
||||
var user = res.locals.user;
|
||||
var user_id = user.purchased.plan.customerId;
|
||||
var sub_id;
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
stripe.customers.listSubscriptions(user_id, cb);
|
||||
},
|
||||
function(response, cb) {
|
||||
sub_id = response.data[0].id;
|
||||
console.warn(sub_id);
|
||||
console.warn([user_id, sub_id, { card: token }]);
|
||||
stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb);
|
||||
},
|
||||
function(response, cb) {
|
||||
user.save(cb);
|
||||
}
|
||||
], function(err, saved){
|
||||
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
|
||||
res.sendStatus(200);
|
||||
token = user = user_id = sub_id;
|
||||
});
|
||||
};
|
||||
256
website/src/controllers/top-level/payments/amazon.js
Normal file
256
website/src/controllers/top-level/payments/amazon.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../libs/api-v3/errors';
|
||||
import amzLib from '../../../libs/api-v3/amazonPayments';
|
||||
import {
|
||||
authWithHeaders,
|
||||
authWithUrl,
|
||||
} from '../../../middlewares/api-v3/auth';
|
||||
import shared from '../../../../../common';
|
||||
import payments from '../../../libs/api-v3/payments';
|
||||
import moment from 'moment';
|
||||
import { model as Coupon } from '../../../models/coupon';
|
||||
import { model as User } from '../../../models/user';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /amazon/verifyAccessToken Amazon Payments: verify access token
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonVerifyAccessToken
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiSuccess {Object} data Empty object
|
||||
**/
|
||||
api.verifyAccessToken = {
|
||||
method: 'POST',
|
||||
url: '/amazon/verifyAccessToken',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let accessToken = req.body.access_token;
|
||||
|
||||
if (!accessToken) throw new BadRequest('Missing req.body.access_token');
|
||||
|
||||
await amzLib.getTokenInfo(accessToken);
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /amazon/createOrderReferenceId Amazon Payments: create order reference id
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonCreateOrderReferenceId
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiSuccess {string} data.orderReferenceId The order reference id.
|
||||
**/
|
||||
api.createOrderReferenceId = {
|
||||
method: 'POST',
|
||||
url: '/amazon/createOrderReferenceId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let billingAgreementId = req.body.billingAgreementId;
|
||||
|
||||
if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId');
|
||||
|
||||
let response = await amzLib.createOrderReferenceId({
|
||||
Id: billingAgreementId,
|
||||
IdType: 'BillingAgreement',
|
||||
ConfirmNow: false,
|
||||
});
|
||||
|
||||
res.respond(200, {
|
||||
orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /amazon/checkout Amazon Payments: checkout
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonCheckout
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiSuccess {object} data Empty object
|
||||
**/
|
||||
api.checkout = {
|
||||
method: 'POST',
|
||||
url: '/amazon/checkout',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let gift = req.body.gift;
|
||||
let user = res.locals.user;
|
||||
let orderReferenceId = req.body.orderReferenceId;
|
||||
let amount = 5;
|
||||
|
||||
if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId');
|
||||
|
||||
if (gift) {
|
||||
if (gift.type === 'gems') {
|
||||
amount = gift.gems.amount / 4;
|
||||
} else if (gift.type === 'subscription') {
|
||||
amount = shared.content.subscriptionBlocks[gift.subscription.key].price;
|
||||
}
|
||||
}
|
||||
|
||||
await amzLib.setOrderReferenceDetails({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: amount,
|
||||
},
|
||||
SellerNote: 'HabitRPG Payment',
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: shared.uuid(),
|
||||
StoreName: 'HabitRPG',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await amzLib.confirmOrderReference({ AmazonOrderReferenceId: orderReferenceId });
|
||||
|
||||
await amzLib.authorize({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: shared.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: 'HabitRPG Payment',
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
});
|
||||
|
||||
await amzLib.closeOrderReference({ AmazonOrderReferenceId: orderReferenceId });
|
||||
|
||||
// execute payment
|
||||
let method = 'buyGems';
|
||||
let data = { user, paymentMethod: 'Amazon Payments' };
|
||||
|
||||
if (gift) {
|
||||
if (gift.type === 'subscription') method = 'createSubscription';
|
||||
gift.member = await User.findById(gift ? gift.uuid : undefined);
|
||||
data.gift = gift;
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
|
||||
await payments[method](data);
|
||||
|
||||
res.respond(200);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /amazon/subscribe Amazon Payments: subscribe
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonSubscribe
|
||||
* @apiGroup Payments
|
||||
*
|
||||
* @apiSuccess {object} data Empty object
|
||||
**/
|
||||
api.subscribe = {
|
||||
method: 'POST',
|
||||
url: '/amazon/subscribe',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let billingAgreementId = req.body.billingAgreementId;
|
||||
let sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
|
||||
let coupon = req.body.coupon;
|
||||
let user = res.locals.user;
|
||||
|
||||
if (!sub) throw new BadRequest(res.t('missingSubscriptionCode'));
|
||||
if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId');
|
||||
|
||||
if (sub.discount) { // apply discount
|
||||
if (!coupon) throw new BadRequest(res.t('couponCodeRequired'));
|
||||
let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key});
|
||||
if (!result) throw new NotAuthorized(res.t('invalidCoupon'));
|
||||
}
|
||||
|
||||
await amzLib.setBillingAgreementDetails({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: 'HabitRPG Subscription',
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: shared.uuid(),
|
||||
StoreName: 'HabitRPG',
|
||||
CustomInformation: 'HabitRPG Subscription',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await amzLib.confirmBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
await amzLib.authorizeOnBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: shared.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: sub.price,
|
||||
},
|
||||
SellerAuthorizationNote: 'HabitRPG Subscription Payment',
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: 'HabitRPG Subscription Payment',
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: shared.uuid(),
|
||||
StoreName: 'HabitRPG',
|
||||
},
|
||||
});
|
||||
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: 'Amazon Payments',
|
||||
sub,
|
||||
});
|
||||
|
||||
res.respond(200);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {get} /amazon/subscribe/cancel Amazon Payments: subscribe cancel
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AmazonSubscribe
|
||||
* @apiGroup Payments
|
||||
**/
|
||||
api.subscribeCancel = {
|
||||
method: 'GET',
|
||||
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 NotAuthorized(res.t('missingSubscription'));
|
||||
|
||||
await amzLib.closeBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: 30 }),
|
||||
paymentMethod: 'Amazon Payments',
|
||||
});
|
||||
|
||||
if (req.query.noRedirect) {
|
||||
res.respond(200);
|
||||
} else {
|
||||
res.redirect('/');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
191
website/src/controllers/top-level/payments/iap.js
Normal file
191
website/src/controllers/top-level/payments/iap.js
Normal file
@@ -0,0 +1,191 @@
|
||||
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
|
||||
googlePublicKeyPath: nconf.get('IAP_GOOGLE_KEYDIR'),
|
||||
});
|
||||
|
||||
// Validation ERROR Codes
|
||||
const INVALID_PAYLOAD = 6778001;
|
||||
// const CONNECTION_FAILED = 6778002;
|
||||
// const PURCHASE_EXPIRED = 6778003;
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @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((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, (err, googleRes) => {
|
||||
if (err) {
|
||||
let resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: err.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
if (iap.isValidated(googleRes)) {
|
||||
let resObj = {
|
||||
ok: true,
|
||||
data: googleRes,
|
||||
};
|
||||
|
||||
payments.buyGems({
|
||||
user,
|
||||
paymentMethod: 'IAP GooglePlay',
|
||||
amount: 5.25,
|
||||
}).then(() => res.json(resObj));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
iap.setup(function iosSetupResult (error) {
|
||||
if (error) {
|
||||
let resObj = {
|
||||
ok: false,
|
||||
data: 'IAP Error',
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
// iap is ready
|
||||
iap.validate(iap.APPLE, iapBody.transaction.receipt, (err, appleRes) => {
|
||||
if (err) {
|
||||
let resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: err.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
if (iap.isValidated(appleRes)) {
|
||||
let purchaseDataList = iap.getPurchaseData(appleRes);
|
||||
if (purchaseDataList.length > 0) {
|
||||
let correctReceipt = true;
|
||||
|
||||
for (let index of purchaseDataList) {
|
||||
switch (purchaseDataList[index].productId) {
|
||||
case 'com.habitrpg.ios.Habitica.4gems':
|
||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.8gems':
|
||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.20gems':
|
||||
case 'com.habitrpg.ios.Habitica.21gems':
|
||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.42gems':
|
||||
payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5});
|
||||
break;
|
||||
default:
|
||||
correctReceipt = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (correctReceipt) {
|
||||
let resObj = {
|
||||
ok: true,
|
||||
data: appleRes,
|
||||
};
|
||||
|
||||
// yay good!
|
||||
return res.json(resObj);
|
||||
}
|
||||
}
|
||||
|
||||
// wrong receipt content
|
||||
let resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: 'Incorrect receipt content',
|
||||
},
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
// invalid receipt
|
||||
let resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: 'Invalid receipt',
|
||||
},
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
278
website/src/controllers/top-level/payments/paypal.js
Normal file
278
website/src/controllers/top-level/payments/paypal.js
Normal file
@@ -0,0 +1,278 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import payments from '../../../libs/api-v3/payments';
|
||||
import ipn from 'paypal-ipn';
|
||||
import paypal from 'paypal-rest-sdk';
|
||||
import shared from '../../../../../common';
|
||||
import cc from 'coupon-code';
|
||||
import Q from 'q';
|
||||
import { model as Coupon } from '../../../models/coupon';
|
||||
import { model as User } from '../../../models/user';
|
||||
import {
|
||||
authWithUrl,
|
||||
authWithSession,
|
||||
} from '../../../middlewares/api-v3/auth';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../libs/api-v3/errors';
|
||||
|
||||
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
|
||||
_.each(shared.content.subscriptionBlocks, (block) => {
|
||||
block.paypalKey = nconf.get(`PAYPAL:billing_plans:${block.key}`);
|
||||
});
|
||||
|
||||
paypal.configure({
|
||||
mode: nconf.get('PAYPAL:mode'), // sandbox or live
|
||||
client_id: nconf.get('PAYPAL:client_id'),
|
||||
client_secret: nconf.get('PAYPAL:client_secret'),
|
||||
});
|
||||
|
||||
// TODO better handling of errors
|
||||
const paypalPaymentCreate = Q.nbind(paypal.payment.create, paypal.payment);
|
||||
const paypalPaymentExecute = Q.nbind(paypal.payment.execute, paypal.payment);
|
||||
const paypalBillingAgreementCreate = Q.nbind(paypal.billingAgreement.create, paypal.billingAgreement);
|
||||
const paypalBillingAgreementExecute = Q.nbind(paypal.billingAgreement.execute, paypal.billingAgreement);
|
||||
const paypalBillingAgreementGet = Q.nbind(paypal.billingAgreement.get, paypal.billingAgreement);
|
||||
const paypalBillingAgreementCancel = Q.nbind(paypal.billingAgreement.cancel, paypal.billingAgreement);
|
||||
|
||||
const ipnVerifyAsync = Q.nbind(ipn.verify, ipn);
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {get} /paypal/checkout Paypal: checkout
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalCheckout
|
||||
* @apiGroup Payments
|
||||
**/
|
||||
api.checkout = {
|
||||
method: 'GET',
|
||||
url: '/paypal/checkout',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||
req.session.gift = req.query.gift;
|
||||
|
||||
let amount = 5.00;
|
||||
let description = 'HabitRPG gems';
|
||||
if (gift) {
|
||||
if (gift.type === 'gems') {
|
||||
amount = Number(gift.gems.amount / 4).toFixed(2);
|
||||
description = `${description} (Gift)`;
|
||||
} else {
|
||||
amount = Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
|
||||
description = 'mo. HabitRPG Subscription (Gift)';
|
||||
}
|
||||
}
|
||||
|
||||
let createPayment = {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
redirect_urls: {
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
},
|
||||
transactions: [{
|
||||
item_list: {
|
||||
items: [{
|
||||
name: description,
|
||||
// sku: 1,
|
||||
price: amount,
|
||||
currency: 'USD',
|
||||
quality: 1,
|
||||
}],
|
||||
},
|
||||
amount: {
|
||||
currency: 'USD',
|
||||
total: amount,
|
||||
},
|
||||
description,
|
||||
}],
|
||||
};
|
||||
|
||||
let result = await paypalPaymentCreate(createPayment);
|
||||
let link = _.find(result.links, { rel: 'approval_url' }).href;
|
||||
res.redirect(link);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {get} /paypal/checkout/success Paypal: checkout success
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalCheckoutSuccess
|
||||
* @apiGroup Payments
|
||||
**/
|
||||
api.checkoutSuccess = {
|
||||
method: 'GET',
|
||||
url: '/paypal/checkout/success',
|
||||
middlewares: [authWithSession],
|
||||
async handler (req, res) {
|
||||
let paymentId = req.query.paymentId;
|
||||
let customerId = req.query.payerID;
|
||||
|
||||
let method = 'buyGems';
|
||||
let data = {
|
||||
user: res.locals.user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
};
|
||||
|
||||
let gift = req.session.gift ? JSON.parse(req.session.gift) : undefined;
|
||||
delete req.session.gift;
|
||||
|
||||
if (gift) {
|
||||
gift.member = await User.findById(gift.uuid);
|
||||
if (gift.type === 'subscription') {
|
||||
method = 'createSubscription';
|
||||
}
|
||||
|
||||
data.paymentMethod = 'Gift';
|
||||
data.gift = gift;
|
||||
}
|
||||
|
||||
await paypalPaymentExecute(paymentId, { payer_id: customerId });
|
||||
await payments[method](data);
|
||||
res.redirect('/');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {get} /paypal/subscribe Paypal: subscribe
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalSubscribe
|
||||
* @apiGroup Payments
|
||||
**/
|
||||
api.subscribe = {
|
||||
method: 'GET',
|
||||
url: '/paypal/subscribe',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
let sub = shared.content.subscriptionBlocks[req.query.sub];
|
||||
|
||||
if (sub.discount) {
|
||||
if (!req.query.coupon) throw new BadRequest(res.t('couponCodeRequired'));
|
||||
let coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key});
|
||||
if (!coupon) throw new NotAuthorized(res.t('invalidCoupon'));
|
||||
}
|
||||
|
||||
let billingPlanTitle = `HabitRPG Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
let billingAgreementAttributes = {
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
};
|
||||
let billingAgreement = await paypalBillingAgreementCreate(billingAgreementAttributes);
|
||||
|
||||
req.session.paypalBlock = req.query.sub;
|
||||
let link = _.find(billingAgreement.links, { rel: 'approval_url' }).href;
|
||||
res.redirect(link);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {get} /paypal/subscribe/success Paypal: subscribe success
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalSubscribeSuccess
|
||||
* @apiGroup Payments
|
||||
**/
|
||||
api.subscribeSuccess = {
|
||||
method: 'GET',
|
||||
url: '/paypal/subscribe/success',
|
||||
middlewares: [authWithSession],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let block = shared.content.subscriptionBlocks[req.session.paypalBlock];
|
||||
delete req.session.paypalBlock;
|
||||
|
||||
let result = await paypalBillingAgreementExecute(req.query.token, {});
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: result.id,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
});
|
||||
|
||||
res.redirect('/');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {get} /paypal/subscribe/cancel Paypal: subscribe cancel
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalSubscribeCancel
|
||||
* @apiGroup Payments
|
||||
**/
|
||||
api.subscribeCancel = {
|
||||
method: 'GET',
|
||||
url: '/paypal/subscribe/cancel',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let customerId = user.purchased.plan.customerId;
|
||||
if (!user.purchased.plan.customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||
|
||||
let customer = await paypalBillingAgreementGet(customerId);
|
||||
|
||||
let nextBillingDate = customer.agreement_details.next_billing_date;
|
||||
if (customer.agreement_details.cycles_completed === '0') { // hasn't billed yet
|
||||
throw new BadRequest(res.t('planNotActive', { nextBillingDate }));
|
||||
}
|
||||
|
||||
await paypalBillingAgreementCancel(customerId, { note: res.t('cancelingSubscription') });
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
});
|
||||
|
||||
res.redirect('/');
|
||||
},
|
||||
};
|
||||
|
||||
// General IPN handler. We catch cancelled HabitRPG subscriptions for users who manually cancel their
|
||||
// recurring paypal payments in their paypal dashboard. TODO ? Remove this when we can move to webhooks or some other solution
|
||||
|
||||
/**
|
||||
* @apiIgnore Payments are considered part of the private API
|
||||
* @api {post} /paypal/ipn Paypal IPN
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName PaypalIpn
|
||||
* @apiGroup Payments
|
||||
**/
|
||||
api.ipn = {
|
||||
method: 'POST',
|
||||
url: '/paypal/ipn',
|
||||
async handler (req, res) {
|
||||
res.sendStatus(200);
|
||||
|
||||
await ipnVerifyAsync(req.body);
|
||||
|
||||
if (req.body.txn_type === 'recurring_payment_profile_cancel' || req.body.txn_type === 'subscr_cancel') {
|
||||
let user = await User.findOne({ 'purchased.plan.customerId': req.body.recurring_payment_id });
|
||||
if (user) {
|
||||
await payments.cancelSubscription({ user, paymentMethod: 'Paypal' });
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
169
website/src/controllers/top-level/payments/stripe.js
Normal file
169
website/src/controllers/top-level/payments/stripe.js
Normal file
@@ -0,0 +1,169 @@
|
||||
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';
|
||||
import nconf from 'nconf';
|
||||
import { model as User } from '../../../models/user';
|
||||
import cc from 'coupon-code';
|
||||
import {
|
||||
authWithHeaders,
|
||||
authWithUrl,
|
||||
} from '../../../middlewares/api-v3/auth';
|
||||
|
||||
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 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 {Object} data Empty object
|
||||
**/
|
||||
api.checkout = {
|
||||
method: 'POST',
|
||||
url: '/stripe/checkout',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let token = req.body.id;
|
||||
let user = res.locals.user;
|
||||
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||
let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
|
||||
let coupon;
|
||||
let response;
|
||||
|
||||
if (!token) throw new BadRequest('Missing req.body.id');
|
||||
|
||||
if (sub) {
|
||||
if (sub.discount) {
|
||||
if (!req.query.coupon) throw new BadRequest(res.t('couponCodeRequired'));
|
||||
coupon = await Coupon.findOne({_id: cc.validate(req.query.coupon), event: sub.key});
|
||||
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
|
||||
}
|
||||
|
||||
response = await stripe.customers.create({
|
||||
email: req.body.email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
} else {
|
||||
let amount = 500; // $5
|
||||
|
||||
if (gift) {
|
||||
if (gift.type === 'subscription') {
|
||||
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
|
||||
} else {
|
||||
amount = `${gift.gems.amount / 4 * 100}`;
|
||||
}
|
||||
}
|
||||
|
||||
response = await stripe.charges.create({
|
||||
amount,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
});
|
||||
} else {
|
||||
let method = 'buyGems';
|
||||
let data = {
|
||||
user,
|
||||
customerId: response.id,
|
||||
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, {});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 Body parameter - The token
|
||||
*
|
||||
* @apiSuccess {Object} data Empty object
|
||||
**/
|
||||
api.subscribeEdit = {
|
||||
method: 'POST',
|
||||
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 NotAuthorized(res.t('missingSubscription'));
|
||||
if (!token) throw new BadRequest('Missing req.body.id');
|
||||
|
||||
let subscriptions = await stripe.customers.listSubscriptions(customerId);
|
||||
let subscriptionId = subscriptions.data[0].id;
|
||||
await stripe.customers.updateSubscription(customerId, subscriptionId, { card: token });
|
||||
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @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
|
||||
**/
|
||||
api.subscribeCancel = {
|
||||
method: 'GET',
|
||||
url: '/stripe/subscribe/cancel',
|
||||
middlewares: [authWithUrl],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
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);
|
||||
await payments.cancelSubscriptoin({
|
||||
user,
|
||||
nextBill: customer.subscription.current_period_end * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
});
|
||||
|
||||
res.redirect('/');
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
62
website/src/libs/api-v3/amazonPayments.js
Normal file
62
website/src/libs/api-v3/amazonPayments.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import amazonPayments from 'amazon-payments';
|
||||
import nconf from 'nconf';
|
||||
import common from '../../../../common';
|
||||
import Q from 'q';
|
||||
import {
|
||||
BadRequest,
|
||||
} from './errors';
|
||||
|
||||
// TODO better handling of errors
|
||||
|
||||
const i18n = common.i18n;
|
||||
const IS_PROD = nconf.get('NODE_ENV') === 'production';
|
||||
|
||||
let amzPayment = amazonPayments.connect({
|
||||
environment: amazonPayments.Environment[IS_PROD ? 'Production' : 'Sandbox'],
|
||||
sellerId: nconf.get('AMAZON_PAYMENTS:SELLER_ID'),
|
||||
mwsAccessKey: nconf.get('AMAZON_PAYMENTS:MWS_KEY'),
|
||||
mwsSecretKey: nconf.get('AMAZON_PAYMENTS:MWS_SECRET'),
|
||||
clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'),
|
||||
});
|
||||
|
||||
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);
|
||||
let confirmOrderReference = Q.nbind(amzPayment.offAmazonPayments.confirmOrderReference, amzPayment.offAmazonPayments);
|
||||
let closeOrderReference = Q.nbind(amzPayment.offAmazonPayments.closeOrderReference, amzPayment.offAmazonPayments);
|
||||
let setBillingAgreementDetails = Q.nbind(amzPayment.offAmazonPayments.setBillingAgreementDetails, amzPayment.offAmazonPayments);
|
||||
let confirmBillingAgreement = Q.nbind(amzPayment.offAmazonPayments.confirmBillingAgreement, amzPayment.offAmazonPayments);
|
||||
let closeBillingAgreement = Q.nbind(amzPayment.offAmazonPayments.closeBillingAgreement, amzPayment.offAmazonPayments);
|
||||
|
||||
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(new BadRequest(i18n.t('paymentNotSuccessful')));
|
||||
return resolve(response);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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(new BadRequest(i18n.t('paymentNotSuccessful')));
|
||||
return resolve(response);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getTokenInfo,
|
||||
createOrderReferenceId,
|
||||
setOrderReferenceDetails,
|
||||
confirmOrderReference,
|
||||
closeOrderReference,
|
||||
confirmBillingAgreement,
|
||||
setBillingAgreementDetails,
|
||||
closeBillingAgreement,
|
||||
authorizeOnBillingAgreement,
|
||||
authorize,
|
||||
};
|
||||
185
website/src/libs/api-v3/payments.js
Normal file
185
website/src/libs/api-v3/payments.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import _ from 'lodash' ;
|
||||
import analytics from './analyticsService';
|
||||
import {
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from './email';
|
||||
import members from '../../controllers/api-v3/members';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import pushNotify from './pushNotifications';
|
||||
import shared from '../../../../common' ;
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
|
||||
let api = {};
|
||||
|
||||
function revealMysteryItems (user) {
|
||||
_.each(shared.content.gear.flat, function findMysteryItems (item) {
|
||||
if (
|
||||
item.klass === 'mystery' &&
|
||||
moment().isAfter(shared.content.mystery[item.mystery].start) &&
|
||||
moment().isBefore(shared.content.mystery[item.mystery].end) &&
|
||||
!user.items.gear.owned[item.key] &&
|
||||
user.purchased.plan.mysteryItems.indexOf(item.key) !== -1
|
||||
) {
|
||||
user.purchased.plan.mysteryItems.push(item.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
api.createSubscription = async function createSubscription (data) {
|
||||
let recipient = data.gift ? data.gift.member : data.user;
|
||||
let plan = recipient.purchased.plan;
|
||||
let block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
|
||||
let months = Number(block.months);
|
||||
|
||||
if (data.gift) {
|
||||
if (plan.customerId && !plan.dateTerminated) { // User has active plan
|
||||
plan.extraMonths += months;
|
||||
} else {
|
||||
plan.dateTerminated = moment(plan.dateTerminated).add({months}).toDate();
|
||||
if (!plan.dateUpdated) plan.dateUpdated = new Date();
|
||||
}
|
||||
|
||||
if (!plan.customerId) plan.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId
|
||||
} else {
|
||||
_(plan).merge({ // override with these values
|
||||
planId: block.key,
|
||||
customerId: data.customerId,
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: data.paymentMethod,
|
||||
extraMonths: Number(plan.extraMonths) +
|
||||
Number(plan.dateTerminated ? moment(plan.dateTerminated).diff(new Date(), 'months', true) : 0),
|
||||
dateTerminated: null,
|
||||
// Specify a lastBillingDate just for Amazon Payments
|
||||
// Resetted every time the subscription restarts
|
||||
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? new Date() : undefined,
|
||||
}).defaults({ // allow non-override if a plan was previously used
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
}).value();
|
||||
}
|
||||
|
||||
// Block sub perks
|
||||
let perks = Math.floor(months / 3);
|
||||
if (perks) {
|
||||
plan.consecutive.offset += months;
|
||||
plan.consecutive.gemCapExtra += perks * 5;
|
||||
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
|
||||
plan.consecutive.trinkets += perks;
|
||||
}
|
||||
|
||||
revealMysteryItems(recipient);
|
||||
|
||||
if (IS_PROD) {
|
||||
if (!data.gift) txnEmail(data.user, 'subscription-begins');
|
||||
|
||||
analytics.trackPurchase({
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: Boolean(data.gift),
|
||||
purchaseValue: block.price,
|
||||
});
|
||||
}
|
||||
|
||||
data.user.purchased.txnCount++;
|
||||
|
||||
if (data.gift) {
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
|
||||
let byUserName = getUserInfo(data.user, ['name']).name;
|
||||
|
||||
if (data.gift.member.preferences.emailNotifications.giftedSubscription !== false) {
|
||||
txnEmail(data.gift.member, 'gifted-subscription', [
|
||||
{name: 'GIFTER', content: byUserName},
|
||||
{name: 'X_MONTHS_SUBSCRIPTION', content: months},
|
||||
]);
|
||||
}
|
||||
|
||||
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`);
|
||||
}
|
||||
}
|
||||
|
||||
await data.user.save();
|
||||
if (data.gift) await data.gift.member.save();
|
||||
};
|
||||
|
||||
// Sets their subscription to be cancelled later
|
||||
api.cancelSubscription = async function cancelSubscription (data) {
|
||||
let plan = data.user.purchased.plan;
|
||||
let now = moment();
|
||||
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
|
||||
let nowStr = `${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`;
|
||||
let nowStrFormat = 'MM/DD/YYYY';
|
||||
|
||||
plan.dateTerminated =
|
||||
moment(nowStr, nowStrFormat)
|
||||
.add({days: remaining}) // end their subscription 1mo from their last payment
|
||||
.add({days: Math.ceil(30 * plan.extraMonths)}) // plus any extra time (carry-over, gifted subscription, etc) they have.
|
||||
.toDate();
|
||||
plan.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
|
||||
|
||||
await data.user.save();
|
||||
|
||||
txnEmail(data.user, 'cancel-subscription');
|
||||
|
||||
analytics.track('unsubscribe', {
|
||||
uuid: data.user._id,
|
||||
gaCategory: 'commerce',
|
||||
gaLabel: data.paymentMethod,
|
||||
paymentMethod: data.paymentMethod,
|
||||
});
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
analytics.trackPurchase({
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Gems',
|
||||
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
|
||||
purchaseType: 'checkout',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: Boolean(data.gift),
|
||||
purchaseValue: amt,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.gift) {
|
||||
let byUsername = getUserInfo(data.user, ['name']).name;
|
||||
let gemAmount = data.gift.gems.amount || 20;
|
||||
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
if (data.gift.member.preferences.emailNotifications.giftedGems !== false) {
|
||||
txnEmail(data.gift.member, 'gifted-gems', [
|
||||
{name: 'GIFTER', content: byUsername},
|
||||
{name: 'X_GEMS_GIFTED', content: gemAmount},
|
||||
]);
|
||||
}
|
||||
|
||||
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
|
||||
}
|
||||
|
||||
await data.gift.member.save();
|
||||
}
|
||||
|
||||
await data.user.save();
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
@@ -55,3 +55,21 @@ export function authWithSession (req, res, next) {
|
||||
})
|
||||
.catch(next);
|
||||
}
|
||||
|
||||
export function authWithUrl (req, res, next) {
|
||||
let userId = req.query._id;
|
||||
let apiToken = req.query.apiToken;
|
||||
|
||||
if (!userId || !apiToken) {
|
||||
throw new NotAuthorized(res.t('missingAuthParams'));
|
||||
}
|
||||
|
||||
User.findOne({ _id: userId, apiToken }).exec()
|
||||
.then((user) => {
|
||||
if (!user) throw new NotAuthorized(res.t('invalidCredentials'));
|
||||
|
||||
res.locals.user = user;
|
||||
next();
|
||||
})
|
||||
.catch(next);
|
||||
}
|
||||
|
||||
@@ -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