mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +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/routes/pages.js
|
||||||
website/src/middlewares/apiThrottle.js
|
website/src/middlewares/apiThrottle.js
|
||||||
website/src/middlewares/forceRefresh.js
|
website/src/middlewares/forceRefresh.js
|
||||||
website/src/controllers/payments/
|
|
||||||
|
|
||||||
debug-scripts/*
|
debug-scripts/*
|
||||||
|
scripts/*
|
||||||
tasks/*.js
|
tasks/*.js
|
||||||
gulpfile.js
|
gulpfile.js
|
||||||
Gruntfile.js
|
Gruntfile.js
|
||||||
|
|||||||
@@ -2,5 +2,8 @@
|
|||||||
"extends": [
|
"extends": [
|
||||||
"habitrpg/server",
|
"habitrpg/server",
|
||||||
"habitrpg/babel"
|
"habitrpg/babel"
|
||||||
]
|
],
|
||||||
|
"globals": {
|
||||||
|
"Promise": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"missingAuthHeaders": "Missing authentication headers.",
|
"missingAuthHeaders": "Missing authentication headers.",
|
||||||
|
"missingAuthParams": "Missing authentication parameters.",
|
||||||
"missingUsernameEmail": "Missing username or email.",
|
"missingUsernameEmail": "Missing username or email.",
|
||||||
"missingEmail": "Missing email.",
|
"missingEmail": "Missing email.",
|
||||||
"missingUsername": "Missing username.",
|
"missingUsername": "Missing username.",
|
||||||
@@ -100,6 +101,8 @@
|
|||||||
"noAdminAccess": "You don't have admin access.",
|
"noAdminAccess": "You don't have admin access.",
|
||||||
"pageMustBeNumber": "req.query.page must be a number",
|
"pageMustBeNumber": "req.query.page must be a number",
|
||||||
"missingUnsubscriptionCode": "Missing unsubscription code.",
|
"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.",
|
"userNotFound": "User not found.",
|
||||||
"spellNotFound": "Spell \"<%= spellId %>\" not found.",
|
"spellNotFound": "Spell \"<%= spellId %>\" not found.",
|
||||||
"partyNotFound": "Party not found",
|
"partyNotFound": "Party not found",
|
||||||
@@ -171,5 +174,8 @@
|
|||||||
"pushDeviceAlreadyAdded": "The user already has the push device",
|
"pushDeviceAlreadyAdded": "The user already has the push device",
|
||||||
"resetComplete": "Reset completed",
|
"resetComplete": "Reset completed",
|
||||||
"lvl10ChangeClass": "To change class you must be at least level 10.",
|
"lvl10ChangeClass": "To change class you must be at least level 10.",
|
||||||
"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) {
|
oldTask.tags = _.map(oldTask.tags || {}, function (tagPresent, tagId) {
|
||||||
return tagPresent && tagId;
|
return tagPresent && tagId;
|
||||||
|
}).filter(function (tag) {
|
||||||
|
return tag !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!oldTask.text) oldTask.text = 'task text'; // required
|
if (!oldTask.text) oldTask.text = 'task text'; // required
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ function processUsers (afterId) {
|
|||||||
if (!oldTask.text) oldTask.text = 'task text'; // required
|
if (!oldTask.text) oldTask.text = 'task text'; // required
|
||||||
oldTask.tags = _.map(oldTask.tags, function (tagPresent, tagId) {
|
oldTask.tags = _.map(oldTask.tags, function (tagPresent, tagId) {
|
||||||
return tagPresent && tagId;
|
return tagPresent && tagId;
|
||||||
|
}).filter(function (tag) {
|
||||||
|
return tag !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oldTask.type !== 'todo' || (oldTask.type === 'todo' && !oldTask.completed)) {
|
if (oldTask.type !== 'todo' || (oldTask.type === 'todo' && !oldTask.completed)) {
|
||||||
|
|||||||
@@ -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
|
// 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),
|
// 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
|
// and once for any time you need to edit the plan thereafter
|
||||||
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var nconf = require('nconf');
|
var nconf = require('nconf');
|
||||||
_ = require('lodash');
|
var _ = require('lodash');
|
||||||
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
|
|
||||||
var paypal = require('paypal-rest-sdk');
|
var paypal = require('paypal-rest-sdk');
|
||||||
var blocks = require('../../../../common').content.subscriptionBlocks;
|
var blocks = require('../../../../common').content.subscriptionBlocks;
|
||||||
var live = nconf.get('PAYPAL:mode')=='live';
|
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
|
var OP = 'create'; // list create update remove
|
||||||
|
|
||||||
paypal.configure({
|
paypal.configure({
|
||||||
@@ -358,6 +358,10 @@ gulp.task('test:api-v3:unit', (done) => {
|
|||||||
pipe(runner);
|
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) => {
|
gulp.task('test:api-v3:integration', (done) => {
|
||||||
let runner = exec(
|
let runner = exec(
|
||||||
testBin('mocha test/api/v3/integration --recursive'),
|
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.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) => {
|
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}`;
|
let url = `http://localhost:${API_TEST_SERVER_PORT}`;
|
||||||
|
|
||||||
// do not prefix with api/apiVersion requests to top level routes like dataexport and payments
|
// 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}`;
|
url += `${route}`;
|
||||||
} else {
|
} else {
|
||||||
url += `/api/${apiVersion}${route}`;
|
url += `/api/${apiVersion}${route}`;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
$http.post(url, res).success(function() {
|
$http.post(url, res).success(function() {
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
}).error(function(res) {
|
}).error(function(res) {
|
||||||
alert(res.err);
|
alert(res.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -55,7 +55,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
$http.post(url, data).success(function() {
|
$http.post(url, data).success(function() {
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
}).error(function(data) {
|
}).error(function(data) {
|
||||||
alert(data.err);
|
alert(data.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -127,12 +127,12 @@ function($rootScope, User, $http, Content) {
|
|||||||
var url = '/amazon/createOrderReferenceId'
|
var url = '/amazon/createOrderReferenceId'
|
||||||
$http.post(url, {
|
$http.post(url, {
|
||||||
billingAgreementId: Payments.amazonPayments.billingAgreementId
|
billingAgreementId: Payments.amazonPayments.billingAgreementId
|
||||||
}).success(function(data){
|
}).success(function(res){
|
||||||
Payments.amazonPayments.loggedIn = true;
|
Payments.amazonPayments.loggedIn = true;
|
||||||
Payments.amazonPayments.orderReferenceId = data.orderReferenceId;
|
Payments.amazonPayments.orderReferenceId = res.data.orderReferenceId;
|
||||||
Payments.amazonPayments.initWidgets();
|
Payments.amazonPayments.initWidgets();
|
||||||
}).error(function(res){
|
}).error(function(res){
|
||||||
alert(res.err);
|
alert(res.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -146,7 +146,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
|
|
||||||
var url = '/amazon/verifyAccessToken'
|
var url = '/amazon/verifyAccessToken'
|
||||||
$http.post(url, response).error(function(res){
|
$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();
|
Payments.amazonPayments.reset();
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
}).error(function(res){
|
}).error(function(res){
|
||||||
alert(res.err);
|
alert(res.message);
|
||||||
Payments.amazonPayments.reset();
|
Payments.amazonPayments.reset();
|
||||||
});
|
});
|
||||||
}else if(Payments.amazonPayments.type === 'subscription'){
|
}else if(Payments.amazonPayments.type === 'subscription'){
|
||||||
@@ -246,7 +246,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
Payments.amazonPayments.reset();
|
Payments.amazonPayments.reset();
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
}).error(function(res){
|
}).error(function(res){
|
||||||
alert(res.err);
|
alert(res.message);
|
||||||
Payments.amazonPayments.reset();
|
Payments.amazonPayments.reset();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,8 +289,6 @@ api.update = function(req, res, next){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
import { _closeChal } from '../api-v3/challenges';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete & close
|
* Delete & close
|
||||||
*/
|
*/
|
||||||
@@ -304,7 +302,7 @@ api.delete = async function(req, res, next){
|
|||||||
if (!challenge.canModify(user)) return next(shared.i18n.t('noPermissionCloseChallenge'));
|
if (!challenge.canModify(user)) return next(shared.i18n.t('noPermissionCloseChallenge'));
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_DELETED'});
|
await challenge.closeChal({broken: 'CHALLENGE_DELETED'});
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
@@ -326,7 +324,7 @@ api.selectWinner = async function(req, res, next) {
|
|||||||
if (!winner || winner.challenges.indexOf(challenge._id) === -1) return next('Winner ' + req.query.uid + ' not found.');
|
if (!winner || winner.challenges.indexOf(challenge._id) === -1) return next('Winner ' + req.query.uid + ' not found.');
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_CLOSED', winner});
|
await challenge.closeChal({broken: 'CHALLENGE_CLOSED', winner});
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ import {
|
|||||||
NotFound,
|
NotFound,
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
} from '../../libs/api-v3/errors';
|
} from '../../libs/api-v3/errors';
|
||||||
import shared from '../../../../common';
|
|
||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
import { sendTxn as txnEmail } from '../../libs/api-v3/email';
|
|
||||||
import sendPushNotification from '../../libs/api-v3/pushNotifications';
|
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import csvStringify from '../../libs/api-v3/csvStringify';
|
import csvStringify from '../../libs/api-v3/csvStringify';
|
||||||
|
|
||||||
@@ -449,64 +446,6 @@ api.updateChallenge = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO everything here should be moved to a worker
|
|
||||||
// actually even for a worker it's probably just too big and will kill mongo
|
|
||||||
// Exported because it's used in v2 controller
|
|
||||||
export async function _closeChal (challenge, broken = {}) {
|
|
||||||
let winner = broken.winner;
|
|
||||||
let brokenReason = broken.broken;
|
|
||||||
|
|
||||||
// Delete the challenge
|
|
||||||
await Challenge.remove({_id: challenge._id}).exec();
|
|
||||||
|
|
||||||
// Refund the leader if the challenge is closed and the group not the tavern
|
|
||||||
if (challenge.group !== TAVERN_ID && brokenReason === 'CHALLENGE_DELETED') {
|
|
||||||
await User.update({_id: challenge.leader}, {$inc: {balance: challenge.prize / 4}}).exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the challengeCount on the group
|
|
||||||
await Group.update({_id: challenge.group}, {$inc: {challengeCount: -1}}).exec();
|
|
||||||
|
|
||||||
// Award prize to winner and notify
|
|
||||||
if (winner) {
|
|
||||||
winner.achievements.challenges.push(challenge.name);
|
|
||||||
winner.balance += challenge.prize / 4;
|
|
||||||
let savedWinner = await winner.save();
|
|
||||||
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
|
|
||||||
txnEmail(savedWinner, 'won-challenge', [
|
|
||||||
{name: 'CHALLENGE_NAME', content: challenge.name},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPushNotification(savedWinner, shared.i18n.t('wonChallenge'), challenge.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run some operations in the background withouth blocking the thread
|
|
||||||
let backgroundTasks = [
|
|
||||||
// And it's tasks
|
|
||||||
Tasks.Task.remove({'challenge.id': challenge._id, userId: {$exists: false}}).exec(),
|
|
||||||
// Set the challenge tag to non-challenge status and remove the challenge from the user's challenges
|
|
||||||
User.update({
|
|
||||||
challenges: challenge._id,
|
|
||||||
'tags._id': challenge._id,
|
|
||||||
}, {
|
|
||||||
$set: {'tags.$.challenge': false},
|
|
||||||
$pull: {challenges: challenge._id},
|
|
||||||
}, {multi: true}).exec(),
|
|
||||||
// Break users' tasks
|
|
||||||
Tasks.Task.update({
|
|
||||||
'challenge.id': challenge._id,
|
|
||||||
}, {
|
|
||||||
$set: {
|
|
||||||
'challenge.broken': brokenReason,
|
|
||||||
'challenge.winner': winner && winner.profile.name,
|
|
||||||
},
|
|
||||||
}, {multi: true}).exec(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Q.all(backgroundTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {delete} /api/v3/challenges/:challengeId Delete a challenge
|
* @api {delete} /api/v3/challenges/:challengeId Delete a challenge
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
@@ -534,7 +473,7 @@ api.deleteChallenge = {
|
|||||||
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_DELETED'});
|
await challenge.closeChal({broken: 'CHALLENGE_DELETED'});
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -571,7 +510,7 @@ api.selectChallengeWinner = {
|
|||||||
if (!winner || winner.challenges.indexOf(challenge._id) === -1) throw new NotFound(res.t('winnerNotFound', {userId: req.params.winnerId}));
|
if (!winner || winner.challenges.indexOf(challenge._id) === -1) throw new NotFound(res.t('winnerNotFound', {userId: req.params.winnerId}));
|
||||||
|
|
||||||
// Close channel in background, some ops are run in the background without `await`ing
|
// Close channel in background, some ops are run in the background without `await`ing
|
||||||
await _closeChal(challenge, {broken: 'CHALLENGE_CLOSED', winner});
|
await challenge.closeChal({broken: 'CHALLENGE_CLOSED', winner});
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
.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) {
|
if (!responseErr || responseErr.httpCode >= 500) {
|
||||||
// Try to identify the error...
|
// Try to identify the error...
|
||||||
// ...
|
// ...
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ v2app.use(responseHandler);
|
|||||||
|
|
||||||
// Custom Directives
|
// Custom Directives
|
||||||
v2app.use('/', require('../../routes/api-v2/auth'));
|
v2app.use('/', require('../../routes/api-v2/auth'));
|
||||||
// v2app.use('/', require('../../routes/api-v2/coupon')); // TODO REMOVE - ONLY v3
|
|
||||||
// v2app.use('/', require('../../routes/api-v2/unsubscription')); // TODO REMOVE - ONLY v3
|
|
||||||
|
|
||||||
require('../../routes/api-v2/swagger')(swagger, v2app);
|
require('../../routes/api-v2/swagger')(swagger, v2app);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import baseModel from '../libs/api-v3/baseModel';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as Tasks from './task';
|
import * as Tasks from './task';
|
||||||
import { model as User } from './user';
|
import { model as User } from './user';
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
TAVERN_ID,
|
||||||
|
} from './group';
|
||||||
import { removeFromArray } from '../libs/api-v3/collectionManipulators';
|
import { removeFromArray } from '../libs/api-v3/collectionManipulators';
|
||||||
|
import shared from '../../../common';
|
||||||
|
import { sendTxn as txnEmail } from '../libs/api-v3/email';
|
||||||
|
import sendPushNotification from '../libs/api-v3/pushNotifications';
|
||||||
|
|
||||||
let Schema = mongoose.Schema;
|
let Schema = mongoose.Schema;
|
||||||
|
|
||||||
@@ -251,6 +258,65 @@ schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO everything here should be moved to a worker
|
||||||
|
// actually even for a worker it's probably just too big and will kill mongo
|
||||||
|
schema.methods.closeChal = async function closeChal (broken = {}) {
|
||||||
|
let challenge = this;
|
||||||
|
|
||||||
|
let winner = broken.winner;
|
||||||
|
let brokenReason = broken.broken;
|
||||||
|
|
||||||
|
// Delete the challenge
|
||||||
|
await this.model('Challenge').remove({_id: challenge._id}).exec();
|
||||||
|
|
||||||
|
// Refund the leader if the challenge is closed and the group not the tavern
|
||||||
|
if (challenge.group !== TAVERN_ID && brokenReason === 'CHALLENGE_DELETED') {
|
||||||
|
await User.update({_id: challenge.leader}, {$inc: {balance: challenge.prize / 4}}).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the challengeCount on the group
|
||||||
|
await Group.update({_id: challenge.group}, {$inc: {challengeCount: -1}}).exec();
|
||||||
|
|
||||||
|
// Award prize to winner and notify
|
||||||
|
if (winner) {
|
||||||
|
winner.achievements.challenges.push(challenge.name);
|
||||||
|
winner.balance += challenge.prize / 4;
|
||||||
|
let savedWinner = await winner.save();
|
||||||
|
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
|
||||||
|
txnEmail(savedWinner, 'won-challenge', [
|
||||||
|
{name: 'CHALLENGE_NAME', content: challenge.name},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPushNotification(savedWinner, shared.i18n.t('wonChallenge'), challenge.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run some operations in the background withouth blocking the thread
|
||||||
|
let backgroundTasks = [
|
||||||
|
// And it's tasks
|
||||||
|
Tasks.Task.remove({'challenge.id': challenge._id, userId: {$exists: false}}).exec(),
|
||||||
|
// Set the challenge tag to non-challenge status and remove the challenge from the user's challenges
|
||||||
|
User.update({
|
||||||
|
challenges: challenge._id,
|
||||||
|
'tags._id': challenge._id,
|
||||||
|
}, {
|
||||||
|
$set: {'tags.$.challenge': false},
|
||||||
|
$pull: {challenges: challenge._id},
|
||||||
|
}, {multi: true}).exec(),
|
||||||
|
// Break users' tasks
|
||||||
|
Tasks.Task.update({
|
||||||
|
'challenge.id': challenge._id,
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
'challenge.broken': brokenReason,
|
||||||
|
'challenge.winner': winner && winner.profile.name,
|
||||||
|
},
|
||||||
|
}, {multi: true}).exec(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Q.all(backgroundTasks);
|
||||||
|
};
|
||||||
|
|
||||||
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the challenge model)
|
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the challenge model)
|
||||||
// These will be removed once API v2 is discontinued
|
// These will be removed once API v2 is discontinued
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user