diff --git a/test/api/v3/integration/groups/POST-groups_invite.test.js b/test/api/v3/integration/groups/POST-groups_invite.test.js
index d163964c37..522fa12043 100644
--- a/test/api/v3/integration/groups/POST-groups_invite.test.js
+++ b/test/api/v3/integration/groups/POST-groups_invite.test.js
@@ -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', () => {
diff --git a/test/api/v3/integration/groups/PUT-groups.test.js b/test/api/v3/integration/groups/PUT-groups.test.js
index 8d581d56ca..a5e4e091cd 100644
--- a/test/api/v3/integration/groups/PUT-groups.test.js
+++ b/test/api/v3/integration/groups/PUT-groups.test.js
@@ -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);
+ });
});
diff --git a/test/api/v3/unit/libs/amazonPayments.test.js b/test/api/v3/unit/libs/amazonPayments.test.js
index 0604d3755c..e4e593e7d7 100644
--- a/test/api/v3/unit/libs/amazonPayments.test.js
+++ b/test/api/v3/unit/libs/amazonPayments.test.js
@@ -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,
+ },
+ });
+ });
+ });
});
diff --git a/test/api/v3/unit/libs/cron.test.js b/test/api/v3/unit/libs/cron.test.js
index 4e8835988a..df8becc493 100644
--- a/test/api/v3/unit/libs/cron.test.js
+++ b/test/api/v3/unit/libs/cron.test.js
@@ -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);
});
diff --git a/test/api/v3/unit/libs/payments.test.js b/test/api/v3/unit/libs/payments.test.js
index a1a2b19b28..56057f8c23 100644
--- a/test/api/v3/unit/libs/payments.test.js
+++ b/test/api/v3/unit/libs/payments.test.js
@@ -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;
});
});
});
diff --git a/test/api/v3/unit/libs/payments/group-plans/group-payments-cancel.test.js b/test/api/v3/unit/libs/payments/group-plans/group-payments-cancel.test.js
new file mode 100644
index 0000000000..5a7dac424f
--- /dev/null
+++ b/test/api/v3/unit/libs/payments/group-plans/group-payments-cancel.test.js
@@ -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);
+ });
+});
diff --git a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js
new file mode 100644
index 0000000000..2aefdcabcf
--- /dev/null
+++ b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js
@@ -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;
+ });
+});
diff --git a/test/api/v3/unit/libs/stripePayments.test.js b/test/api/v3/unit/libs/stripePayments.test.js
index e1ff3cdf3d..26ce880715 100644
--- a/test/api/v3/unit/libs/stripePayments.test.js
+++ b/test/api/v3/unit/libs/stripePayments.test.js
@@ -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);
+ });
+ });
});
diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js
index bbb656cb97..1fa1000d9c 100644
--- a/test/api/v3/unit/models/group.test.js
+++ b/test/api/v3/unit/models/group.test.js
@@ -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;
+ });
+ });
});
});
diff --git a/test/api/v3/unit/models/user.test.js b/test/api/v3/unit/models/user.test.js
index 1c2b55ce1c..0192fe42ee 100644
--- a/test/api/v3/unit/models/user.test.js
+++ b/test/api/v3/unit/models/user.test.js
@@ -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;
+ });
+ });
});
diff --git a/test/helpers/api-integration/api-classes.js b/test/helpers/api-integration/api-classes.js
index f584826520..7612c88cfd 100644
--- a/test/helpers/api-integration/api-classes.js
+++ b/test/helpers/api-integration/api-classes.js
@@ -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 {
diff --git a/website/client-old/js/controllers/groupPlansCtrl.js b/website/client-old/js/controllers/groupPlansCtrl.js
index 0ebf814aef..6fd7d96317 100644
--- a/website/client-old/js/controllers/groupPlansCtrl.js
+++ b/website/client-old/js/controllers/groupPlansCtrl.js
@@ -28,7 +28,7 @@ angular.module('habitrpg')
};
$scope.newGroupIsReady = function () {
- return $scope.newGroup.name && $scope.newGroup.description;
+ return !!$scope.newGroup.name;
};
$scope.createGroup = function () {
diff --git a/website/client-old/js/controllers/guildsCtrl.js b/website/client-old/js/controllers/guildsCtrl.js
index fa8574b03c..14513217d0 100644
--- a/website/client-old/js/controllers/guildsCtrl.js
+++ b/website/client-old/js/controllers/guildsCtrl.js
@@ -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
diff --git a/website/client-old/js/controllers/partyCtrl.js b/website/client-old/js/controllers/partyCtrl.js
index 342656c03d..146b12021b 100644
--- a/website/client-old/js/controllers/partyCtrl.js
+++ b/website/client-old/js/controllers/partyCtrl.js
@@ -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;
diff --git a/website/client-old/js/directives/task-list.directive.js b/website/client-old/js/directives/task-list.directive.js
index 91d0a77105..8ed4d7f096 100644
--- a/website/client-old/js/directives/task-list.directive.js
+++ b/website/client-old/js/directives/task-list.directive.js
@@ -1,78 +1,78 @@
-'use strict';
-
-(function(){
- angular
- .module('habitrpg')
- .directive('taskList', taskList);
-
- taskList.$inject = [
- '$state',
- 'User',
- '$rootScope',
- ];
-
- function taskList($state, User, $rootScope) {
- return {
- restrict: 'EA',
- templateUrl: 'templates/task-list.html',
- transclude: true,
- scope: true,
- // scope: {
- // taskList: '=list',
- // list: '=listDetails',
- // obj: '=object',
- // user: "=",
- // },
- link: function($scope, element, attrs) {
- // @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;
- $scope.obj = obj;
- setUpGroupedList();
- setUpTaskWatch();
- }
-
- $rootScope.$on('obj-updated', function (event, obj) {
- setObj(obj, true);
- });
-
- function setUpGroupedList () {
- if (!$scope.obj) return;
- $scope.groupedList = {};
- ['habit', 'daily', 'todo', 'reward'].forEach(function (listType) {
- groupTasksByChallenge($scope.obj[listType + 's'], listType);
- });
- }
- setUpGroupedList();
-
- function groupTasksByChallenge (taskList, type) {
- $scope.groupedList[type] = _.groupBy(taskList, 'challenge.shortName');
- };
-
- function setUpTaskWatch () {
- if (!$scope.obj) return;
- $scope.$watch(function () { return $scope.obj.tasksOrder; }, function () {
- setUpGroupedList();
- }, true);
- }
- setUpTaskWatch();
-
- $scope.getTaskList = function (list, taskList, obj) {
- setObj(obj);
- if (!$scope.obj) return [];
- if (taskList) return taskList;
- return $scope.obj[list.type+'s'];
- };
-
- $scope.showNormalList = function () {
- return !$state.includes("options.social.challenges") && !User.user.preferences.tasks.groupByChallenge;
- };
-
- $scope.showChallengeList = function () {
- return $state.includes("options.social.challenges");
- };
- }
- }
- }
-}());
+'use strict';
+
+(function(){
+ angular
+ .module('habitrpg')
+ .directive('taskList', taskList);
+
+ taskList.$inject = [
+ '$state',
+ 'User',
+ '$rootScope',
+ ];
+
+ function taskList($state, User, $rootScope) {
+ return {
+ restrict: 'EA',
+ templateUrl: 'templates/task-list.html',
+ transclude: true,
+ scope: true,
+ // scope: {
+ // taskList: '=list',
+ // list: '=listDetails',
+ // obj: '=object',
+ // user: "=",
+ // },
+ link: function($scope, element, attrs) {
+ // @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;
+ $scope.obj = obj;
+ setUpGroupedList();
+ setUpTaskWatch();
+ }
+
+ $rootScope.$on('obj-updated', function (event, obj) {
+ setObj(obj, true);
+ });
+
+ function setUpGroupedList () {
+ if (!$scope.obj) return;
+ $scope.groupedList = {};
+ ['habit', 'daily', 'todo', 'reward'].forEach(function (listType) {
+ groupTasksByChallenge($scope.obj[listType + 's'], listType);
+ });
+ }
+ setUpGroupedList();
+
+ function groupTasksByChallenge (taskList, type) {
+ $scope.groupedList[type] = _.groupBy(taskList, 'challenge.shortName');
+ };
+
+ function setUpTaskWatch () {
+ if (!$scope.obj) return;
+ $scope.$watch(function () { return $scope.obj.tasksOrder; }, function () {
+ setUpGroupedList();
+ }, true);
+ }
+ setUpTaskWatch();
+
+ $scope.getTaskList = function (list, taskList, obj) {
+ setObj(obj);
+ if (!$scope.obj) return [];
+ if (taskList) return taskList;
+ return $scope.obj[list.type+'s'];
+ };
+
+ $scope.showNormalList = function () {
+ return !$state.includes("options.social.challenges") && !User.user.preferences.tasks.groupByChallenge;
+ };
+
+ $scope.showChallengeList = function () {
+ return $state.includes("options.social.challenges");
+ };
+ }
+ }
+ }
+}());
diff --git a/website/client-old/js/services/paymentServices.js b/website/client-old/js/services/paymentServices.js
index e64ddb1185..2262ebbcf9 100644
--- a/website/client-old/js/services/paymentServices.js
+++ b/website/client-old/js/services/paymentServices.js
@@ -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,
diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json
index 19eb5b3f18..3adc92b236 100644
--- a/website/common/locales/en/groups.json
+++ b/website/common/locales/en/groups.json
@@ -1,258 +1,262 @@
-{
- "tavern": "Tavern Chat",
- "innCheckOut": "Check Out of Inn",
- "innCheckIn": "Rest in the Inn",
- "innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
- "innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
- "lfgPosts": "Looking for Group (Party Wanted) Posts",
- "tutorial": "Tutorial",
- "glossary": "Glossary",
- "wiki": "Wiki",
- "wikiLink": "Wiki",
- "reportAP": "Report a Problem",
- "requestAF": "Request a Feature",
- "community": "Community Forum",
- "dataTool": "Data Display Tool",
- "resources": "Resources",
- "askQuestionNewbiesGuild": "Ask a Question (Habitica Help guild)",
- "tavernAlert1": "To report a bug, visit",
- "tavernAlert2": "the Report a Bug Guild",
- "moderatorIntro1": "Tavern and guild moderators are: ",
- "communityGuidelines": "Community Guidelines",
- "communityGuidelinesRead1": "Please read our",
- "communityGuidelinesRead2": "before chatting.",
- "party": "Party",
- "createAParty": "Create A Party",
- "updatedParty": "Party settings updated.",
- "noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
- "LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
- "wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
- "joinExistingParty": "Join Someone Else's Party",
- "needPartyToStartQuest": "Whoops! You need to create or join a party before you can start a quest!",
- "create": "Create",
- "userId": "User ID",
- "invite": "Invite",
- "leave": "Leave",
- "invitedTo": "Invited to <%= name %>",
- "invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
- "invitationAcceptedHeader": "Your Invitation has been Accepted",
- "invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
- "joinNewParty": "Join New Party",
- "declineInvitation": "Decline Invitation",
- "partyLoading1": "Your party is being summoned. Please wait...",
- "partyLoading2": "Your party is coming in from battle. Please wait...",
- "partyLoading3": "Your party is gathering. Please wait...",
- "partyLoading4": "Your party is materializing. Please wait...",
- "systemMessage": "System Message",
- "newMsg": "New message in \"<%= name %>\"",
- "chat": "Chat",
- "sendChat": "Send Chat",
- "toolTipMsg": "Fetch Recent Messages",
- "sendChatToolTip": "You can send a chat from the keyboard by tabbing to the 'Send Chat' button and pressing Enter or by pressing Control (Command on a Mac) + Enter.",
- "syncPartyAndChat": "Sync Party and Chat",
- "guildBankPop1": "Guild Bank",
- "guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
- "guildGems": "Guild Gems",
- "editGroup": "Edit Group",
- "newGroupName": "<%= groupType %> Name",
- "groupName": "Group Name",
- "groupLeader": "Group Leader",
- "groupID": "Group ID",
- "groupDescr": "Description shown in public Guilds list (Markdown OK)",
- "logoUrl": "Logo URL",
- "assignLeader": "Assign Group Leader",
- "members": "Members",
- "partyList": "Order for party members in header",
- "banTip": "Boot Member",
- "moreMembers": "more members",
- "invited": "Invited",
- "leaderMsg": "Message from group leader (Markdown OK)",
- "name": "Name",
- "description": "Description",
- "public": "Public",
- "inviteOnly": "Invite Only",
- "gemCost": "The Gem cost promotes high quality Guilds, and is transferred into your Guild's bank for use as prizes in Guild Challenges!",
- "search": "Search",
- "publicGuilds": "Public Guilds",
- "createGuild": "Create Guild",
- "guild": "Guild",
- "guilds": "Guilds",
- "guildsLink": "Guilds",
- "sureKick": "Do you really want to remove this member from the party/guild?",
- "optionalMessage": "Optional message",
- "yesRemove": "Yes, remove them",
- "foreverAlone": "Can't like your own message. Don't be that person.",
- "sortLevel": "Sort by level",
- "sortRandom": "Sort randomly",
- "sortPets": "Sort by number of pets",
- "sortName": "Sort by avatar name",
- "sortBackgrounds": "Sort by background",
- "sortHabitrpgJoined": "Sort by Habitica date joined",
- "sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
- "ascendingSort": "Sort Ascending",
- "descendingSort": "Sort Descending",
- "confirmGuild": "Create Guild for 4 Gems?",
- "leaveGroupCha": "Leave Guild challenges and...",
- "confirm": "Confirm",
- "leaveGroup": "Leave Guild?",
- "leavePartyCha": "Leave party challenges and...",
- "leaveParty": "Leave party?",
- "sendPM": "Send private message",
- "send": "Send",
- "messageSentAlert": "Message sent",
- "pmHeading": "Private message to <%= name %>",
- "pmsMarkedRead": "Your private messages have been marked as read",
- "possessiveParty": "<%= name %>'s Party",
- "clearAll": "Delete All Messages",
- "confirmDeleteAllMessages": "Are you sure you want to delete all messages in your inbox? Other users will still see messages you have sent to them.",
- "optOutPopover": "Don't like private messages? Click to completely opt out",
- "block": "Block",
- "unblock": "Un-block",
- "pm-reply": "Send a reply",
- "inbox": "Inbox",
- "messageRequired": "A message is required.",
- "toUserIDRequired": "A User ID is required",
- "gemAmountRequired": "A number of gems is required",
- "notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
- "privateMessageGiftGemsMessage": "Hello <%= receiverName %>, <%= senderName %> has sent you <%= gemAmount %> gems!",
- "privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> months of subscription! ",
- "cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.",
- "badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.",
- "abuseFlag": "Report violation of Community Guidelines",
- "abuseFlagModalHeading": "Report <%= name %> for violation?",
- "abuseFlagModalBody": "Are you sure you want to report this post? You should ONLY report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction. Appropriate reasons to flag a post include but are not limited to: