mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 14:17:22 +01:00
Group plan subscription (#8153)
* Added payment to groups and pay with group plan with Stripe * Added edit card for Stripe * Added stripe cancel * Added subscribe with Amazon payments * Added Amazon cancel for group subscription * Added group subscription with paypal * Added paypal cancel * Added ipn cancel for Group plan * Added a subscription tab and hid only the task tab when group is not subscribed * Fixed linting issues * Fixed tests * Added payment unit tests * Added back refresh after stripe payment * Fixed style issues * Limited grouop query fields and checked access * Abstracted subscription schema * Added year group plan and more access checks * Maded purchase fields private * Removed id and timestampes * Added else checks to ensure user subscription is not altered. Removed active field from group model * Added toJSONTransform function * Moved plan active check to other toJson function * Added check to see if purchaed has been populated * Added purchase details to private * Added correct data usage when paying for group sub
This commit is contained in:
committed by
Matteo Pagliazzi
parent
7f38c61c70
commit
d8c37f6e2d
@@ -3,15 +3,27 @@ import * as api from '../../../../../website/server/libs/payments';
|
|||||||
import analytics from '../../../../../website/server/libs/analyticsService';
|
import analytics from '../../../../../website/server/libs/analyticsService';
|
||||||
import notifications from '../../../../../website/server/libs/pushNotifications';
|
import notifications from '../../../../../website/server/libs/pushNotifications';
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
|
import { model as Group } from '../../../../../website/server/models/group';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import {
|
||||||
|
generateGroup,
|
||||||
|
} from '../../../../helpers/api-unit.helper.js';
|
||||||
|
|
||||||
describe('payments/index', () => {
|
describe('payments/index', () => {
|
||||||
let user, data, plan;
|
let user, group, data, plan;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
user = new User();
|
user = new User();
|
||||||
user.profile.name = 'sender';
|
user.profile.name = 'sender';
|
||||||
|
|
||||||
|
group = generateGroup({
|
||||||
|
name: 'test group',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
leader: user._id,
|
||||||
|
});
|
||||||
|
await group.save();
|
||||||
|
|
||||||
sandbox.stub(sender, 'sendTxn');
|
sandbox.stub(sender, 'sendTxn');
|
||||||
sandbox.stub(user, 'sendMessage');
|
sandbox.stub(user, 'sendMessage');
|
||||||
sandbox.stub(analytics, 'trackPurchase');
|
sandbox.stub(analytics, 'trackPurchase');
|
||||||
@@ -186,6 +198,7 @@ describe('payments/index', () => {
|
|||||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||||
expect(analytics.trackPurchase).to.be.calledWith({
|
expect(analytics.trackPurchase).to.be.calledWith({
|
||||||
uuid: user._id,
|
uuid: user._id,
|
||||||
|
groupId: undefined,
|
||||||
itemPurchased: 'Subscription',
|
itemPurchased: 'Subscription',
|
||||||
sku: 'payment method-subscription',
|
sku: 'payment method-subscription',
|
||||||
purchaseType: 'subscribe',
|
purchaseType: 'subscribe',
|
||||||
@@ -276,6 +289,7 @@ describe('payments/index', () => {
|
|||||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||||
expect(analytics.trackPurchase).to.be.calledWith({
|
expect(analytics.trackPurchase).to.be.calledWith({
|
||||||
uuid: user._id,
|
uuid: user._id,
|
||||||
|
groupId: undefined,
|
||||||
itemPurchased: 'Subscription',
|
itemPurchased: 'Subscription',
|
||||||
sku: 'payment method-subscription',
|
sku: 'payment method-subscription',
|
||||||
purchaseType: 'subscribe',
|
purchaseType: 'subscribe',
|
||||||
@@ -291,6 +305,53 @@ describe('payments/index', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('Purchasing a subscription for group', () => {
|
||||||
|
it('creates a subscription', async () => {
|
||||||
|
expect(group.purchased.plan.planId).to.not.exist;
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
|
||||||
|
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
|
||||||
|
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
|
||||||
|
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
|
||||||
|
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
|
||||||
|
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||||
|
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
|
||||||
|
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
|
||||||
|
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets extraMonths if plan has dateTerminated date', async () => {
|
||||||
|
group.purchased.plan = plan;
|
||||||
|
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
|
||||||
|
await group.save();
|
||||||
|
expect(group.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||||
|
group.purchased.plan = plan;
|
||||||
|
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
|
||||||
|
await group.save();
|
||||||
|
expect(group.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
context('Block subscription perks', () => {
|
context('Block subscription perks', () => {
|
||||||
it('adds block months to plan.consecutive.offset', async () => {
|
it('adds block months to plan.consecutive.offset', async () => {
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
@@ -426,61 +487,136 @@ describe('payments/index', () => {
|
|||||||
data = { user };
|
data = { user };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds a month termination date by default', async () => {
|
context('Canceling a subscription for self', () => {
|
||||||
await api.cancelSubscription(data);
|
it('adds a month termination date by default', async () => {
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
let now = new Date();
|
let now = new Date();
|
||||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds extraMonths to dateTerminated value', async () => {
|
||||||
|
user.purchased.plan.extraMonths = 2;
|
||||||
|
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
let now = new Date();
|
||||||
|
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
|
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles extra month fractions', async () => {
|
||||||
|
user.purchased.plan.extraMonths = 0.3;
|
||||||
|
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
let now = new Date();
|
||||||
|
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
|
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
||||||
|
});
|
||||||
|
|
||||||
|
it('terminates at next billing date if it exists', async () => {
|
||||||
|
data.nextBill = moment().add({ days: 15 });
|
||||||
|
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
let now = new Date();
|
||||||
|
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
|
expect(daysTillTermination).to.be.within(13, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets plan.extraMonths', async () => {
|
||||||
|
user.purchased.plan.extraMonths = 5;
|
||||||
|
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends an email', async () => {
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
expect(sender.sendTxn).to.be.calledOnce;
|
||||||
|
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds extraMonths to dateTerminated value', async () => {
|
context('Canceling a subscription for group', () => {
|
||||||
user.purchased.plan.extraMonths = 2;
|
it('adds a month termination date by default', async () => {
|
||||||
|
data.groupId = group._id;
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
await api.cancelSubscription(data);
|
let now = new Date();
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
let now = new Date();
|
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
});
|
||||||
|
|
||||||
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
it('adds extraMonths to dateTerminated value', async () => {
|
||||||
});
|
group.purchased.plan.extraMonths = 2;
|
||||||
|
await group.save();
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
it('handles extra month fractions', async () => {
|
await api.cancelSubscription(data);
|
||||||
user.purchased.plan.extraMonths = 0.3;
|
|
||||||
|
|
||||||
await api.cancelSubscription(data);
|
let now = new Date();
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
let now = new Date();
|
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
});
|
||||||
|
|
||||||
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
it('handles extra month fractions', async () => {
|
||||||
});
|
group.purchased.plan.extraMonths = 0.3;
|
||||||
|
await group.save();
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
it('terminates at next billing date if it exists', async () => {
|
await api.cancelSubscription(data);
|
||||||
data.nextBill = moment().add({ days: 15 });
|
|
||||||
|
|
||||||
await api.cancelSubscription(data);
|
let now = new Date();
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
let now = new Date();
|
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
||||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
});
|
||||||
|
|
||||||
expect(daysTillTermination).to.be.within(13, 15);
|
it('terminates at next billing date if it exists', async () => {
|
||||||
});
|
data.nextBill = moment().add({ days: 15 });
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
it('resets plan.extraMonths', async () => {
|
await api.cancelSubscription(data);
|
||||||
user.purchased.plan.extraMonths = 5;
|
|
||||||
|
|
||||||
await api.cancelSubscription(data);
|
let now = new Date();
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||||
|
|
||||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
expect(daysTillTermination).to.be.within(13, 15);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends an email', async () => {
|
it('resets plan.extraMonths', async () => {
|
||||||
await api.cancelSubscription(data);
|
group.purchased.plan.extraMonths = 5;
|
||||||
|
await group.save();
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
expect(sender.sendTxn).to.be.calledOnce;
|
await api.cancelSubscription(data);
|
||||||
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends an email', async () => {
|
||||||
|
data.groupId = group._id;
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
expect(sender.sendTxn).to.be.calledOnce;
|
||||||
|
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,21 @@ function($rootScope, User, $http, Content) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Payments.showStripe = function(data) {
|
Payments.showStripe = function(data) {
|
||||||
var sub =
|
var sub = false;
|
||||||
data.subscription ? data.subscription
|
|
||||||
: data.gift && data.gift.type=='subscription' ? data.gift.subscription.key
|
if (data.subscription) {
|
||||||
: false;
|
sub = data.subscription;
|
||||||
|
} else if (data.gift && data.gift.type=='subscription') {
|
||||||
|
sub = data.gift.subscription.key;
|
||||||
|
}
|
||||||
|
|
||||||
sub = sub && Content.subscriptionBlocks[sub];
|
sub = sub && Content.subscriptionBlocks[sub];
|
||||||
|
|
||||||
var amount = // 500 = $5
|
var amount = // 500 = $5
|
||||||
sub ? sub.price*100
|
sub ? sub.price*100
|
||||||
: data.gift && data.gift.type=='gems' ? data.gift.gems.amount/4*100
|
: data.gift && data.gift.type=='gems' ? data.gift.gems.amount/4*100
|
||||||
: 500;
|
: 500;
|
||||||
|
|
||||||
StripeCheckout.open({
|
StripeCheckout.open({
|
||||||
key: window.env.STRIPE_PUB_KEY,
|
key: window.env.STRIPE_PUB_KEY,
|
||||||
address: false,
|
address: false,
|
||||||
@@ -34,6 +40,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
|
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
|
||||||
if (data.subscription) url += '&sub='+sub.key;
|
if (data.subscription) url += '&sub='+sub.key;
|
||||||
if (data.coupon) url += '&coupon='+data.coupon;
|
if (data.coupon) url += '&coupon='+data.coupon;
|
||||||
|
if (data.groupId) url += '&groupId=' + data.groupId;
|
||||||
$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) {
|
||||||
@@ -43,7 +50,12 @@ function($rootScope, User, $http, Content) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Payments.showStripeEdit = function(){
|
Payments.showStripeEdit = function(config) {
|
||||||
|
var groupId;
|
||||||
|
if (config.groupId) {
|
||||||
|
groupId = config.groupId;
|
||||||
|
}
|
||||||
|
|
||||||
StripeCheckout.open({
|
StripeCheckout.open({
|
||||||
key: window.env.STRIPE_PUB_KEY,
|
key: window.env.STRIPE_PUB_KEY,
|
||||||
address: false,
|
address: false,
|
||||||
@@ -51,6 +63,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
description: window.env.t('subUpdateDescription'),
|
description: window.env.t('subUpdateDescription'),
|
||||||
panelLabel: window.env.t('subUpdateCard'),
|
panelLabel: window.env.t('subUpdateCard'),
|
||||||
token: function(data) {
|
token: function(data) {
|
||||||
|
data.groupId = groupId;
|
||||||
var url = '/stripe/subscribe/edit';
|
var url = '/stripe/subscribe/edit';
|
||||||
$http.post(url, data).success(function() {
|
$http.post(url, data).success(function() {
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
@@ -85,20 +98,24 @@ function($rootScope, User, $http, Content) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Needs to be called everytime the modal/router is accessed
|
// Needs to be called everytime the modal/router is accessed
|
||||||
Payments.amazonPayments.init = function(data){
|
Payments.amazonPayments.init = function(data) {
|
||||||
if(!isAmazonReady) return;
|
if(!isAmazonReady) return;
|
||||||
if(data.type !== 'single' && data.type !== 'subscription') return;
|
if(data.type !== 'single' && data.type !== 'subscription') return;
|
||||||
|
|
||||||
if(data.gift){
|
if (data.gift) {
|
||||||
if(data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return;
|
if(data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return;
|
||||||
data.gift.uuid = data.giftedTo;
|
data.gift.uuid = data.giftedTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(data.subscription){
|
if (data.subscription) {
|
||||||
Payments.amazonPayments.subscription = data.subscription;
|
Payments.amazonPayments.subscription = data.subscription;
|
||||||
Payments.amazonPayments.coupon = data.coupon;
|
Payments.amazonPayments.coupon = data.coupon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.groupId) {
|
||||||
|
Payments.amazonPayments.groupId = data.groupId;
|
||||||
|
}
|
||||||
|
|
||||||
Payments.amazonPayments.gift = data.gift;
|
Payments.amazonPayments.gift = data.gift;
|
||||||
Payments.amazonPayments.type = data.type;
|
Payments.amazonPayments.type = data.type;
|
||||||
|
|
||||||
@@ -120,10 +137,10 @@ function($rootScope, User, $http, Content) {
|
|||||||
onSignIn: function(contract){
|
onSignIn: function(contract){
|
||||||
Payments.amazonPayments.billingAgreementId = contract.getAmazonBillingAgreementId();
|
Payments.amazonPayments.billingAgreementId = contract.getAmazonBillingAgreementId();
|
||||||
|
|
||||||
if(Payments.amazonPayments.type === 'subscription'){
|
if (Payments.amazonPayments.type === 'subscription') {
|
||||||
Payments.amazonPayments.loggedIn = true;
|
Payments.amazonPayments.loggedIn = true;
|
||||||
Payments.amazonPayments.initWidgets();
|
Payments.amazonPayments.initWidgets();
|
||||||
}else{
|
} else {
|
||||||
var url = '/amazon/createOrderReferenceId'
|
var url = '/amazon/createOrderReferenceId'
|
||||||
$http.post(url, {
|
$http.post(url, {
|
||||||
billingAgreementId: Payments.amazonPayments.billingAgreementId
|
billingAgreementId: Payments.amazonPayments.billingAgreementId
|
||||||
@@ -137,11 +154,11 @@ function($rootScope, User, $http, Content) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
authorization: function(){
|
authorization: function() {
|
||||||
amazon.Login.authorize({
|
amazon.Login.authorize({
|
||||||
scope: 'payments:widget',
|
scope: 'payments:widget',
|
||||||
popup: true
|
popup: true
|
||||||
}, function(response){
|
}, function(response) {
|
||||||
if(response.error) return alert(response.error);
|
if(response.error) return alert(response.error);
|
||||||
|
|
||||||
var url = '/amazon/verifyAccessToken'
|
var url = '/amazon/verifyAccessToken'
|
||||||
@@ -169,7 +186,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Payments.amazonPayments.initWidgets = function(){
|
Payments.amazonPayments.initWidgets = function() {
|
||||||
var walletParams = {
|
var walletParams = {
|
||||||
sellerId: window.env.AMAZON_PAYMENTS.SELLER_ID,
|
sellerId: window.env.AMAZON_PAYMENTS.SELLER_ID,
|
||||||
design: {
|
design: {
|
||||||
@@ -177,7 +194,7 @@ function($rootScope, User, $http, Content) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onPaymentSelect: function() {
|
onPaymentSelect: function() {
|
||||||
$rootScope.$apply(function(){
|
$rootScope.$apply(function() {
|
||||||
Payments.amazonPayments.paymentSelected = true;
|
Payments.amazonPayments.paymentSelected = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -185,11 +202,11 @@ function($rootScope, User, $http, Content) {
|
|||||||
onError: amazonOnError
|
onError: amazonOnError
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Payments.amazonPayments.type === 'subscription'){
|
if (Payments.amazonPayments.type === 'subscription') {
|
||||||
walletParams.agreementType = 'BillingAgreement';
|
walletParams.agreementType = 'BillingAgreement';
|
||||||
console.log(Payments.amazonPayments.billingAgreementId);
|
console.log(Payments.amazonPayments.billingAgreementId);
|
||||||
walletParams.billingAgreementId = Payments.amazonPayments.billingAgreementId;
|
walletParams.billingAgreementId = Payments.amazonPayments.billingAgreementId;
|
||||||
walletParams.onReady = function(billingAgreement){
|
walletParams.onReady = function(billingAgreement) {
|
||||||
Payments.amazonPayments.billingAgreementId = billingAgreement.getAmazonBillingAgreementId();
|
Payments.amazonPayments.billingAgreementId = billingAgreement.getAmazonBillingAgreementId();
|
||||||
|
|
||||||
new OffAmazonPayments.Widgets.Consent({
|
new OffAmazonPayments.Widgets.Consent({
|
||||||
@@ -215,14 +232,14 @@ function($rootScope, User, $http, Content) {
|
|||||||
onError: amazonOnError
|
onError: amazonOnError
|
||||||
}).bind('AmazonPayRecurring');
|
}).bind('AmazonPayRecurring');
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
walletParams.amazonOrderReferenceId = Payments.amazonPayments.orderReferenceId;
|
walletParams.amazonOrderReferenceId = Payments.amazonPayments.orderReferenceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
new OffAmazonPayments.Widgets.Wallet(walletParams).bind('AmazonPayWallet');
|
new OffAmazonPayments.Widgets.Wallet(walletParams).bind('AmazonPayWallet');
|
||||||
}
|
}
|
||||||
|
|
||||||
Payments.amazonPayments.checkout = function(){
|
Payments.amazonPayments.checkout = function() {
|
||||||
if(Payments.amazonPayments.type === 'single'){
|
if(Payments.amazonPayments.type === 'single'){
|
||||||
var url = '/amazon/checkout';
|
var url = '/amazon/checkout';
|
||||||
$http.post(url, {
|
$http.post(url, {
|
||||||
@@ -235,13 +252,14 @@ function($rootScope, User, $http, Content) {
|
|||||||
alert(res.message);
|
alert(res.message);
|
||||||
Payments.amazonPayments.reset();
|
Payments.amazonPayments.reset();
|
||||||
});
|
});
|
||||||
}else if(Payments.amazonPayments.type === 'subscription'){
|
} else if(Payments.amazonPayments.type === 'subscription') {
|
||||||
var url = '/amazon/subscribe';
|
var url = '/amazon/subscribe';
|
||||||
|
|
||||||
$http.post(url, {
|
$http.post(url, {
|
||||||
billingAgreementId: Payments.amazonPayments.billingAgreementId,
|
billingAgreementId: Payments.amazonPayments.billingAgreementId,
|
||||||
subscription: Payments.amazonPayments.subscription,
|
subscription: Payments.amazonPayments.subscription,
|
||||||
coupon: Payments.amazonPayments.coupon
|
coupon: Payments.amazonPayments.coupon,
|
||||||
|
groupId: Payments.amazonPayments.groupId,
|
||||||
}).success(function(){
|
}).success(function(){
|
||||||
Payments.amazonPayments.reset();
|
Payments.amazonPayments.reset();
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
@@ -252,20 +270,33 @@ function($rootScope, User, $http, Content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Payments.cancelSubscription = function(){
|
Payments.cancelSubscription = function(config) {
|
||||||
if (!confirm(window.env.t('sureCancelSub'))) return;
|
if (!confirm(window.env.t('sureCancelSub'))) return;
|
||||||
var paymentMethod = User.user.purchased.plan.paymentMethod;
|
|
||||||
|
|
||||||
if(paymentMethod === 'Amazon Payments'){
|
var group;
|
||||||
|
if (config.group) {
|
||||||
|
group = config.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paymentMethod = User.user.purchased.plan.paymentMethod;
|
||||||
|
if (group) {
|
||||||
|
paymentMethod = group.purchased.plan.paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentMethod === 'Amazon Payments') {
|
||||||
paymentMethod = 'amazon';
|
paymentMethod = 'amazon';
|
||||||
}else{
|
} else {
|
||||||
paymentMethod = paymentMethod.toLowerCase();
|
paymentMethod = paymentMethod.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = '/' + paymentMethod + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.settings.auth.apiToken;
|
var cancelUrl = '/' + paymentMethod + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.settings.auth.apiToken;
|
||||||
|
if (group) {
|
||||||
|
cancelUrl += '&groupId=' + group._id;
|
||||||
|
}
|
||||||
|
window.location.href = cancelUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Payments.encodeGift = function(uuid, gift){
|
Payments.encodeGift = function(uuid, gift) {
|
||||||
gift.uuid = uuid;
|
gift.uuid = uuid;
|
||||||
var encodedString = JSON.stringify(gift);
|
var encodedString = JSON.stringify(gift);
|
||||||
return encodeURIComponent(encodedString);
|
return encodeURIComponent(encodedString);
|
||||||
|
|||||||
@@ -215,5 +215,6 @@
|
|||||||
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
|
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
|
||||||
"assignTask": "Assign Task",
|
"assignTask": "Assign Task",
|
||||||
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
|
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
|
||||||
"claim": "Claim"
|
"claim": "Claim",
|
||||||
|
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2628,7 +2628,15 @@ api.subscriptionBlocks = {
|
|||||||
basic_12mo: {
|
basic_12mo: {
|
||||||
months: 12,
|
months: 12,
|
||||||
price: 48
|
price: 48
|
||||||
}
|
},
|
||||||
|
group_monthly: {
|
||||||
|
months: 1,
|
||||||
|
price: 5
|
||||||
|
},
|
||||||
|
group_yearly: {
|
||||||
|
months: 1,
|
||||||
|
price: 48,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
_.each(api.subscriptionBlocks, function(b, k) {
|
_.each(api.subscriptionBlocks, function(b, k) {
|
||||||
|
|||||||
@@ -165,12 +165,17 @@ api.getGroup = {
|
|||||||
throw new NotFound(res.t('groupNotFound'));
|
throw new NotFound(res.t('groupNotFound'));
|
||||||
}
|
}
|
||||||
|
|
||||||
group = Group.toJSONCleanChat(group, user);
|
let groupJson = Group.toJSONCleanChat(group, user);
|
||||||
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
|
||||||
let leader = await User.findById(group.leader).select(nameFields).exec();
|
|
||||||
if (leader) group.leader = leader.toJSON({minimize: true});
|
|
||||||
|
|
||||||
res.respond(200, group);
|
if (groupJson.leader === user._id) {
|
||||||
|
groupJson.purchased.plan = group.purchased.plan.toObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||||
|
let leader = await User.findById(groupJson.leader).select(nameFields).exec();
|
||||||
|
if (leader) groupJson.leader = leader.toJSON({minimize: true});
|
||||||
|
|
||||||
|
res.respond(200, groupJson);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
|
NotFound,
|
||||||
} from '../../../libs/errors';
|
} from '../../../libs/errors';
|
||||||
import amzLib from '../../../libs/amazonPayments';
|
import amzLib from '../../../libs/amazonPayments';
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +13,10 @@ import payments from '../../../libs/payments';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { model as Coupon } from '../../../models/coupon';
|
import { model as Coupon } from '../../../models/coupon';
|
||||||
import { model as User } from '../../../models/user';
|
import { model as User } from '../../../models/user';
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
basicFields as basicGroupFields,
|
||||||
|
} from '../../../models/group';
|
||||||
import cc from 'coupon-code';
|
import cc from 'coupon-code';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
@@ -34,6 +39,7 @@ api.verifyAccessToken = {
|
|||||||
if (!accessToken) throw new BadRequest('Missing req.body.access_token');
|
if (!accessToken) throw new BadRequest('Missing req.body.access_token');
|
||||||
|
|
||||||
await amzLib.getTokenInfo(accessToken);
|
await amzLib.getTokenInfo(accessToken);
|
||||||
|
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -164,6 +170,7 @@ api.subscribe = {
|
|||||||
let sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
|
let sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
|
||||||
let coupon = req.body.coupon;
|
let coupon = req.body.coupon;
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
let groupId = req.body.groupId;
|
||||||
|
|
||||||
if (!sub) throw new BadRequest(res.t('missingSubscriptionCode'));
|
if (!sub) throw new BadRequest(res.t('missingSubscriptionCode'));
|
||||||
if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId');
|
if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId');
|
||||||
@@ -213,6 +220,7 @@ api.subscribe = {
|
|||||||
paymentMethod: 'Amazon Payments',
|
paymentMethod: 'Amazon Payments',
|
||||||
sub,
|
sub,
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
|
groupId,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.respond(200);
|
res.respond(200);
|
||||||
@@ -231,7 +239,32 @@ api.subscribeCancel = {
|
|||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let billingAgreementId = user.purchased.plan.customerId;
|
let groupId = req.query.groupId;
|
||||||
|
|
||||||
|
let billingAgreementId;
|
||||||
|
let planId;
|
||||||
|
let lastBillingDate;
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
let groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFound(res.t('groupNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group.leader === user._id) {
|
||||||
|
throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
|
}
|
||||||
|
|
||||||
|
billingAgreementId = group.purchased.plan.customerId;
|
||||||
|
planId = group.purchased.plan.planId;
|
||||||
|
lastBillingDate = group.purchased.plan.lastBillingDate;
|
||||||
|
} else {
|
||||||
|
billingAgreementId = user.purchased.plan.customerId;
|
||||||
|
planId = user.purchased.plan.planId;
|
||||||
|
lastBillingDate = user.purchased.plan.lastBillingDate;
|
||||||
|
}
|
||||||
|
|
||||||
if (!billingAgreementId) throw new NotAuthorized(res.t('missingSubscription'));
|
if (!billingAgreementId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||||
|
|
||||||
@@ -245,12 +278,13 @@ api.subscribeCancel = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscriptionBlock = shared.content.subscriptionBlocks[user.purchased.plan.planId];
|
let subscriptionBlock = shared.content.subscriptionBlocks[planId];
|
||||||
let subscriptionLength = subscriptionBlock.months * 30;
|
let subscriptionLength = subscriptionBlock.months * 30;
|
||||||
|
|
||||||
await payments.cancelSubscription({
|
await payments.cancelSubscription({
|
||||||
user,
|
user,
|
||||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
groupId,
|
||||||
|
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
|
||||||
paymentMethod: 'Amazon Payments',
|
paymentMethod: 'Amazon Payments',
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import cc from 'coupon-code';
|
|||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import { model as Coupon } from '../../../models/coupon';
|
import { model as Coupon } from '../../../models/coupon';
|
||||||
import { model as User } from '../../../models/user';
|
import { model as User } from '../../../models/user';
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
basicFields as basicGroupFields,
|
||||||
|
} from '../../../models/group';
|
||||||
import {
|
import {
|
||||||
authWithUrl,
|
authWithUrl,
|
||||||
authWithSession,
|
authWithSession,
|
||||||
@@ -18,6 +22,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
|
NotFound,
|
||||||
} from '../../../libs/errors';
|
} from '../../../libs/errors';
|
||||||
|
|
||||||
const BASE_URL = nconf.get('BASE_URL');
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
@@ -178,6 +183,7 @@ api.subscribe = {
|
|||||||
let billingAgreement = await paypalBillingAgreementCreate(billingAgreementAttributes);
|
let billingAgreement = await paypalBillingAgreementCreate(billingAgreementAttributes);
|
||||||
|
|
||||||
req.session.paypalBlock = req.query.sub;
|
req.session.paypalBlock = req.query.sub;
|
||||||
|
req.session.groupId = req.query.groupId;
|
||||||
let link = _.find(billingAgreement.links, { rel: 'approval_url' }).href;
|
let link = _.find(billingAgreement.links, { rel: 'approval_url' }).href;
|
||||||
res.redirect(link);
|
res.redirect(link);
|
||||||
},
|
},
|
||||||
@@ -196,11 +202,15 @@ api.subscribeSuccess = {
|
|||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let block = shared.content.subscriptionBlocks[req.session.paypalBlock];
|
let block = shared.content.subscriptionBlocks[req.session.paypalBlock];
|
||||||
|
let groupId = req.session.groupId;
|
||||||
|
|
||||||
delete req.session.paypalBlock;
|
delete req.session.paypalBlock;
|
||||||
|
delete req.session.groupId;
|
||||||
|
|
||||||
let result = await paypalBillingAgreementExecute(req.query.token, {});
|
let result = await paypalBillingAgreementExecute(req.query.token, {});
|
||||||
await payments.createSubscription({
|
await payments.createSubscription({
|
||||||
user,
|
user,
|
||||||
|
groupId,
|
||||||
customerId: result.id,
|
customerId: result.id,
|
||||||
paymentMethod: 'Paypal',
|
paymentMethod: 'Paypal',
|
||||||
sub: block,
|
sub: block,
|
||||||
@@ -223,8 +233,26 @@ api.subscribeCancel = {
|
|||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let customerId = user.purchased.plan.customerId;
|
let groupId = req.query.groupId;
|
||||||
if (!user.purchased.plan.customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
|
||||||
|
let customerId;
|
||||||
|
if (groupId) {
|
||||||
|
let groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFound(res.t('groupNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group.leader === user._id) {
|
||||||
|
throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
|
}
|
||||||
|
customerId = group.purchased.plan.customerId;
|
||||||
|
} else {
|
||||||
|
customerId = user.purchased.plan.customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||||
|
|
||||||
let customer = await paypalBillingAgreementGet(customerId);
|
let customer = await paypalBillingAgreementGet(customerId);
|
||||||
|
|
||||||
@@ -236,6 +264,7 @@ api.subscribeCancel = {
|
|||||||
await paypalBillingAgreementCancel(customerId, { note: res.t('cancelingSubscription') });
|
await paypalBillingAgreementCancel(customerId, { note: res.t('cancelingSubscription') });
|
||||||
await payments.cancelSubscription({
|
await payments.cancelSubscription({
|
||||||
user,
|
user,
|
||||||
|
groupId,
|
||||||
paymentMethod: 'Paypal',
|
paymentMethod: 'Paypal',
|
||||||
nextBill: nextBillingDate,
|
nextBill: nextBillingDate,
|
||||||
});
|
});
|
||||||
@@ -265,6 +294,13 @@ api.ipn = {
|
|||||||
let user = await User.findOne({ 'purchased.plan.customerId': req.body.recurring_payment_id });
|
let user = await User.findOne({ 'purchased.plan.customerId': req.body.recurring_payment_id });
|
||||||
if (user) {
|
if (user) {
|
||||||
await payments.cancelSubscription({ user, paymentMethod: 'Paypal' });
|
await payments.cancelSubscription({ user, paymentMethod: 'Paypal' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
let group = await Group.findOne({ 'purchased.plan.customerId': req.body.recurring_payment_id }).select(groupFields).exec();
|
||||||
|
if (group) {
|
||||||
|
await payments.cancelSubscription({ groupId: group._id, paymentMethod: 'Paypal' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ import shared from '../../../../common';
|
|||||||
import {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
|
NotFound,
|
||||||
} from '../../../libs/errors';
|
} from '../../../libs/errors';
|
||||||
import { model as Coupon } from '../../../models/coupon';
|
import { model as Coupon } from '../../../models/coupon';
|
||||||
import payments from '../../../libs/payments';
|
import payments from '../../../libs/payments';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import { model as User } from '../../../models/user';
|
import { model as User } from '../../../models/user';
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
basicFields as basicGroupFields,
|
||||||
|
} from '../../../models/group';
|
||||||
import cc from 'coupon-code';
|
import cc from 'coupon-code';
|
||||||
import {
|
import {
|
||||||
authWithHeaders,
|
authWithHeaders,
|
||||||
@@ -41,6 +46,7 @@ api.checkout = {
|
|||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||||
let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
|
let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
|
||||||
|
let groupId = req.query.groupId;
|
||||||
let coupon;
|
let coupon;
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
@@ -84,6 +90,7 @@ api.checkout = {
|
|||||||
paymentMethod: 'Stripe',
|
paymentMethod: 'Stripe',
|
||||||
sub,
|
sub,
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
|
groupId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let method = 'buyGems';
|
let method = 'buyGems';
|
||||||
@@ -124,8 +131,26 @@ api.subscribeEdit = {
|
|||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let token = req.body.id;
|
let token = req.body.id;
|
||||||
|
let groupId = req.body.groupId;
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let customerId = user.purchased.plan.customerId;
|
let customerId;
|
||||||
|
|
||||||
|
// If we are buying a group subscription
|
||||||
|
if (groupId) {
|
||||||
|
let groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFound(res.t('groupNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group.leader === user._id) {
|
||||||
|
throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
|
}
|
||||||
|
customerId = group.purchased.plan.customerId;
|
||||||
|
} else {
|
||||||
|
customerId = user.purchased.plan.customerId;
|
||||||
|
}
|
||||||
|
|
||||||
if (!customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
if (!customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||||
if (!token) throw new BadRequest('Missing req.body.id');
|
if (!token) throw new BadRequest('Missing req.body.id');
|
||||||
@@ -150,13 +175,39 @@ api.subscribeCancel = {
|
|||||||
middlewares: [authWithUrl],
|
middlewares: [authWithUrl],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
if (!user.purchased.plan.customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
let groupId = req.query.groupId;
|
||||||
|
let customerId;
|
||||||
|
|
||||||
let customer = await stripe.customers.retrieve(user.purchased.plan.customerId);
|
if (groupId) {
|
||||||
await stripe.customers.del(user.purchased.plan.customerId);
|
let groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFound(res.t('groupNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group.leader === user._id) {
|
||||||
|
throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
|
}
|
||||||
|
customerId = group.purchased.plan.customerId;
|
||||||
|
} else {
|
||||||
|
customerId = user.purchased.plan.customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||||
|
|
||||||
|
let customer = await stripe.customers.retrieve(customerId);
|
||||||
|
|
||||||
|
let subscription = customer.subscription;
|
||||||
|
if (!subscription) {
|
||||||
|
subscription = customer.subscriptions.data[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
await stripe.customers.del(customerId);
|
||||||
await payments.cancelSubscription({
|
await payments.cancelSubscription({
|
||||||
user,
|
user,
|
||||||
nextBill: customer.subscription.current_period_end * 1000, // timestamp in seconds
|
groupId,
|
||||||
|
nextBill: subscription.current_period_end * 1000, // timestamp in seconds
|
||||||
paymentMethod: 'Stripe',
|
paymentMethod: 'Stripe',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ module.exports = {
|
|||||||
confirmOrderReference,
|
confirmOrderReference,
|
||||||
closeOrderReference,
|
closeOrderReference,
|
||||||
confirmBillingAgreement,
|
confirmBillingAgreement,
|
||||||
setBillingAgreementDetails,
|
|
||||||
getBillingAgreementDetails,
|
getBillingAgreementDetails,
|
||||||
|
setBillingAgreementDetails,
|
||||||
closeBillingAgreement,
|
closeBillingAgreement,
|
||||||
authorizeOnBillingAgreement,
|
authorizeOnBillingAgreement,
|
||||||
authorize,
|
authorize,
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import {
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { sendNotification as sendPushNotification } from './pushNotifications';
|
import { sendNotification as sendPushNotification } from './pushNotifications';
|
||||||
import shared from '../../common' ;
|
import shared from '../../common' ;
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
basicFields as basicGroupFields,
|
||||||
|
} from '../models/group';
|
||||||
|
import {
|
||||||
|
NotAuthorized,
|
||||||
|
NotFound,
|
||||||
|
} from './errors';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -32,10 +40,35 @@ function _dateDiff (earlyDate, lateDate) {
|
|||||||
|
|
||||||
api.createSubscription = async function createSubscription (data) {
|
api.createSubscription = async function createSubscription (data) {
|
||||||
let recipient = data.gift ? data.gift.member : data.user;
|
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 block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
|
||||||
let months = Number(block.months);
|
let months = Number(block.months);
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
|
let plan;
|
||||||
|
let group;
|
||||||
|
let groupId;
|
||||||
|
let itemPurchased = 'Subscription';
|
||||||
|
let purchaseType = 'subscribe';
|
||||||
|
|
||||||
|
// If we are buying a group subscription
|
||||||
|
if (data.groupId) {
|
||||||
|
let groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
group = await Group.getGroup({user: data.user, groupId: data.groupId, populateLeader: false, groupFields});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFound(shared.i18n.t('groupNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group.leader === data.user._id) {
|
||||||
|
throw new NotAuthorized(shared.i18n.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
|
}
|
||||||
|
|
||||||
|
recipient = group;
|
||||||
|
itemPurchased = 'Group-Subscription';
|
||||||
|
purchaseType = 'group-subscribe';
|
||||||
|
groupId = group._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
plan = recipient.purchased.plan;
|
||||||
|
|
||||||
if (data.gift) {
|
if (data.gift) {
|
||||||
if (plan.customerId && !plan.dateTerminated) { // User has active plan
|
if (plan.customerId && !plan.dateTerminated) { // User has active plan
|
||||||
@@ -79,7 +112,9 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
plan.consecutive.trinkets += perks;
|
plan.consecutive.trinkets += perks;
|
||||||
}
|
}
|
||||||
|
|
||||||
revealMysteryItems(recipient);
|
if (recipient !== group) {
|
||||||
|
revealMysteryItems(recipient);
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.gift) {
|
if (!data.gift) {
|
||||||
txnEmail(data.user, 'subscription-begins');
|
txnEmail(data.user, 'subscription-begins');
|
||||||
@@ -87,9 +122,10 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
|
|
||||||
analytics.trackPurchase({
|
analytics.trackPurchase({
|
||||||
uuid: data.user._id,
|
uuid: data.user._id,
|
||||||
itemPurchased: 'Subscription',
|
groupId,
|
||||||
|
itemPurchased,
|
||||||
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
|
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
|
||||||
purchaseType: 'subscribe',
|
purchaseType,
|
||||||
paymentMethod: data.paymentMethod,
|
paymentMethod: data.paymentMethod,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
gift: Boolean(data.gift),
|
gift: Boolean(data.gift),
|
||||||
@@ -97,7 +133,7 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
headers: data.headers,
|
headers: data.headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
data.user.purchased.txnCount++;
|
if (!group) data.user.purchased.txnCount++;
|
||||||
|
|
||||||
if (data.gift) {
|
if (data.gift) {
|
||||||
let message = `\`Hello ${data.gift.member.profile.name}, ${data.user.profile.name} has sent you ${shared.content.subscriptionBlocks[data.gift.subscription.key].months} months of subscription!\``;
|
let message = `\`Hello ${data.gift.member.profile.name}, ${data.user.profile.name} has sent you ${shared.content.subscriptionBlocks[data.gift.subscription.key].months} months of subscription!\``;
|
||||||
@@ -128,13 +164,39 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await data.user.save();
|
if (group) {
|
||||||
|
await group.save();
|
||||||
|
} else {
|
||||||
|
await data.user.save();
|
||||||
|
}
|
||||||
|
|
||||||
if (data.gift) await data.gift.member.save();
|
if (data.gift) await data.gift.member.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sets their subscription to be cancelled later
|
// Sets their subscription to be cancelled later
|
||||||
api.cancelSubscription = async function cancelSubscription (data) {
|
api.cancelSubscription = async function cancelSubscription (data) {
|
||||||
let plan = data.user.purchased.plan;
|
let plan;
|
||||||
|
let group;
|
||||||
|
let cancelType = 'unsubscribe';
|
||||||
|
let groupId;
|
||||||
|
|
||||||
|
// If we are buying a group subscription
|
||||||
|
if (data.groupId) {
|
||||||
|
let groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
group = await Group.getGroup({user: data.user, groupId: data.groupId, populateLeader: false, groupFields});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFound(shared.i18n.t('groupNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group.leader === data.user._id) {
|
||||||
|
throw new NotAuthorized(shared.i18n.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
|
}
|
||||||
|
plan = group.purchased.plan;
|
||||||
|
} else {
|
||||||
|
plan = data.user.purchased.plan;
|
||||||
|
}
|
||||||
|
|
||||||
let now = moment();
|
let now = moment();
|
||||||
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
|
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
|
||||||
let extraDays = Math.ceil(30.5 * plan.extraMonths);
|
let extraDays = Math.ceil(30.5 * plan.extraMonths);
|
||||||
@@ -149,12 +211,22 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
|||||||
|
|
||||||
plan.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
|
plan.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
|
||||||
|
|
||||||
await data.user.save();
|
if (group) {
|
||||||
|
await group.save();
|
||||||
|
} else {
|
||||||
|
await data.user.save();
|
||||||
|
}
|
||||||
|
|
||||||
txnEmail(data.user, 'cancel-subscription');
|
txnEmail(data.user, 'cancel-subscription');
|
||||||
|
|
||||||
analytics.track('unsubscribe', {
|
if (group) {
|
||||||
|
cancelType = 'group-unsubscribe';
|
||||||
|
groupId = group._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
analytics.track(cancelType, {
|
||||||
uuid: data.user._id,
|
uuid: data.user._id,
|
||||||
|
groupId,
|
||||||
gaCategory: 'commerce',
|
gaCategory: 'commerce',
|
||||||
gaLabel: data.paymentMethod,
|
gaLabel: data.paymentMethod,
|
||||||
paymentMethod: data.paymentMethod,
|
paymentMethod: data.paymentMethod,
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import pusher from '../libs/pusher';
|
|||||||
import {
|
import {
|
||||||
syncableAttrs,
|
syncableAttrs,
|
||||||
} from '../libs/taskManager';
|
} from '../libs/taskManager';
|
||||||
|
import {
|
||||||
|
schema as SubscriptionPlanSchema,
|
||||||
|
} from './subscriptionPlan';
|
||||||
|
|
||||||
const questScrolls = shared.content.quests;
|
const questScrolls = shared.content.quests;
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
@@ -91,7 +94,9 @@ export let schema = new Schema({
|
|||||||
rewards: [{type: String, ref: 'Task'}],
|
rewards: [{type: String, ref: 'Task'}],
|
||||||
},
|
},
|
||||||
purchased: {
|
purchased: {
|
||||||
active: {type: Boolean, default: false},
|
plan: {type: SubscriptionPlanSchema, default: () => {
|
||||||
|
return {};
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
strict: true,
|
strict: true,
|
||||||
@@ -100,6 +105,10 @@ export let schema = new Schema({
|
|||||||
|
|
||||||
schema.plugin(baseModel, {
|
schema.plugin(baseModel, {
|
||||||
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased'],
|
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased'],
|
||||||
|
private: ['purchased.plan'],
|
||||||
|
toJSONTransform (plainObj, originalDoc) {
|
||||||
|
if (plainObj.purchased) plainObj.purchased.active = originalDoc.purchased.plan && originalDoc.purchased.plan.customerId;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// A list of additional fields that cannot be updated (but can be set on creation)
|
// A list of additional fields that cannot be updated (but can be set on creation)
|
||||||
|
|||||||
32
website/server/models/subscriptionPlan.js
Normal file
32
website/server/models/subscriptionPlan.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import baseModel from '../libs/baseModel';
|
||||||
|
|
||||||
|
export let schema = new mongoose.Schema({
|
||||||
|
planId: String,
|
||||||
|
paymentMethod: String, // enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']}
|
||||||
|
customerId: String, // Billing Agreement Id in case of Amazon Payments
|
||||||
|
dateCreated: Date,
|
||||||
|
dateTerminated: Date,
|
||||||
|
dateUpdated: Date,
|
||||||
|
extraMonths: {type: Number, default: 0},
|
||||||
|
gemsBought: {type: Number, default: 0},
|
||||||
|
mysteryItems: {type: Array, default: () => []},
|
||||||
|
lastBillingDate: Date, // Used only for Amazon Payments to keep track of billing date
|
||||||
|
consecutive: {
|
||||||
|
count: {type: Number, default: 0},
|
||||||
|
offset: {type: Number, default: 0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0
|
||||||
|
gemCapExtra: {type: Number, default: 0},
|
||||||
|
trinkets: {type: Number, default: 0},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
strict: true,
|
||||||
|
minimize: false, // So empty objects are returned
|
||||||
|
_id: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.plugin(baseModel, {
|
||||||
|
noSet: ['_id'],
|
||||||
|
timestamps: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export let model = mongoose.model('SubscriptionPlan', schema);
|
||||||
@@ -8,6 +8,9 @@ import { schema as WebhookSchema } from '../webhook';
|
|||||||
import {
|
import {
|
||||||
schema as UserNotificationSchema,
|
schema as UserNotificationSchema,
|
||||||
} from '../userNotification';
|
} from '../userNotification';
|
||||||
|
import {
|
||||||
|
schema as SubscriptionPlanSchema,
|
||||||
|
} from '../subscriptionPlan';
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
@@ -144,24 +147,9 @@ let schema = new Schema({
|
|||||||
}},
|
}},
|
||||||
txnCount: {type: Number, default: 0},
|
txnCount: {type: Number, default: 0},
|
||||||
mobileChat: Boolean,
|
mobileChat: Boolean,
|
||||||
plan: {
|
plan: {type: SubscriptionPlanSchema, default: () => {
|
||||||
planId: String,
|
return {};
|
||||||
paymentMethod: String, // enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']}
|
}},
|
||||||
customerId: String, // Billing Agreement Id in case of Amazon Payments
|
|
||||||
dateCreated: Date,
|
|
||||||
dateTerminated: Date,
|
|
||||||
dateUpdated: Date,
|
|
||||||
extraMonths: {type: Number, default: 0},
|
|
||||||
gemsBought: {type: Number, default: 0},
|
|
||||||
mysteryItems: {type: Array, default: () => []},
|
|
||||||
lastBillingDate: Date, // Used only for Amazon Payments to keep track of billing date
|
|
||||||
consecutive: {
|
|
||||||
count: {type: Number, default: 0},
|
|
||||||
offset: {type: Number, default: 0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0
|
|
||||||
gemCapExtra: {type: Number, default: 0},
|
|
||||||
trinkets: {type: Number, default: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
flags: {
|
flags: {
|
||||||
|
|||||||
@@ -147,16 +147,15 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
|||||||
h3.popover-title {{group.leader.profile.name}}
|
h3.popover-title {{group.leader.profile.name}}
|
||||||
.popover-content
|
.popover-content
|
||||||
markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
|
markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
|
||||||
|
|
||||||
ul.options-menu(ng-init="groupPane = 'chat'", ng-show="group.purchased.active")
|
ul.options-menu(ng-init="groupPane = 'chat'")
|
||||||
li
|
li
|
||||||
a(ng-click="groupPane = 'chat'")
|
a(ng-click="groupPane = 'chat'")=env.t('chat')
|
||||||
| Chat
|
|
||||||
li
|
li
|
||||||
a(ng-click="groupPane = 'tasks'")
|
a(ng-click="groupPane = 'tasks'", ng-if='group.purchased.active')=env.t('tasks')
|
||||||
| Tasks
|
li
|
||||||
|
a(ng-click="groupPane = 'subscription'", ng-show='group.leader._id === user._id')=env.t('subscription')
|
||||||
|
|
||||||
.tab-content
|
.tab-content
|
||||||
.tab-pane.active
|
.tab-pane.active
|
||||||
|
|
||||||
@@ -168,5 +167,43 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
|||||||
+chatMessages()
|
+chatMessages()
|
||||||
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
|
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
|
||||||
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
|
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
|
||||||
|
|
||||||
|
group-tasks(ng-show="groupPane == 'tasks'")
|
||||||
|
|
||||||
|
//TODO: This can be a directive and the group/user can be an object passed via attribute
|
||||||
|
div(ng-show="groupPane == 'subscription'")
|
||||||
|
.col-md-12
|
||||||
|
table.table.alert.alert-info(ng-if='group.purchased.plan.customerId')
|
||||||
|
tr(ng-if='group.purchased.plan.dateTerminated'): td.alert.alert-warning
|
||||||
|
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
|
||||||
|
i.glyphicon.glyphicon-time
|
||||||
|
| #{env.t('subCanceled')} <strong>{{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
|
||||||
|
tr(ng-if='!group.purchased.plan.dateTerminated'): td
|
||||||
|
h4=env.t('subscribed')
|
||||||
|
p(ng-if='group.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[group.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[group.purchased.plan.planId].months}}', plan: '{{group.purchased.plan.paymentMethod}}'})
|
||||||
|
tr(ng-if='group.purchased.plan.extraMonths'): td
|
||||||
|
span.glyphicon.glyphicon-credit-card
|
||||||
|
| #{env.t('purchasedPlanExtraMonths', {months: '{{group.purchased.plan.extraMonths | number:2}}'})}
|
||||||
|
tr(ng-if='group.purchased.plan.consecutive.count || group.purchased.plan.consecutive.offset'): td
|
||||||
|
span.glyphicon.glyphicon-forward
|
||||||
|
| #{env.t('consecutiveSubscription')}
|
||||||
|
ul.list-unstyled
|
||||||
|
li #{env.t('consecutiveMonths')} {{group.purchased.plan.consecutive.count + group.purchased.plan.consecutive.offset}}
|
||||||
|
li #{env.t('gemCapExtra')} {{group.purchased.plan.consecutive.gemCapExtra}}
|
||||||
|
li #{env.t('mysticHourglasses')} {{group.purchased.plan.consecutive.trinkets}}
|
||||||
|
|
||||||
group-tasks(ng-show="groupPane == 'tasks'")
|
div(ng-if='group.purchased.plan.customerId')
|
||||||
|
.btn.btn-primary(ng-if='!group.purchased.plan.dateTerminated && group.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit({groupId: group.id})')=env.t('subUpdateCard')
|
||||||
|
.btn.btn-sm.btn-danger(ng-if='!group.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription({group: group})')=env.t('cancelSub')
|
||||||
|
|
||||||
|
.container-fluid.slight-vertical-padding(ng-if='!group.purchased.plan.customerId || (group.purchased.plan.customerId && group.purchased.plan.dateTerminated)', ng-init="_subscription.key='group_monthly'")
|
||||||
|
small.muted=env.t('subscribeUsing')
|
||||||
|
.row.text-center
|
||||||
|
.col-xs-4
|
||||||
|
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})', ng-disabled='!_subscription.key')= env.t('card')
|
||||||
|
.col-xs-4
|
||||||
|
a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}&groupId={{group.id}}', ng-disabled='!_subscription.key')
|
||||||
|
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
|
||||||
|
.col-xs-4
|
||||||
|
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})")
|
||||||
|
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
||||||
|
|||||||
Reference in New Issue
Block a user