From 22d696219aaef24cd91b939ee91f51c584faf5eb Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Mon, 20 Apr 2020 23:53:41 +0200 Subject: [PATCH] Revert "Squashed commit of the following:" This reverts commit 9014943a86e07141853c1c861bdfcf8124c3a13d. --- .../stripe/calculate-termination-date.test.js | 64 ------------------- test/api/unit/models/group.test.js | 10 +-- ...e.test.js => POST-groups_groupId_leave.js} | 46 ------------- .../api-integration/v3/object-generators.js | 30 +-------- website/server/controllers/api-v3/groups.js | 12 +++- website/server/libs/chat/group-chat.js | 2 +- website/server/libs/invites/index.js | 6 +- website/server/libs/payments/subscriptions.js | 20 ++++-- website/server/libs/payments/util.js | 32 ---------- website/server/models/group.js | 10 +-- website/server/models/user/methods.js | 4 +- 11 files changed, 43 insertions(+), 193 deletions(-) delete mode 100644 test/api/unit/libs/payments/stripe/calculate-termination-date.test.js rename test/api/v3/integration/groups/{POST-groups_groupId_leave.test.js => POST-groups_groupId_leave.js} (86%) delete mode 100644 website/server/libs/payments/util.js diff --git a/test/api/unit/libs/payments/stripe/calculate-termination-date.test.js b/test/api/unit/libs/payments/stripe/calculate-termination-date.test.js deleted file mode 100644 index 7badd7e62a..0000000000 --- a/test/api/unit/libs/payments/stripe/calculate-termination-date.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import moment from 'moment'; -import { calculateSubscriptionTerminationDate } from '../../../../../../website/server/libs/payments/util'; -import api from '../../../../../../website/server/libs/payments/payments'; - -describe('#calculateSubscriptionTerminationDate', () => { - let plan; - let nextBill; - - beforeEach(() => { - plan = { - customerId: 'customer-id', - extraMonths: 0, - }; - nextBill = moment(); - }); - it('should extend date to the exact amount of days left before the next bill will occur', () => { - nextBill = moment() - .add(5, 'days'); - const expectedTerminationDate = moment() - .add(5, 'days'); - - const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants); - expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0); - }); - it('if nextBill is null, add 30 days to termination date', () => { - nextBill = null; - const expectedTerminationDate = moment() - .add(30, 'days'); - const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants); - - expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0); - }); - it('if nextBill is null and it\'s a group plan, add 2 days instead of 30', () => { - nextBill = null; - plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID; - const expectedTerminationDate = moment() - .add(2, 'days'); - - const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants); - expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0); - }); - it('should add 30.5 days for each extraMonth', () => { - plan.extraMonths = 4; - const expectedTerminationDate = moment() - .add(30.5 * 4, 'days'); - - const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants); - expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0); - }); - it('should round up if total days gained by extraMonth is a decimal number', () => { - plan.extraMonths = 5; - const expectedTerminationDate = moment() - .add(Math.ceil(30.5 * 5), 'days'); - - const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants); - expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0); - }); - it('behaves like extraMonths is 0 if it\'s set to a negative number', () => { - plan.extraMonths = -5; - const expectedTerminationDate = moment(); - const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants); - expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0); - }); -}); diff --git a/test/api/unit/models/group.test.js b/test/api/unit/models/group.test.js index c75d1dc7fc..5cb124d96f 100644 --- a/test/api/unit/models/group.test.js +++ b/test/api/unit/models/group.test.js @@ -2402,29 +2402,29 @@ describe('Group Model', () => { }); }); - context('hasActiveGroupPlan', () => { + context('isSubscribed', () => { it('returns false if group does not have customer id', () => { - expect(party.hasActiveGroupPlan()).to.be.undefined; + expect(party.isSubscribed()).to.be.undefined; }); it('returns true if group does not have plan.dateTerminated', () => { party.purchased.plan.customerId = 'test-id'; - expect(party.hasActiveGroupPlan()).to.be.true; + 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.hasActiveGroupPlan()).to.be.true; + 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.hasActiveGroupPlan()).to.be.false; + expect(party.isSubscribed()).to.be.false; }); }); diff --git a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js b/test/api/v3/integration/groups/POST-groups_groupId_leave.js similarity index 86% rename from test/api/v3/integration/groups/POST-groups_groupId_leave.test.js rename to test/api/v3/integration/groups/POST-groups_groupId_leave.js index fea70d3176..c4764e3428 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_leave.js @@ -2,7 +2,6 @@ import { v4 as generateUUID } from 'uuid'; import { each, } from 'lodash'; -import moment from 'moment'; import { generateChallenge, checkExistence, @@ -13,7 +12,6 @@ import { } from '../../../../helpers/api-integration/v3'; import { model as User } from '../../../../../website/server/models/user'; import payments from '../../../../../website/server/libs/payments/payments'; -import { calculateSubscriptionTerminationDate } from '../../../../../website/server/libs/payments/util'; describe('POST /groups/:groupId/leave', () => { const typesOfGroups = { @@ -340,48 +338,4 @@ describe('POST /groups/:groupId/leave', () => { }); }); }); - - each(typesOfGroups, (groupDetails, groupType) => { - context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => { - const extraMonths = 12; - let groupWithPlan; - let member; - - beforeEach(async () => { - const { group, members } = await createAndPopulateGroup({ - groupDetails, - members: 1, - upgradeToGroupPlan: true, - }); - [member] = members; - groupWithPlan = group; - await member.update({ - 'purchased.plan.extraMonths': extraMonths, - }); - }); - it('calculates dateTerminated and sets extraMonths to zero after user leaves the group', async () => { - const userBeforeLeave = await User.findById(member._id).exec(); - - await member.post(`/groups/${groupWithPlan._id}/leave`); - const userAfterLeave = await User.findById(member._id).exec(); - - const dateTerminatedBefore = userBeforeLeave.purchased.plan.dateTerminated; - const extraMonthsBefore = userBeforeLeave.purchased.plan.extraMonths; - const dateTerminatedAfter = userAfterLeave.purchased.plan.dateTerminated; - const extraMonthsAfter = userAfterLeave.purchased.plan.extraMonths; - - const expectedTerminationDate = calculateSubscriptionTerminationDate(null, { - customerId: payments.constants.GROUP_PLAN_CUSTOMER_ID, - extraMonths, - }, payments.constants); - - expect(extraMonthsBefore).to.gte(12); - expect(extraMonthsAfter).to.equal(0); - expect(dateTerminatedBefore).to.be.null; - expect(dateTerminatedAfter).to.exist; - - expect(moment(dateTerminatedAfter).diff(expectedTerminationDate, 'days')).to.equal(0); - }); - }); - }); }); diff --git a/test/helpers/api-integration/v3/object-generators.js b/test/helpers/api-integration/v3/object-generators.js index b48dd862a9..e713c1084c 100644 --- a/test/helpers/api-integration/v3/object-generators.js +++ b/test/helpers/api-integration/v3/object-generators.js @@ -5,8 +5,6 @@ import { v4 as generateUUID } from 'uuid'; import { ApiUser, ApiGroup, ApiChallenge } from '../api-classes'; import { requester } from '../requester'; import * as Tasks from '../../../../website/server/models/task'; -import payments from '../../../../website/server/libs/payments/payments'; -import { model as User } from '../../../../website/server/models/user'; // Creates a new user and returns it // If you need the user to have specific requirements, @@ -85,35 +83,14 @@ export async function generateGroup (leader, details = {}, update = {}) { return apiGroup; } -async function _upgradeToGroupPlan (groupLeader, group) { - const groupLeaderModel = await User.findById(groupLeader._id).exec(); - - // Create subscription - const paymentData = { - user: groupLeaderModel, - groupId: group._id, - sub: { - key: 'basic_3mo', - }, - customerId: 'customer-id', - paymentMethod: 'Payment Method', - headers: { - 'x-client': 'habitica-web', - 'user-agent': '', - }, - }; - await payments.createSubscription(paymentData); -} - // This is generate group + the ability to create // real users to populate it. The settings object // takes in: // members: Number - the number of group members to create. // Defaults to 0. Does not include group leader. -// invites: Number - the number of users to create and invite to the group. Defaults to 0. +// inivtes: Number - the number of users to create and invite to the group. Defaults to 0. // groupDetails: Object - how to initialize the group // leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user -// addGroupPlan: boolean - will add group plan with basic subscription. Defaults to false // can create the group // // Returns an object with @@ -124,7 +101,6 @@ async function _upgradeToGroupPlan (groupLeader, group) { export async function createAndPopulateGroup (settings = {}) { const numberOfMembers = settings.members || 0; const numberOfInvites = settings.invites || 0; - const upgradeToGroupPlan = settings.upgradeToGroupPlan || false; const { groupDetails } = settings; const leaderDetails = settings.leaderDetails || { balance: 10 }; @@ -154,10 +130,6 @@ export async function createAndPopulateGroup (settings = {}) { await Promise.all(invitees.map(invitee => invitee.sync())); - if (upgradeToGroupPlan) { - await _upgradeToGroupPlan(groupLeader, group); - } - return { groupLeader, group, diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index f00e90f497..f411c9db1f 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -858,6 +858,16 @@ api.leaveGroup = { _removeMessagesFromMember(user, group._id); await user.save(); + if (group.type !== 'party') { + const guildIndex = user.guilds.indexOf(group._id); + if (guildIndex >= 0) user.guilds.splice(guildIndex, 1); + } + + const isMemberOfGroupPlan = await user.isMemberOfGroupPlan(); + if (!isMemberOfGroupPlan) { + await payments.cancelGroupSubscriptionForUser(user, group); + } + if (group.hasNotCancelled()) await group.updateGroupPlan(true); res.respond(200, {}); }, @@ -1307,7 +1317,7 @@ api.getGroupPlans = { .select('leaderOnly leader purchased name managers') .exec(); - const groupPlans = groups.filter(group => group.hasActiveGroupPlan()); + const groupPlans = groups.filter(group => group.isSubscribed()); res.respond(200, groupPlans); }, diff --git a/website/server/libs/chat/group-chat.js b/website/server/libs/chat/group-chat.js index dc6b0d8278..1a864b0f47 100644 --- a/website/server/libs/chat/group-chat.js +++ b/website/server/libs/chat/group-chat.js @@ -10,7 +10,7 @@ const questScrolls = shared.content.quests; // @TODO: Don't use this method when the group can be saved. export async function getGroupChat (group) { - const maxChatCount = group.hasActiveGroupPlan() ? MAX_SUBBED_GROUP_CHAT_COUNT : MAX_CHAT_COUNT; + const maxChatCount = group.isSubscribed() ? MAX_SUBBED_GROUP_CHAT_COUNT : MAX_CHAT_COUNT; const groupChat = await Chat.find({ groupId: group._id }) .limit(maxChatCount) diff --git a/website/server/libs/invites/index.js b/website/server/libs/invites/index.js index 1679b1bf73..58474510fb 100644 --- a/website/server/libs/invites/index.js +++ b/website/server/libs/invites/index.js @@ -73,7 +73,7 @@ function inviteUserToGuild (userToInvite, group, inviter, publicGuild, res) { publicGuild, }; - if (group.hasActiveGroupPlan() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true; + if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true; userToInvite.invitations.guilds.push(guildInvite); } @@ -94,7 +94,7 @@ async function inviteUserToParty (userToInvite, group, inviter, res) { } const partyInvite = { id: group._id, name: group.name, inviter: inviter._id }; - if (group.hasActiveGroupPlan() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true; + if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true; userToInvite.invitations.parties.push(partyInvite); userToInvite.invitations.party = partyInvite; @@ -167,7 +167,7 @@ async function inviteByEmail (invite, group, inviter, req, res) { userReturnInfo = invite.email; let cancelledPlan = false; - if (group.hasActiveGroupPlan() && !group.hasNotCancelled()) cancelledPlan = true; + if (group.isSubscribed() && !group.hasNotCancelled()) cancelledPlan = true; const groupQueryString = JSON.stringify({ id: group._id, diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 4718ab7f02..fbf81b90fb 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -17,7 +17,6 @@ import { } from '../errors'; import shared from '../../../common'; import { sendNotification as sendPushNotification } from '../pushNotifications'; // eslint-disable-line import/no-cycle -import { calculateSubscriptionTerminationDate } from './util'; // @TODO: Abstract to shared/constant const JOINED_GROUP_PLAN = 'joined group plan'; @@ -74,7 +73,6 @@ async function createSubscription (data) { let itemPurchased = 'Subscription'; let purchaseType = 'subscribe'; let emailType = 'subscription-begins'; - let recipientIsSubscribed = recipient.isSubscribed(); // If we are buying a group subscription if (data.groupId) { @@ -95,7 +93,6 @@ async function createSubscription (data) { itemPurchased = 'Group-Subscription'; purchaseType = 'group-subscribe'; emailType = 'group-subscription-begins'; - recipientIsSubscribed = group.hasActiveGroupPlan(); groupId = group._id; recipient.purchased.plan.quantity = data.sub.quantity; @@ -108,7 +105,7 @@ async function createSubscription (data) { if (plan.customerId && !plan.dateTerminated) { // User has active plan plan.extraMonths += months; } else { - if (!recipientIsSubscribed || !plan.dateUpdated) plan.dateUpdated = today; + if (!recipient.isSubscribed() || !plan.dateUpdated) plan.dateUpdated = today; if (moment(plan.dateTerminated).isAfter()) { plan.dateTerminated = moment(plan.dateTerminated).add({ months }).toDate(); } else { @@ -310,11 +307,24 @@ async function cancelSubscription (data) { if (data.cancellationReason && data.cancellationReason === JOINED_GROUP_PLAN) sendEmail = false; } + const now = moment(); + let defaultRemainingDays = 30; + if (plan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID) { + defaultRemainingDays = 2; sendEmail = false; // because group-member-cancel email has already been sent } - plan.dateTerminated = calculateSubscriptionTerminationDate(data.nextBill, plan, this.constants); + const remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days', true) : defaultRemainingDays; + if (plan.extraMonths < 0) plan.extraMonths = 0; + const extraDays = Math.ceil(30.5 * plan.extraMonths); + const nowStr = `${now.format('MM')}/${now.format('DD')}/${now.format('YYYY')}`; + const nowStrFormat = 'MM/DD/YYYY'; + + plan.dateTerminated = moment(nowStr, nowStrFormat) + .add({ days: remaining }) + .add({ days: extraDays }) + .toDate(); // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated plan.extraMonths = 0; diff --git a/website/server/libs/payments/util.js b/website/server/libs/payments/util.js deleted file mode 100644 index fb6b65daaa..0000000000 --- a/website/server/libs/payments/util.js +++ /dev/null @@ -1,32 +0,0 @@ -import moment from 'moment'; - -const DEFAULT_REMAINING_DAYS = 30; -const DEFAULT_REMAINING_DAYS_FOR_GROUP_PLAN = 2; - -/** - * paymentsApiConstants is provided as parameter because of a dependency cycle - * with subscriptions api which will occur if api.constants would be used directly - */ -export function calculateSubscriptionTerminationDate ( - nextBill, purchasedPlan, paymentsApiConstants, -) { - const defaultRemainingDays = ( - purchasedPlan.customerId === paymentsApiConstants.GROUP_PLAN_CUSTOMER_ID - ) ? DEFAULT_REMAINING_DAYS_FOR_GROUP_PLAN - : DEFAULT_REMAINING_DAYS; - const now = moment(); - - const remaining = nextBill - ? moment(nextBill).diff(new Date(), 'days', true) - : defaultRemainingDays; - - const extraMonths = Math.max(purchasedPlan.extraMonths, 0); - const extraDays = Math.ceil(30.5 * extraMonths); - const nowStr = `${now.format('MM')}/${now.format('DD')}/${now.format('YYYY')}`; - const nowStrFormat = 'MM/DD/YYYY'; - - return moment(nowStr, nowStrFormat) - .add({ days: remaining }) - .add({ days: extraDays }) - .toDate(); -} diff --git a/website/server/models/group.js b/website/server/models/group.js index f9548d994f..b1e4c439e3 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -153,7 +153,7 @@ schema.plugin(baseModel, { noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased', 'managers'], private: ['purchased.plan'], toJSONTransform (plainObj, originalDoc) { - if (plainObj.purchased) plainObj.purchased.active = originalDoc.hasActiveGroupPlan(); + if (plainObj.purchased) plainObj.purchased.active = originalDoc.isSubscribed(); }, }); @@ -1701,7 +1701,7 @@ schema.methods.checkChatSpam = function groupCheckChatSpam (user) { return false; }; -schema.methods.hasActiveGroupPlan = function hasActiveGroupPlan () { +schema.methods.isSubscribed = function isSubscribed () { const now = new Date(); const { plan } = this.purchased; return plan && plan.customerId @@ -1710,12 +1710,12 @@ schema.methods.hasActiveGroupPlan = function hasActiveGroupPlan () { schema.methods.hasNotCancelled = function hasNotCancelled () { const { plan } = this.purchased; - return Boolean(this.hasActiveGroupPlan() && !plan.dateTerminated); + return Boolean(this.isSubscribed() && !plan.dateTerminated); }; -schema.methods.hasCancelled = function hasCancelled () { +schema.methods.hasCancelled = function hasNotCancelled () { const { plan } = this.purchased; - return Boolean(this.hasActiveGroupPlan() && plan.dateTerminated); + return Boolean(this.isSubscribed() && plan.dateTerminated); }; schema.methods.updateGroupPlan = async function updateGroupPlan (removingMember) { diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index 70821e0b95..2bf8227e3a 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -477,13 +477,13 @@ schema.methods.canGetGems = async function canObtainGems () { const groups = await getUserGroupData(user); return groups - .every(g => !g.hasActiveGroupPlan() || g.leader === user._id || g.leaderOnly.getGems !== true); + .every(g => !g.isSubscribed() || g.leader === user._id || g.leaderOnly.getGems !== true); }; schema.methods.isMemberOfGroupPlan = async function isMemberOfGroupPlan () { const groups = await getUserGroupData(this); - return groups.some(g => g.hasActiveGroupPlan()); + return groups.some(g => g.isSubscribed()); }; schema.methods.isAdmin = function isAdmin () {