mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
Group plans subs to all (#8394)
* Added subscriptions to all members when group subs * Added unsub when group cancels * Give user a subscription when they join a subbed group * Removed subscription when user leaves or is removed from group * Fixed linting issues: * Added tests for users with a subscription being upgraded to group plan * Added tests for checking if existing recurring user sub gets updated during group plan. Added better merging for plans * Added test for existing gift subscriptions * Added additional months to user when they have an existing recurring subscription and get upgraded to group sub * Adds test for user who has cancelled with date termined in the future * Added test to ensure date termined is reset * Added tests for extra months carrying over * Added test for gems bought field * Add tests to for fields that should remain when upgrading * Added test for all payment methods * Added prevention for when a user joins a second group plan * Fixed subscribing tests * Separated group plan payment tests * Added prevention of editing a user with a unlimited sub * Add tests to ensure group keeps plan if they are in two and leave one * Ensured users with two group plans do not get cancelled when on group plan is cancelled * Ensured users without group sub are untouched when group cancels * Fixed lint issues * Added new emails * Added fix for cron tests * Add restore to stubbed methods * Ensured cancelled group subscriptions are updated * Changed group plan exist check to check for date terminated * Updated you cannont delete active group message * Removed description requirement * Added upgrade group plan for Amazon payments * Fixed lint issues * Fixed broken tests * Fixed user delete tests * Fixed function calls * Hid cancel button if user has group plan * Hide difficulty from rewards * Prevented add user functions to be called when group plan is cancelled * Fixed merge issue * Correctly displayed group price * Added message when you are about to join canclled group plan * Fixed linting issues * Updated tests to have no redirect to homes * Allowed leaving a group with a canceld subscription * Fixed spelling issues * Prevented user from changing leader with active sub * Added payment details title to replace subscription title * Ensured we do not count leader when displaying upcoming cost * Prevented party tasks from being displayed twice * Prevented cancelling and already cancelled sub * Fixed styles of subscriptions * Added more specific mystery item tests * Fixed test to refer to leader * Extended test range to account for short months * Fixed merge conflicts * Updated yarn file * Added missing locales * Trigger notification * Removed yarn * Fixed locales * Fixed scope mispelling * Fixed line endings * Removed extra advanced options from rewards * Prevent group leader from leaving an active group plan * Fixed issue with extra months applied to cancelled group plan * Ensured member count is calculated when updatedGroupPlan * Updated amazon payment method constant name * Added comment to cancel sub user method * Fixed smantic issues * Added unite test for user isSubscribed and hasNotCancelled * Add tests for isSubscribed and hasNotCanceled * Changed default days remaining to 2 days for group plans * Fixed logic with adding canceled notice to group invite
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
@@ -12,7 +13,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
let groupName = 'Test Public Guild';
|
||||
|
||||
beforeEach(async () => {
|
||||
inviter = await generateUser({balance: 1});
|
||||
inviter = await generateUser({balance: 4});
|
||||
group = await inviter.post('/groups', {
|
||||
name: groupName,
|
||||
type: 'guild',
|
||||
@@ -265,6 +266,25 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
expect(invitedUser.invitations.guilds[0].id).to.equal(group._id);
|
||||
expect(invite).to.exist;
|
||||
});
|
||||
|
||||
it('invites marks invite with cancelled plan', async () => {
|
||||
let cancelledPlanGroup = await generateGroup(inviter, {
|
||||
type: 'guild',
|
||||
name: generateUUID(),
|
||||
});
|
||||
await cancelledPlanGroup.createCancelledSubscription();
|
||||
|
||||
let newUser = await generateUser();
|
||||
let invite = await inviter.post(`/groups/${cancelledPlanGroup._id}/invite`, {
|
||||
uuids: [newUser._id],
|
||||
emails: [{name: 'test', email: 'test@habitica.com'}],
|
||||
});
|
||||
let invitedUser = await newUser.get('/user');
|
||||
|
||||
expect(invitedUser.invitations.guilds[0].id).to.equal(cancelledPlanGroup._id);
|
||||
expect(invitedUser.invitations.guilds[0].cancelledPlan).to.be.true;
|
||||
expect(invite).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('guild invites', () => {
|
||||
|
||||
@@ -43,4 +43,15 @@ describe('PUT /group', () => {
|
||||
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
|
||||
expect(updatedGroup.name).to.equal(groupUpdatedName);
|
||||
});
|
||||
|
||||
it('allows a leader to change leaders', async () => {
|
||||
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
leader: nonLeader._id,
|
||||
});
|
||||
|
||||
expect(updatedGroup.leader._id).to.eql(nonLeader._id);
|
||||
expect(updatedGroup.leader.profile.name).to.eql(nonLeader.profile.name);
|
||||
expect(updatedGroup.name).to.equal(groupUpdatedName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as Coupon } from '../../../../../website/server/models/coupon';
|
||||
import amzLib from '../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
@@ -105,7 +107,7 @@ describe('Amazon Payments', () => {
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
@@ -128,7 +130,7 @@ describe('Amazon Payments', () => {
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
@@ -153,7 +155,7 @@ describe('Amazon Payments', () => {
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
@@ -316,7 +318,7 @@ describe('Amazon Payments', () => {
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
@@ -375,7 +377,7 @@ describe('Amazon Payments', () => {
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
@@ -455,7 +457,7 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
@@ -485,7 +487,7 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
@@ -523,7 +525,7 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
@@ -553,10 +555,84 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy, data, user, group, uuidString;
|
||||
|
||||
beforeEach(async function () {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
spy.returnsPromise().resolves([]);
|
||||
|
||||
uuidString = 'uuid-v4';
|
||||
sinon.stub(uuid, 'v4').returns(uuidString);
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(amzLib.authorizeOnBillingAgreement);
|
||||
uuid.v4.restore();
|
||||
});
|
||||
|
||||
it('charges for a new member', async () => {
|
||||
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
|
||||
AuthorizationReferenceId: uuidString.substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: 3,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: uuidString,
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,7 +134,10 @@ describe('cron', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
||||
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import * as sender from '../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../website/server/libs/payments';
|
||||
import analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import stripeModule from 'stripe';
|
||||
import moment from 'moment';
|
||||
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user, group, data, plan;
|
||||
|
||||
let stripe = stripeModule('test');
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
@@ -319,53 +315,6 @@ 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', () => {
|
||||
it('adds block months to plan.consecutive.offset', async () => {
|
||||
await api.createSubscription(data);
|
||||
@@ -558,112 +507,6 @@ describe('payments/index', () => {
|
||||
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
||||
});
|
||||
});
|
||||
|
||||
context('Canceling a subscription for group', () => {
|
||||
it('adds a month termination date by default', async () => {
|
||||
data.groupId = group._id;
|
||||
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(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
group.purchased.plan.extraMonths = 2;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
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(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||
});
|
||||
|
||||
it('handles extra month fractions', async () => {
|
||||
group.purchased.plan.extraMonths = 0.3;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
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(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 });
|
||||
data.groupId = group._id;
|
||||
|
||||
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(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
group.purchased.plan.extraMonths = 5;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
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, 'group-cancel-subscription');
|
||||
});
|
||||
|
||||
it('prevents non group leader from manging subscription', async () => {
|
||||
let groupMember = new User();
|
||||
data.user = groupMember;
|
||||
data.groupId = group._id;
|
||||
|
||||
await expect(api.cancelSubscription(data))
|
||||
.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows old group leader to cancel if they created the subscription', async () => {
|
||||
data.groupId = group._id;
|
||||
data.sub = {
|
||||
key: 'group_monthly',
|
||||
};
|
||||
data.paymentMethod = 'Payment Method';
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let newLeader = new User();
|
||||
updatedGroup.leader = newLeader._id;
|
||||
await updatedGroup.save();
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#buyGems', () => {
|
||||
@@ -771,49 +614,24 @@ describe('payments/index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy;
|
||||
|
||||
beforeEach(function () {
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves([]);
|
||||
describe('addSubToGroupUser', () => {
|
||||
it('adds a group subscription to a new user', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
});
|
||||
await api.addSubToGroupUser(user, group);
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await api.createSubscription(data);
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await api.updateStripeGroupPlan(updatedGroup, stripe);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||
});
|
||||
|
||||
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await api.updateStripeGroupPlan(updatedGroup, stripe);
|
||||
|
||||
expect(spy.calledOnce).to.be.false;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import * as sender from '../../../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../../../website/server/libs/payments';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import i18n from '../../../../../../../website/common/script/i18n';
|
||||
|
||||
describe('Canceling a subscription for group', () => {
|
||||
let plan, group, user, data;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
plan = {
|
||||
planId: 'basic_3mo',
|
||||
customerId: 'customer-id',
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: 'paymentMethod',
|
||||
extraMonths: 0,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: new Date(),
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sender.sendTxn.restore();
|
||||
});
|
||||
|
||||
it('adds a month termination date by default', async () => {
|
||||
data.groupId = group._id;
|
||||
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(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
group.purchased.plan.extraMonths = 2;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
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(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||
});
|
||||
|
||||
it('handles extra month fractions', async () => {
|
||||
group.purchased.plan.extraMonths = 0.3;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
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(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 });
|
||||
data.groupId = group._id;
|
||||
|
||||
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(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
group.purchased.plan.extraMonths = 5;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
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.firstCall.args[0]._id).to.equal(user._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-cancel-subscription');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents non group leader from manging subscription', async () => {
|
||||
let groupMember = new User();
|
||||
data.user = groupMember;
|
||||
data.groupId = group._id;
|
||||
|
||||
await expect(api.cancelSubscription(data))
|
||||
.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows old group leader to cancel if they created the subscription', async () => {
|
||||
data.groupId = group._id;
|
||||
data.sub = {
|
||||
key: 'group_monthly',
|
||||
};
|
||||
data.paymentMethod = 'Payment Method';
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let newLeader = new User();
|
||||
updatedGroup.leader = newLeader._id;
|
||||
await updatedGroup.save();
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
|
||||
it('cancels member subscriptions', async () => {
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
await api.createSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
let updatedLeader = await User.findById(user._id).exec();
|
||||
let daysTillTermination = moment(updatedLeader.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
expect(daysTillTermination).to.be.within(2, 3); // only a few days
|
||||
});
|
||||
|
||||
it('sends an email to members of group', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.have.callCount(4);
|
||||
expect(sender.sendTxn.thirdCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.thirdCall.args[1]).to.equal('group-member-cancel');
|
||||
expect(sender.sendTxn.thirdCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not cancel member subscriptions when member does not have a group plan sub (i.e. UNLIMITED_CUSTOMER_ID)', async () => {
|
||||
plan.key = 'basic_earned';
|
||||
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let updatedLeader = await User.findById(user._id).exec();
|
||||
expect(updatedLeader.purchased.plan.dateTerminated).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not cancel a user subscription if they are still in another active group plan', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
|
||||
it('does cancel a leader subscription with two cancelled group plans', async () => {
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
user.guilds.push(group2._id);
|
||||
await user.save();
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
data.groupId = group._id;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.exist;
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,634 @@
|
||||
import moment from 'moment';
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import * as sender from '../../../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../../../website/server/libs/payments';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('Purchasing a subscription for group', () => {
|
||||
let plan, group, user, data;
|
||||
let stripe = stripeModule('test');
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
plan = {
|
||||
planId: 'basic_3mo',
|
||||
customerId: 'customer-id',
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: 'paymentMethod',
|
||||
extraMonths: 0,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: new Date(),
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
|
||||
let subscriptionId = 'subId';
|
||||
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
|
||||
let currentPeriodEndTimeStamp = moment().add(3, 'months').unix();
|
||||
sinon.stub(stripe.customers, 'retrieve')
|
||||
.returnsPromise().resolves({
|
||||
subscriptions: {
|
||||
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
stripePayments.setStripeApi(stripe);
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
stripe.customers.retrieve.restore();
|
||||
sender.sendTxn.restore();
|
||||
});
|
||||
|
||||
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('sends an email', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledWith(user, 'group-subscription-begins');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('grants all members of a group a subscription', async () => {
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedLeader = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedLeader.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedLeader.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedLeader.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedLeader.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedLeader.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedLeader.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedLeader.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedLeader.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedLeader.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('sends an email to members of group', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds months to members with existing gift subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = 'paymentMethod';
|
||||
data.gift = {
|
||||
member: recipient,
|
||||
subscription: {
|
||||
key: 'basic_earned',
|
||||
months: 1,
|
||||
},
|
||||
};
|
||||
await api.createSubscription(data);
|
||||
await recipient.save();
|
||||
|
||||
data.gift = undefined;
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(1, 3);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds months to members with existing multi-month gift subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
data.gift = {
|
||||
member: recipient,
|
||||
subscription: {
|
||||
key: 'basic_3mo',
|
||||
months: 3,
|
||||
},
|
||||
};
|
||||
await api.createSubscription(data);
|
||||
await recipient.save();
|
||||
|
||||
data.gift = undefined;
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Stripe)', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Amazon)', async () => {
|
||||
sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
plan.lastBillingDate = moment().add(3, 'months');
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Paypal)', async () => {
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: { // eslint-disable-line camelcase
|
||||
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
|
||||
cycles_completed: 1, // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Android)');
|
||||
it('adds months to members with existing recurring subscription (iOs)');
|
||||
|
||||
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await recipient.cancelSubscription();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
});
|
||||
|
||||
it('adds months to members who already cancelled but not yet terminated group plan subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = api.constants.GROUP_PLAN_PAYMENT_METHOD;
|
||||
plan.extraMonths = 2.94;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await recipient.cancelSubscription();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
|
||||
});
|
||||
|
||||
it('resets date terminated if user has old subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.not.exist;
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription and includes existing extraMonths', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
plan.extraMonths = 5;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(7, 8);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription and ignores existing negative extraMonths', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
plan.extraMonths = -5;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
});
|
||||
|
||||
it('does not override gemsBought, mysteryItems, dateCreated, and consective fields', async () => {
|
||||
let planCreatedDate = moment().toDate();
|
||||
let mysteryItem = {title: 'item'};
|
||||
let mysteryItems = [mysteryItem];
|
||||
let consecutive = {
|
||||
trinkets: 3,
|
||||
gemCapExtra: 20,
|
||||
offset: 1,
|
||||
count: 13,
|
||||
};
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
|
||||
plan.key = 'basic_earned';
|
||||
plan.gemsBought = 3;
|
||||
plan.dateCreated = planCreatedDate;
|
||||
plan.mysteryItems = mysteryItems;
|
||||
plan.consecutive = consecutive;
|
||||
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.equal(3);
|
||||
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
|
||||
expect(updatedUser.purchased.plan.consecutive.count).to.equal(consecutive.count);
|
||||
expect(updatedUser.purchased.plan.consecutive.offset).to.equal(consecutive.offset);
|
||||
expect(updatedUser.purchased.plan.consecutive.gemCapExtra).to.equal(consecutive.gemCapExtra);
|
||||
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(planCreatedDate);
|
||||
});
|
||||
|
||||
it('does not modify a user with a group subscription when they join another group', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
|
||||
it('does not remove a user who is in two groups plans and leaves one', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
await updatedGroup.leave(recipient);
|
||||
|
||||
updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
|
||||
it('does not modify a user with an unlimited subscription', async () => {
|
||||
plan.key = 'basic_earned';
|
||||
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql(api.constants.UNLIMITED_CUSTOMER_ID);
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('paymentMethod');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('updates a user with a cancelled but active group subscription', async () => {
|
||||
plan.key = 'basic_earned';
|
||||
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||
plan.dateTerminated = moment().add(1, 'months');
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql(api.constants.GROUP_PLAN_CUSTOMER_ID);
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(0, 2);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as Coupon } from '../../../../../website/server/models/coupon';
|
||||
import stripePayments from '../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
@@ -658,4 +659,60 @@ describe('Stripe Payments', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy, data, user, group;
|
||||
|
||||
beforeEach(async function () {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves([]);
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
stripePayments.setStripeApi(stripe);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
});
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await stripePayments.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import validator from 'validator';
|
||||
import { sleep } from '../../../../helpers/api-unit.helper';
|
||||
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
@@ -7,9 +10,7 @@ import {
|
||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import validator from 'validator';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import shared from '../../../../../website/common';
|
||||
|
||||
describe('Group Model', () => {
|
||||
@@ -667,6 +668,49 @@ describe('Group Model', () => {
|
||||
expect(party.memberCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('does not allow a leader to leave a group with an active subscription', async () => {
|
||||
party.memberCount = 2;
|
||||
party.purchased.plan.customerId = '110002222333';
|
||||
|
||||
await expect(party.leave(questLeader))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
name: 'NotAuthorized',
|
||||
httpCode: 401,
|
||||
message: shared.i18n.t('leaderCannotLeaveGroupWithActiveGroup'),
|
||||
});
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.exist;
|
||||
expect(party.memberCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('deletes a private group when the last member leaves and a subscription is cancelled', async () => {
|
||||
let guild = new Group({
|
||||
name: 'test guild',
|
||||
type: 'guild',
|
||||
memberCount: 1,
|
||||
});
|
||||
|
||||
let leader = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
guild.leader = leader._id;
|
||||
|
||||
await Promise.all([
|
||||
guild.save(),
|
||||
leader.save(),
|
||||
]);
|
||||
|
||||
guild.purchased.plan.customerId = '110002222333';
|
||||
guild.purchased.plan.dateTerminated = new Date();
|
||||
|
||||
await guild.leave(leader);
|
||||
|
||||
party = await Group.findOne({_id: guild._id});
|
||||
expect(party).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not delete a public group when the last member leaves', async () => {
|
||||
party.privacy = 'public';
|
||||
|
||||
@@ -1545,5 +1589,57 @@ describe('Group Model', () => {
|
||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('isSubscribed', () => {
|
||||
it('returns false if group does not have customer id', () => {
|
||||
expect(party.isSubscribed()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('returns true if group does not have plan.dateTerminated', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(party.isSubscribed()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns true if group if plan.dateTerminated is after today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(party.isSubscribed()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if group if plan.dateTerminated is before today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(party.isSubscribed()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
context('hasNotCancelled', () => {
|
||||
it('returns false if group does not have customer id', () => {
|
||||
expect(party.hasNotCancelled()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('returns true if party does not have plan.dateTerminated', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(party.hasNotCancelled()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if party if plan.dateTerminated is after today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(party.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if party if plan.dateTerminated is before today', () => {
|
||||
party.purchased.plan.customerId = 'test-id';
|
||||
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(party.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Bluebird from 'bluebird';
|
||||
import moment from 'moment';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
describe('User Model', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
@@ -145,4 +146,68 @@ describe('User Model', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('isSubscribed', () => {
|
||||
let user;
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
|
||||
it('returns false if user does not have customer id', () => {
|
||||
expect(user.isSubscribed()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('returns true if user does not have plan.dateTerminated', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(user.isSubscribed()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns true if user if plan.dateTerminated is after today', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(user.isSubscribed()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if user if plan.dateTerminated is before today', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(user.isSubscribed()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
context('hasNotCancelled', () => {
|
||||
let user;
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
|
||||
it('returns false if user does not have customer id', () => {
|
||||
expect(user.hasNotCancelled()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('returns true if user does not have plan.dateTerminated', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
|
||||
expect(user.hasNotCancelled()).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if user if plan.dateTerminated is after today', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
|
||||
|
||||
expect(user.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if user if plan.dateTerminated is before today', () => {
|
||||
user.purchased.plan.customerId = 'test-id';
|
||||
user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
expect(user.hasNotCancelled()).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
|
||||
import moment from 'moment';
|
||||
import { requester } from './requester';
|
||||
import {
|
||||
getDocument as getDocumentFromMongo,
|
||||
@@ -82,6 +82,19 @@ export class ApiGroup extends ApiObject {
|
||||
|
||||
return await this.update(update);
|
||||
}
|
||||
|
||||
async createCancelledSubscription () {
|
||||
let update = {
|
||||
purchased: {
|
||||
plan: {
|
||||
customerId: 'example-customer',
|
||||
dateTerminated: moment().add(1, 'days').toDate(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return await this.update(update);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiChallenge extends ApiObject {
|
||||
|
||||
@@ -28,7 +28,7 @@ angular.module('habitrpg')
|
||||
};
|
||||
|
||||
$scope.newGroupIsReady = function () {
|
||||
return $scope.newGroup.name && $scope.newGroup.description;
|
||||
return !!$scope.newGroup.name;
|
||||
};
|
||||
|
||||
$scope.createGroup = function () {
|
||||
|
||||
@@ -40,6 +40,10 @@ habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$r
|
||||
}
|
||||
|
||||
$scope.join = function (group) {
|
||||
if (group.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
var groupId = group._id;
|
||||
|
||||
// If we don't have the _id property, we are joining from an invitation
|
||||
|
||||
@@ -65,6 +65,12 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
|
||||
})
|
||||
.then(function (response) {
|
||||
var tasks = response.data.data;
|
||||
|
||||
$scope.group['habits'] = [];
|
||||
$scope.group['dailys'] = [];
|
||||
$scope.group['todos'] = [];
|
||||
$scope.group['rewards'] = [];
|
||||
|
||||
tasks.forEach(function (element, index, array) {
|
||||
if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
|
||||
$scope.group[element.type + 's'].unshift(element);
|
||||
@@ -119,6 +125,10 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
|
||||
};
|
||||
|
||||
$scope.join = function (party) {
|
||||
if (party.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
Groups.Group.join(party.id)
|
||||
.then(function (response) {
|
||||
$rootScope.party = $scope.group = response.data.data;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
// @TODO: The use of scope with tasks is incorrect. We need to fix all task ctrls to use directives/services
|
||||
// $scope.obj = {};
|
||||
function setObj (obj, force) {
|
||||
if (!force && ($scope.obj || scope.obj !== {} || !obj)) return;
|
||||
if (!force && ($scope.obj || $scope.obj !== {} || !obj)) return;
|
||||
$scope.obj = obj;
|
||||
setUpGroupedList();
|
||||
setUpTaskWatch();
|
||||
|
||||
@@ -22,10 +22,10 @@ function($rootScope, User, $http, Content) {
|
||||
|
||||
sub = sub && Content.subscriptionBlocks[sub];
|
||||
|
||||
var amount = // 500 = $5
|
||||
sub ? sub.price*100
|
||||
: data.gift && data.gift.type=='gems' ? data.gift.gems.amount/4*100
|
||||
: 500;
|
||||
var amount = 500;// 500 = $5
|
||||
if (sub) amount = sub.price * 100;
|
||||
if (data.gift && data.gift.type=='gems') amount = data.gift.gems.amount / 4 * 100;
|
||||
if (data.group) amount = (sub.price + 3 * (data.group.memberCount - 1)) * 100;
|
||||
|
||||
StripeCheckout.open({
|
||||
key: window.env.STRIPE_PUB_KEY,
|
||||
|
||||
@@ -254,5 +254,9 @@
|
||||
"cantDeleteAssignedGroupTasks": "Can't delete group tasks that are assigned to you.",
|
||||
"confirmGuildPlanCreation": "Create this group?",
|
||||
"onlyGroupLeaderCanInviteToGroupPlan": "Only the group leader can invite users to a group with a subscription.",
|
||||
"remainOrLeaveChallenges": "req.query.keep must be either 'remain-in-challenges' or 'leave-challenges'"
|
||||
"remainOrLeaveChallenges": "req.query.keep must be either 'remain-in-challenges' or 'leave-challenges'",
|
||||
"paymentDetails": "Payment Details",
|
||||
"aboutToJoinCancelledGroupPlan": "You are about to join a group with a canceled plan. You will NOT receive a free subscription.",
|
||||
"cannotChangeLeaderWithActiveGroupPlan": "You can not change the leader while the group has an active plan.",
|
||||
"leaderCannotLeaveGroupWithActiveGroup": "A leader can not leave a group while the group has an active plan"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ let subscriptionBlocks = {
|
||||
price: 9,
|
||||
quantity: 3, // Default quantity for Stripe - The same as having 3 user subscriptions
|
||||
},
|
||||
group_plan_auto: {
|
||||
type: 'group',
|
||||
months: 0,
|
||||
price: 0,
|
||||
quantity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
each(subscriptionBlocks, function createKeys (b, k) {
|
||||
|
||||
@@ -387,10 +387,13 @@ api.updateGroup = {
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
|
||||
|
||||
if (req.body.leader !== user._id && group.hasNotCancelled()) throw new NotAuthorized(res.t('cannotChangeLeaderWithActiveGroupPlan'));
|
||||
|
||||
_.assign(group, _.merge(group.toObject(), Group.sanitizeUpdate(req.body)));
|
||||
|
||||
let savedGroup = await group.save();
|
||||
let response = Group.toJSONCleanChat(savedGroup, user);
|
||||
|
||||
// If the leader changed fetch new data, otherwise use authenticated user
|
||||
if (response.leader !== user._id) {
|
||||
let rawLeader = await User.findById(response.leader).select(nameFields).exec();
|
||||
@@ -493,7 +496,10 @@ api.joinGroup = {
|
||||
|
||||
group.memberCount += 1;
|
||||
|
||||
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||
if (group.hasNotCancelled()) {
|
||||
await payments.addSubToGroupUser(user, group);
|
||||
await group.updateGroupPlan();
|
||||
}
|
||||
|
||||
let promises = [group.save(), user.save()];
|
||||
|
||||
@@ -676,7 +682,7 @@ api.leaveGroup = {
|
||||
|
||||
await group.leave(user, req.query.keep, req.body.keepChallenges);
|
||||
|
||||
if (group.purchased.plan && group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
|
||||
|
||||
_removeMessagesFromMember(user, group._id);
|
||||
|
||||
@@ -763,7 +769,10 @@ api.removeGroupMember = {
|
||||
|
||||
if (isInGroup) {
|
||||
group.memberCount -= 1;
|
||||
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||
if (group.hasNotCancelled()) {
|
||||
await group.updateGroupPlan(true);
|
||||
await payments.cancelGroupSubscriptionForUser(member, group);
|
||||
}
|
||||
|
||||
if (group.quest && group.quest.leader === member._id) {
|
||||
group.quest.key = undefined;
|
||||
@@ -831,7 +840,10 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
|
||||
if (_.find(userToInvite.invitations.guilds, {id: group._id})) {
|
||||
throw new NotAuthorized(res.t('userAlreadyInvitedToGroup'));
|
||||
}
|
||||
userToInvite.invitations.guilds.push({id: group._id, name: group.name, inviter: inviter._id});
|
||||
|
||||
let guildInvite = {id: group._id, name: group.name, inviter: inviter._id};
|
||||
if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true;
|
||||
userToInvite.invitations.guilds.push(guildInvite);
|
||||
} else if (group.type === 'party') {
|
||||
if (userToInvite.invitations.party.id) {
|
||||
throw new NotAuthorized(res.t('userAlreadyPendingInvitation'));
|
||||
@@ -844,7 +856,9 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
|
||||
if (userParty && userParty.memberCount !== 1) throw new NotAuthorized(res.t('userAlreadyInAParty'));
|
||||
}
|
||||
|
||||
userToInvite.invitations.party = {id: group._id, name: group.name, inviter: inviter._id};
|
||||
let partyInvite = {id: group._id, name: group.name, inviter: inviter._id};
|
||||
if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true;
|
||||
userToInvite.invitations.party = partyInvite;
|
||||
}
|
||||
|
||||
let groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
|
||||
@@ -907,10 +921,15 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
|
||||
userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res);
|
||||
} else {
|
||||
userReturnInfo = invite.email;
|
||||
|
||||
let cancelledPlan = false;
|
||||
if (group.isSubscribed() && !group.hasNotCancelled()) cancelledPlan = true;
|
||||
|
||||
const groupQueryString = JSON.stringify({
|
||||
id: group._id,
|
||||
inviter: inviter._id,
|
||||
sentAt: Date.now(), // so we can let it expire
|
||||
cancelledPlan,
|
||||
});
|
||||
let link = `/static/front?groupInvite=${encrypt(groupQueryString)}`;
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ api.deleteUser = {
|
||||
}
|
||||
|
||||
let types = ['party', 'guilds'];
|
||||
let groupFields = basicGroupFields.concat(' leader memberCount');
|
||||
let groupFields = basicGroupFields.concat(' leader memberCount purchased');
|
||||
|
||||
let groupsUserIsMemberOf = await Group.getGroups({user, types, groupFields});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import nconf from 'nconf';
|
||||
import Bluebird from 'bluebird';
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import common from '../../common';
|
||||
import {
|
||||
@@ -38,6 +39,7 @@ api.constants = {
|
||||
SELLER_NOTE: 'Habitica Payment',
|
||||
SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
|
||||
SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
|
||||
SELLER_NOTE_GROUP_NEW_MEMBER: 'Habitica Group Plan New Member',
|
||||
STORE_NAME: 'Habitica',
|
||||
|
||||
GIFT_TYPE_GEMS: 'gems',
|
||||
@@ -45,8 +47,8 @@ api.constants = {
|
||||
|
||||
METHOD_BUY_GEMS: 'buyGems',
|
||||
METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
|
||||
PAYMENT_METHOD_AMAZON: 'Amazon Payments',
|
||||
PAYMENT_METHOD_AMAZON_GIFT: 'Amazon Payments (Gift)',
|
||||
PAYMENT_METHOD: 'Amazon Payments',
|
||||
PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
|
||||
};
|
||||
|
||||
api.getTokenInfo = Bluebird.promisify(amzPayment.api.getTokenInfo, {context: amzPayment.api});
|
||||
@@ -138,7 +140,7 @@ api.checkout = async function checkout (options = {}) {
|
||||
|
||||
let data = {
|
||||
user,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
};
|
||||
|
||||
@@ -146,7 +148,7 @@ api.checkout = async function checkout (options = {}) {
|
||||
if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) method = this.constants.METHOD_CREATE_SUBSCRIPTION;
|
||||
gift.member = await User.findById(gift ? gift.uuid : undefined).exec();
|
||||
data.gift = gift;
|
||||
data.paymentMethod = this.constants.PAYMENT_METHOD_AMAZON_GIFT;
|
||||
data.paymentMethod = this.constants.PAYMENT_METHOD_GIFT;
|
||||
}
|
||||
|
||||
await payments[method](data);
|
||||
@@ -209,7 +211,7 @@ api.cancelSubscription = async function cancelSubscription (options = {}) {
|
||||
user,
|
||||
groupId,
|
||||
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
@@ -281,11 +283,36 @@ api.subscribe = async function subscribe (options) {
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) {
|
||||
// @TODO: Can we get this from the content plan?
|
||||
let priceForNewMember = 3;
|
||||
|
||||
// @TODO: Prorate?
|
||||
|
||||
return this.authorizeOnBillingAgreement({
|
||||
AmazonBillingAgreementId: group.purchased.plan.customerId,
|
||||
AuthorizationReferenceId: uuid.v4().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: this.constants.CURRENCY_CODE,
|
||||
Amount: priceForNewMember,
|
||||
},
|
||||
SellerAuthorizationNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: uuid.v4(),
|
||||
StoreName: this.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -23,7 +23,9 @@ const PLATFORM_MAP = Object.freeze({
|
||||
'habitica-android': 'Android',
|
||||
});
|
||||
|
||||
let amplitude = new Amplitude(AMPLIUDE_TOKEN);
|
||||
let amplitude;
|
||||
if (AMPLIUDE_TOKEN) amplitude = new Amplitude(AMPLIUDE_TOKEN);
|
||||
|
||||
let ga = googleAnalytics(GA_TOKEN);
|
||||
|
||||
let _lookUpItemName = (itemKey) => {
|
||||
|
||||
@@ -75,24 +75,21 @@ function grantEndOfTheMonthPerks (user, now) {
|
||||
}
|
||||
|
||||
function removeTerminatedSubscription (user) {
|
||||
// If subscription's termination date has arrived
|
||||
let plan = user.purchased.plan;
|
||||
|
||||
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date())) {
|
||||
_.merge(plan, {
|
||||
planId: null,
|
||||
customerId: null,
|
||||
paymentMethod: null,
|
||||
});
|
||||
_.merge(plan, {
|
||||
planId: null,
|
||||
customerId: null,
|
||||
paymentMethod: null,
|
||||
});
|
||||
|
||||
_.merge(plan.consecutive, {
|
||||
count: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
});
|
||||
_.merge(plan.consecutive, {
|
||||
count: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
});
|
||||
|
||||
user.markModified('purchased.plan');
|
||||
}
|
||||
user.markModified('purchased.plan');
|
||||
}
|
||||
|
||||
function performSleepTasks (user, tasksByType, now) {
|
||||
@@ -195,11 +192,15 @@ export function cron (options = {}) {
|
||||
if (user.purchased && user.purchased.plan && !moment(user.purchased.plan.dateUpdated).startOf('month').isSame(moment().startOf('month'))) {
|
||||
user.purchased.plan.gemsBought = 0;
|
||||
}
|
||||
|
||||
if (user.isSubscribed()) {
|
||||
grantEndOfTheMonthPerks(user, now);
|
||||
if (!CRON_SAFE_MODE) removeTerminatedSubscription(user);
|
||||
}
|
||||
|
||||
let plan = user.purchased.plan;
|
||||
let userHasTerminatedSubscription = plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date());
|
||||
if (!CRON_SAFE_MODE && userHasTerminatedSubscription) removeTerminatedSubscription(user);
|
||||
|
||||
// Login Incentives
|
||||
user.loginIncentives++;
|
||||
awardLoginIncentives(user);
|
||||
|
||||
@@ -11,19 +11,21 @@ import {
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../models/group';
|
||||
import { model as User } from '../models/user';
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from './errors';
|
||||
import slack from './slack';
|
||||
import nconf from 'nconf';
|
||||
|
||||
import stripeModule from 'stripe';
|
||||
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||
|
||||
|
||||
let api = {};
|
||||
|
||||
api.constants = {
|
||||
UNLIMITED_CUSTOMER_ID: 'habitrpg', // Users with the customerId have an unlimted free subscription
|
||||
GROUP_PLAN_CUSTOMER_ID: 'group-plan',
|
||||
GROUP_PLAN_PAYMENT_METHOD: 'Group Plan',
|
||||
};
|
||||
|
||||
function revealMysteryItems (user) {
|
||||
_.each(shared.content.gear.flat, function findMysteryItems (item) {
|
||||
if (
|
||||
@@ -44,6 +46,158 @@ function _dateDiff (earlyDate, lateDate) {
|
||||
return moment(lateDate).diff(earlyDate, 'months', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subscription to members of a group
|
||||
*
|
||||
* @param group The Group Model that is subscribed to a group plan
|
||||
*
|
||||
* @return undefined
|
||||
*/
|
||||
api.addSubscriptionToGroupUsers = async function addSubscriptionToGroupUsers (group) {
|
||||
let members;
|
||||
if (group.type === 'guild') {
|
||||
members = await User.find({guilds: group._id}).select('_id purchased').exec();
|
||||
} else {
|
||||
members = await User.find({'party._id': group._id}).select('_id purchased').exec();
|
||||
}
|
||||
|
||||
let promises = members.map((member) => {
|
||||
return this.addSubToGroupUser(member, group);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a subscription to a new member of a group
|
||||
*
|
||||
* @param member The new member of the group
|
||||
*
|
||||
* @return undefined
|
||||
*/
|
||||
api.addSubToGroupUser = async function addSubToGroupUser (member, group) {
|
||||
let customerIdsToIgnore = [this.constants.GROUP_PLAN_CUSTOMER_ID, this.constants.UNLIMITED_CUSTOMER_ID];
|
||||
|
||||
let data = {
|
||||
user: {},
|
||||
sub: {
|
||||
key: 'group_plan_auto',
|
||||
},
|
||||
customerId: 'group-plan',
|
||||
paymentMethod: 'Group Plan',
|
||||
headers: {},
|
||||
};
|
||||
|
||||
let plan = {
|
||||
planId: 'group_plan_auto',
|
||||
customerId: 'group-plan',
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: 'groupPlan',
|
||||
extraMonths: 0,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: null,
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
|
||||
if (member.isSubscribed()) {
|
||||
let memberPlan = member.purchased.plan;
|
||||
let customerHasCancelledGroupPlan = memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID && !member.hasNotCancelled();
|
||||
if (customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1 && !customerHasCancelledGroupPlan) return;
|
||||
|
||||
if (member.hasNotCancelled()) await member.cancelSubscription();
|
||||
|
||||
let today = new Date();
|
||||
plan = _.clone(member.purchased.plan.toObject());
|
||||
let extraMonths = Number(plan.extraMonths);
|
||||
if (plan.dateTerminated) extraMonths += _dateDiff(today, plan.dateTerminated);
|
||||
|
||||
_(plan).merge({ // override with these values
|
||||
planId: 'group_plan_auto',
|
||||
customerId: 'group-plan',
|
||||
dateUpdated: today,
|
||||
paymentMethod: 'groupPlan',
|
||||
extraMonths,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: null,
|
||||
owner: member._id,
|
||||
}).defaults({ // allow non-override if a plan was previously used
|
||||
gemsBought: 0,
|
||||
dateCreated: today,
|
||||
mysteryItems: [],
|
||||
}).value();
|
||||
}
|
||||
|
||||
member.purchased.plan = plan;
|
||||
|
||||
data.user = member;
|
||||
await this.createSubscription(data);
|
||||
|
||||
let leader = await User.findById(group.leader).exec();
|
||||
txnEmail(data.user, 'group-member-joining', [
|
||||
{name: 'LEADER', content: leader.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Cancels subscriptions of members of a group
|
||||
*
|
||||
* @param group The Group Model that is cancelling a group plan
|
||||
*
|
||||
* @return undefined
|
||||
*/
|
||||
api.cancelGroupUsersSubscription = async function cancelGroupUsersSubscription (group) {
|
||||
let members;
|
||||
if (group.type === 'guild') {
|
||||
members = await User.find({guilds: group._id}).select('_id guilds purchased').exec();
|
||||
} else {
|
||||
members = await User.find({'party._id': group._id}).select('_id guilds purchased').exec();
|
||||
}
|
||||
|
||||
let promises = members.map((member) => {
|
||||
return this.cancelGroupSubscriptionForUser(member, group);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
api.cancelGroupSubscriptionForUser = async function cancelGroupSubscriptionForUser (user, group) {
|
||||
if (user.purchased.plan.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) return;
|
||||
|
||||
let userGroups = _.clone(user.guilds.toObject());
|
||||
userGroups.push('party');
|
||||
|
||||
let index = userGroups.indexOf(group._id);
|
||||
userGroups.splice(index, 1);
|
||||
|
||||
let groupPlansQuery = {
|
||||
type: {$in: ['guild', 'party']},
|
||||
// privacy: 'private',
|
||||
_id: {$in: userGroups},
|
||||
'purchased.plan.dateTerminated': null,
|
||||
};
|
||||
|
||||
let groupFields = `${basicGroupFields} purchased`;
|
||||
let userGroupPlans = await Group.find(groupPlansQuery).select(groupFields).exec();
|
||||
|
||||
if (userGroupPlans.length === 0) {
|
||||
let leader = await User.findById(group.leader).exec();
|
||||
txnEmail(user, 'group-member-cancel', [
|
||||
{name: 'LEADER', content: leader.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
await this.cancelSubscription({user});
|
||||
}
|
||||
};
|
||||
|
||||
api.createSubscription = async function createSubscription (data) {
|
||||
let recipient = data.gift ? data.gift.member : data.user;
|
||||
let block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
|
||||
@@ -75,6 +229,8 @@ api.createSubscription = async function createSubscription (data) {
|
||||
emailType = 'group-subscription-begins';
|
||||
groupId = group._id;
|
||||
recipient.purchased.plan.quantity = data.sub.quantity;
|
||||
|
||||
await this.addSubscriptionToGroupUsers(group);
|
||||
}
|
||||
|
||||
plan = recipient.purchased.plan;
|
||||
@@ -135,7 +291,8 @@ api.createSubscription = async function createSubscription (data) {
|
||||
revealMysteryItems(recipient);
|
||||
}
|
||||
|
||||
if (!data.gift) {
|
||||
// @TODO: Create a factory pattern for use cases
|
||||
if (!data.gift && data.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) {
|
||||
txnEmail(data.user, emailType);
|
||||
}
|
||||
|
||||
@@ -225,22 +382,6 @@ api.createSubscription = async function createSubscription (data) {
|
||||
});
|
||||
};
|
||||
|
||||
api.updateStripeGroupPlan = async function updateStripeGroupPlan (group, stripeInc) {
|
||||
if (group.purchased.plan.paymentMethod !== 'Stripe') return;
|
||||
let stripeApi = stripeInc || stripe;
|
||||
let plan = shared.content.subscriptionBlocks.group_monthly;
|
||||
|
||||
await stripeApi.subscriptions.update(
|
||||
group.purchased.plan.subscriptionId,
|
||||
{
|
||||
plan: plan.key,
|
||||
quantity: group.memberCount + plan.quantity - 1,
|
||||
}
|
||||
);
|
||||
|
||||
group.purchased.plan.quantity = group.memberCount + plan.quantity - 1;
|
||||
};
|
||||
|
||||
// Sets their subscription to be cancelled later
|
||||
api.cancelSubscription = async function cancelSubscription (data) {
|
||||
let plan;
|
||||
@@ -248,6 +389,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
let cancelType = 'unsubscribe';
|
||||
let groupId;
|
||||
let emailType = 'cancel-subscription';
|
||||
let emailMergeData = [];
|
||||
|
||||
// If we are buying a group subscription
|
||||
if (data.groupId) {
|
||||
@@ -265,12 +407,23 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
}
|
||||
plan = group.purchased.plan;
|
||||
emailType = 'group-cancel-subscription';
|
||||
emailMergeData.push({name: 'GROUP_NAME', content: group.name});
|
||||
|
||||
await this.cancelGroupUsersSubscription(group);
|
||||
} else {
|
||||
plan = data.user.purchased.plan;
|
||||
}
|
||||
|
||||
let customerId = plan.customerId;
|
||||
let now = moment();
|
||||
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
|
||||
let defaultRemainingDays = 30;
|
||||
|
||||
if (plan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID) {
|
||||
defaultRemainingDays = 2;
|
||||
}
|
||||
|
||||
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : defaultRemainingDays;
|
||||
if (plan.extraMonths < 0) plan.extraMonths = 0;
|
||||
let extraDays = Math.ceil(30.5 * plan.extraMonths);
|
||||
let nowStr = `${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`;
|
||||
let nowStrFormat = 'MM/DD/YYYY';
|
||||
@@ -289,7 +442,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
await data.user.save();
|
||||
}
|
||||
|
||||
txnEmail(data.user, emailType);
|
||||
if (customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) txnEmail(data.user, emailType, emailMergeData);
|
||||
|
||||
if (group) {
|
||||
cancelType = 'group-unsubscribe';
|
||||
|
||||
@@ -42,6 +42,22 @@ paypal.configure({
|
||||
|
||||
let api = {};
|
||||
|
||||
api.constants = {
|
||||
// CURRENCY_CODE: 'USD',
|
||||
// SELLER_NOTE: 'Habitica Payment',
|
||||
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
|
||||
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
|
||||
// STORE_NAME: 'Habitica',
|
||||
//
|
||||
// GIFT_TYPE_GEMS: 'gems',
|
||||
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
|
||||
//
|
||||
// METHOD_BUY_GEMS: 'buyGems',
|
||||
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
|
||||
PAYMENT_METHOD: 'Paypal',
|
||||
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
|
||||
};
|
||||
|
||||
api.paypalPaymentCreate = Bluebird.promisify(paypal.payment.create, {context: paypal.payment});
|
||||
api.paypalPaymentExecute = Bluebird.promisify(paypal.payment.execute, {context: paypal.payment});
|
||||
api.paypalBillingAgreementCreate = Bluebird.promisify(paypal.billingAgreement.create, {context: paypal.billingAgreement});
|
||||
@@ -68,7 +84,7 @@ api.checkout = async function checkout (options = {}) {
|
||||
|
||||
let createPayment = {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
payer: { payment_method: this.constants.PAYMENT_METHOD },
|
||||
redirect_urls: {
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
@@ -103,7 +119,7 @@ api.checkoutSuccess = async function checkoutSuccess (options = {}) {
|
||||
let data = {
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
};
|
||||
|
||||
if (gift) {
|
||||
@@ -138,7 +154,7 @@ api.subscribe = async function subscribe (options = {}) {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
payment_method: this.constants.PAYMENT_METHOD,
|
||||
},
|
||||
};
|
||||
let billingAgreement = await this.paypalBillingAgreementCreate(billingAgreementAttributes);
|
||||
@@ -154,7 +170,7 @@ api.subscribeSuccess = async function subscribeSuccess (options = {}) {
|
||||
user,
|
||||
groupId,
|
||||
customerId: result.id,
|
||||
paymentMethod: 'Paypal',
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
@@ -194,7 +210,7 @@ api.subscribeCancel = async function subscribeCancel (options = {}) {
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
nextBill: nextBillingDate,
|
||||
});
|
||||
};
|
||||
@@ -208,7 +224,7 @@ api.ipn = async function ipnApi (options = {}) {
|
||||
// @TODO: Should this request billing date?
|
||||
let user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec();
|
||||
if (user) {
|
||||
await payments.cancelSubscription({ user, paymentMethod: 'Paypal' });
|
||||
await payments.cancelSubscription({ user, paymentMethod: this.constants.PAYMENT_METHOD });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,7 +235,7 @@ api.ipn = async function ipnApi (options = {}) {
|
||||
.exec();
|
||||
|
||||
if (group) {
|
||||
await payments.cancelSubscription({ groupId: group._id, paymentMethod: 'Paypal' });
|
||||
await payments.cancelSubscription({ groupId: group._id, paymentMethod: this.constants.PAYMENT_METHOD });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,11 +16,32 @@ import {
|
||||
} from '../models/group';
|
||||
import shared from '../../common';
|
||||
|
||||
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||
let stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||
const i18n = shared.i18n;
|
||||
|
||||
let api = {};
|
||||
|
||||
api.constants = {
|
||||
// CURRENCY_CODE: 'USD',
|
||||
// SELLER_NOTE: 'Habitica Payment',
|
||||
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
|
||||
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
|
||||
// STORE_NAME: 'Habitica',
|
||||
//
|
||||
// GIFT_TYPE_GEMS: 'gems',
|
||||
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
|
||||
//
|
||||
// METHOD_BUY_GEMS: 'buyGems',
|
||||
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
|
||||
PAYMENT_METHOD: 'Stripe',
|
||||
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
|
||||
};
|
||||
|
||||
api.setStripeApi = function setStripeApi (stripeInc) {
|
||||
stripe = stripeInc;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Allows for purchasing a user subscription, group subscription or gems with Stripe
|
||||
*
|
||||
@@ -97,7 +118,7 @@ api.checkout = async function checkout (options, stripeInc) {
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: 'Stripe',
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
@@ -108,7 +129,7 @@ api.checkout = async function checkout (options, stripeInc) {
|
||||
let data = {
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: 'Stripe',
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
gift,
|
||||
};
|
||||
|
||||
@@ -204,20 +225,38 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
|
||||
|
||||
if (!customerId) throw new NotAuthorized(i18n.t('missingSubscription'));
|
||||
|
||||
// @TODO: Handle error response
|
||||
let customer = await stripeApi.customers.retrieve(customerId);
|
||||
|
||||
let subscription = customer.subscription;
|
||||
if (!subscription) {
|
||||
if (!subscription && customer.subscriptions) {
|
||||
subscription = customer.subscriptions.data[0];
|
||||
}
|
||||
|
||||
if (!subscription) return;
|
||||
|
||||
await stripeApi.customers.del(customerId);
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: subscription.current_period_end * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
});
|
||||
};
|
||||
|
||||
api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) {
|
||||
let stripeApi = stripe;
|
||||
let plan = shared.content.subscriptionBlocks.group_monthly;
|
||||
|
||||
await stripeApi.subscriptions.update(
|
||||
group.purchased.plan.subscriptionId,
|
||||
{
|
||||
plan: plan.key,
|
||||
quantity: group.memberCount + plan.quantity - 1,
|
||||
}
|
||||
);
|
||||
|
||||
group.purchased.plan.quantity = group.memberCount + plan.quantity - 1;
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { model as Challenge} from './challenge';
|
||||
import * as Tasks from './task';
|
||||
import validator from 'validator';
|
||||
import { removeFromArray } from '../libs/collectionManipulators';
|
||||
import payments from '../libs/payments';
|
||||
import { groupChatReceivedWebhook } from '../libs/webhook';
|
||||
import {
|
||||
InternalServerError,
|
||||
@@ -28,6 +29,8 @@ import {
|
||||
import {
|
||||
schema as SubscriptionPlanSchema,
|
||||
} from './subscriptionPlan';
|
||||
import amazonPayments from '../libs/amazonPayments';
|
||||
import stripePayments from '../libs/stripePayments';
|
||||
|
||||
const questScrolls = shared.content.quests;
|
||||
const Schema = mongoose.Schema;
|
||||
@@ -904,10 +907,14 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
|
||||
let group = this;
|
||||
let update = {};
|
||||
|
||||
if (group.memberCount <= 1 && group.privacy === 'private' && group.isSubscribed()) {
|
||||
if (group.memberCount <= 1 && group.privacy === 'private' && group.hasNotCancelled()) {
|
||||
throw new NotAuthorized(shared.i18n.t('cannotDeleteActiveGroup'));
|
||||
}
|
||||
|
||||
if (group.leader === user._id && group.hasNotCancelled()) {
|
||||
throw new NotAuthorized(shared.i18n.t('leaderCannotLeaveGroupWithActiveGroup'));
|
||||
}
|
||||
|
||||
// only remove user from challenges if it's set to leave-challenges
|
||||
if (keepChallenges === 'leave-challenges') {
|
||||
let challenges = await Challenge.find({
|
||||
@@ -948,6 +955,10 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
|
||||
update.$unset = {[`quest.members.${user._id}`]: 1};
|
||||
}
|
||||
|
||||
if (group.purchased.plan.customerId) {
|
||||
promises.push(payments.cancelGroupSubscriptionForUser(user, this));
|
||||
}
|
||||
|
||||
// If user is the last one in group and group is private, delete it
|
||||
if (group.memberCount <= 1 && group.privacy === 'private') {
|
||||
// double check the member count is correct so we don't accidentally delete a group that still has users in it
|
||||
@@ -957,7 +968,9 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
|
||||
} else {
|
||||
members = await User.find({'party._id': group._id}).select('_id').exec();
|
||||
}
|
||||
|
||||
_.remove(members, {_id: user._id});
|
||||
|
||||
if (members.length === 0) {
|
||||
promises.push(group.remove());
|
||||
return await Bluebird.all(promises);
|
||||
@@ -1165,6 +1178,28 @@ schema.methods.isSubscribed = function isSubscribed () {
|
||||
return plan && plan.customerId && (!plan.dateTerminated || moment(plan.dateTerminated).isAfter(now));
|
||||
};
|
||||
|
||||
schema.methods.hasNotCancelled = function hasNotCancelled () {
|
||||
let plan = this.purchased.plan;
|
||||
return this.isSubscribed() && !plan.dateTerminated;
|
||||
};
|
||||
|
||||
schema.methods.updateGroupPlan = async function updateGroupPlan (removingMember) {
|
||||
// Recheck the group plan count
|
||||
let members;
|
||||
if (this.type === 'guild') {
|
||||
members = await User.find({guilds: this._id}).select('_id').exec();
|
||||
} else {
|
||||
members = await User.find({'party._id': this._id}).select('_id').exec();
|
||||
}
|
||||
this.memberCount = members.length;
|
||||
|
||||
if (this.purchased.plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) {
|
||||
await stripePayments.chargeForAdditionalGroupMember(this);
|
||||
} else if (this.purchased.plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD && !removingMember) {
|
||||
await amazonPayments.chargeForAdditionalGroupMember(this);
|
||||
}
|
||||
};
|
||||
|
||||
export let model = mongoose.model('Group', schema);
|
||||
|
||||
// initialize tavern if !exists (fresh installs)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import common from '../../../common';
|
||||
import Bluebird from 'bluebird';
|
||||
import {
|
||||
@@ -7,9 +8,21 @@ import {
|
||||
import { defaults } from 'lodash';
|
||||
import { model as UserNotification } from '../userNotification';
|
||||
import schema from './schema';
|
||||
import payments from '../../libs/payments';
|
||||
import amazonPayments from '../../libs/amazonPayments';
|
||||
import stripePayments from '../../libs/stripePayments';
|
||||
import paypalPayments from '../../libs/paypalPayments';
|
||||
|
||||
schema.methods.isSubscribed = function isSubscribed () {
|
||||
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
|
||||
let now = new Date();
|
||||
let plan = this.purchased.plan;
|
||||
|
||||
return plan && plan.customerId && (!plan.dateTerminated || moment(plan.dateTerminated).isAfter(now));
|
||||
};
|
||||
|
||||
schema.methods.hasNotCancelled = function hasNotCancelled () {
|
||||
let plan = this.purchased.plan;
|
||||
return this.isSubscribed() && !plan.dateTerminated;
|
||||
};
|
||||
|
||||
// Get an array of groups ids the user is member of
|
||||
@@ -91,3 +104,22 @@ schema.methods.addComputedStatsToJSONObj = function addComputedStatsToUserJSONOb
|
||||
|
||||
return statsObject;
|
||||
};
|
||||
|
||||
// @TODO: There is currently a three way relation between the user, payment methods and the payment helper
|
||||
// This creates some odd Dependency Injection issues. To counter that, we use the user as the third layer
|
||||
// To negotiate between the payment providers and the payment helper (which probably has too many responsiblities)
|
||||
// In summary, currently is is best practice to use this method to cancel a user subscription, rather than calling the
|
||||
// payment helper.
|
||||
schema.methods.cancelSubscription = async function cancelSubscription () {
|
||||
let plan = this.purchased.plan;
|
||||
|
||||
if (plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD) {
|
||||
return await amazonPayments.cancelSubscription({user: this});
|
||||
} else if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) {
|
||||
return await stripePayments.cancelSubscription({user: this});
|
||||
} else if (plan.paymentMethod === paypalPayments.constants.PAYMENT_METHOD) {
|
||||
return await paypalPayments.subscribeCancel({user: this});
|
||||
}
|
||||
|
||||
return await payments.cancelSubscription({user: this});
|
||||
};
|
||||
|
||||
@@ -47,17 +47,17 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template',
|
||||
|
||||
div(ng-if='user.purchased.plan.customerId')
|
||||
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
|
||||
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
|
||||
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.customerId !== "group-plan"', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
|
||||
|
||||
.container-fluid.slight-vertical-padding(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
|
||||
small.muted=env.t('subscribeUsing')
|
||||
.row.text-center
|
||||
.col-xs-4
|
||||
.col-md-4
|
||||
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
|
||||
.col-xs-4
|
||||
.col-md-4
|
||||
a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', 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
|
||||
.col-md-4
|
||||
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")
|
||||
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
||||
li(ng-show='group.purchased.active && group.leader._id === user._id')
|
||||
a(ng-click="groupPanel = 'approvals'")=env.t('approvalsTitle')
|
||||
li
|
||||
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('subscription')
|
||||
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('paymentDetails')
|
||||
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && !group.purchased.plan.customerId')=env.t('upgradeTitle')
|
||||
|
||||
.tab-content
|
||||
|
||||
@@ -37,8 +37,8 @@ mixin groupSubscription()
|
||||
.row.text-center
|
||||
h3 Upgrade My Group
|
||||
div
|
||||
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')
|
||||
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})")
|
||||
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id, group: group})', ng-disabled='!_subscription.key')= env.t('card')
|
||||
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id, group: group})")
|
||||
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
||||
//- .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')
|
||||
|
||||
@@ -27,36 +27,35 @@ div(ng-if='(task.type !== "reward") || (!obj.auth && obj.purchased && obj.purcha
|
||||
|
||||
hr
|
||||
|
||||
.form-group
|
||||
legend.option-title=env.t('repeat')
|
||||
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
|
||||
option(value='daily')=env.t('daily')
|
||||
option(value='weekly')=env.t('weekly')
|
||||
option(value='monthly')=env.t('monthly')
|
||||
option(value='yearly')=env.t('yearly')
|
||||
fieldset.option-group.advanced-option(ng-show="task._edit._advanced && task.type !== 'reward'")
|
||||
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
|
||||
option(value='daily')=env.t('daily')
|
||||
option(value='weekly')=env.t('weekly')
|
||||
option(value='monthly')=env.t('monthly')
|
||||
option(value='yearly')=env.t('yearly')
|
||||
|
||||
//- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
|
||||
//- option(value='weekly')=env.t('repeatWeek')
|
||||
//- option(value='daily')=env.t('repeatDays')
|
||||
//- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
|
||||
//- option(value='weekly')=env.t('repeatWeek')
|
||||
//- option(value='daily')=env.t('repeatDays')
|
||||
|
||||
include ./dailies/repeat_options
|
||||
include ./dailies/repeat_options
|
||||
|
||||
.form-group(ng-show='task._edit.frequency === "monthly"')
|
||||
legend.option-title=env.t('repeatsOn')
|
||||
label
|
||||
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfMonth')
|
||||
=env.t('dayOfMonth')
|
||||
label
|
||||
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek')
|
||||
=env.t('dayOfWeek')
|
||||
.form-group(ng-show='task._edit.frequency === "monthly"')
|
||||
legend.option-title=env.t('repeatsOn')
|
||||
label
|
||||
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfMonth')
|
||||
=env.t('dayOfMonth')
|
||||
label
|
||||
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek')
|
||||
=env.t('dayOfWeek')
|
||||
|
||||
.form-group
|
||||
legend.option-title=env.t('summary')
|
||||
div {{summary}}
|
||||
.form-group
|
||||
legend.option-title=env.t('summary')
|
||||
div {{summary}}
|
||||
|
||||
hr
|
||||
hr
|
||||
|
||||
fieldset.option-group.advanced-option(ng-show="task._edit._advanced")
|
||||
fieldset.option-group.advanced-option(ng-show="task._edit._advanced && task.type !== 'reward'")
|
||||
legend.option-title
|
||||
a.hint.priority-multiplier-help(href='http://habitica.wikia.com/wiki/Difficulty', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty')
|
||||
ul.priority-multiplier
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
//- .form-group
|
||||
//- legend.option-title=env.t('repeat')
|
||||
//- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
|
||||
//- option(value='weekly')=env.t('repeatWeek')
|
||||
//- option(value='daily')=env.t('repeatDays')
|
||||
.form-group
|
||||
legend.option-title
|
||||
span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'),
|
||||
popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery')
|
||||
|
||||
legend.option-title
|
||||
span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'),
|
||||
popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery')
|
||||
// If frequency is daily
|
||||
ng-form.form-group(name='everyX')
|
||||
.input-group
|
||||
input.form-control(type='number', ng-model='task._edit.everyX', min='0', ng-disabled='!canEdit(task)', required)
|
||||
span.input-group-addon {{repeatSuffix}}
|
||||
|
||||
// If frequency is daily
|
||||
ng-form.form-group(name='everyX')
|
||||
.input-group
|
||||
input.form-control(type='number', ng-model='task._edit.everyX', min='0', ng-disabled='!canEdit(task)', required)
|
||||
span.input-group-addon {{repeatSuffix}}
|
||||
|
||||
// If frequency is weekly
|
||||
.form-group(ng-if='task._edit.frequency=="weekly"')
|
||||
legend.option-title=env.t('onDays')
|
||||
ul.repeat-days
|
||||
|
||||
Reference in New Issue
Block a user