From 7ea6c911cb660698a850695b0a2d85e95316ac55 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 15 Jun 2018 14:49:18 -0500 Subject: [PATCH] Better group plan member counts (#10449) * fix(group-plans): improved member count accuracy * fix(migration): don't leave server running after completion * fix(migration): don't update Stripe for non-Stripe methods Also fixes a linting issue. * fix(lint): no comma dangle here * fix(async): put async token in relevant spot * fix(lint): still more linting * fix(async): better handling for async and promises Also adds additional logging where discrepancies are found. * feat(migration): provide CSV output * fix(promises): better pause/resume * fix(migration): don't update already canceled subs * fix(groups): also address quantity/memberCount discrepancies * fix(migration): also log quantity issues * fix(migration): equation was reversed * refactor(migration): condense logic, add error catch * fix(migration): fix root cause of failed quantity update?? * fix(lint): gratuitous parens * fix(test): expect group to be updated db-side * fix(migration): actually update quantities? * fix(groups): roll back unneeded Stripe lib change, refactor migration --- config.json.example | 3 +- .../groups/reconcile-group-plan-members.js | 107 ++++++++++++++++++ migrations/tasks/tasks-set-everyX.js | 2 +- website/server/controllers/api-v3/groups.js | 1 + 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 migrations/groups/reconcile-group-plan-members.js diff --git a/config.json.example b/config.json.example index 1073ead7cb..0feacd1184 100644 --- a/config.json.example +++ b/config.json.example @@ -112,5 +112,6 @@ "CLOUDKARAFKA_USERNAME": "", "CLOUDKARAFKA_PASSWORD": "", "CLOUDKARAFKA_TOPIC_PREFIX": "" - } + }, + "MIGRATION_CONNECT_STRING": "mongodb://localhost:27017/habitrpg?auto_reconnect=true" } diff --git a/migrations/groups/reconcile-group-plan-members.js b/migrations/groups/reconcile-group-plan-members.js new file mode 100644 index 0000000000..06296090e4 --- /dev/null +++ b/migrations/groups/reconcile-group-plan-members.js @@ -0,0 +1,107 @@ +import monk from 'monk'; +import nconf from 'nconf'; +import stripePayments from '../../website/server/libs/payments/stripe'; + +/* + * Ensure that group plan billing is accurate by doing the following: + * 1. Correct the memberCount in all paid groups whose counts are wrong + * 2. Where the above uses Stripe, update their subscription counts in Stripe + * + * Provides output on what groups were fixed, which can be piped to CSV. + */ + +const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); + +let dbGroups = monk(CONNECTION_STRING).get('groups', { castIds: false }); +let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); + +async function fixGroupPlanMembers () { + console.info('Group ID, Customer ID, Plan ID, Quantity, Recorded Member Count, Actual Member Count'); + let groupPlanCount = 0; + let fixedGroupCount = 0; + + dbGroups.find( + { + $and: + [ + {'purchased.plan.planId': {$ne: null}}, + {'purchased.plan.planId': {$ne: ''}}, + {'purchased.plan.customerId': {$ne: 'cus_9f0DV4g7WHRzpM'}}, // Demo groups + {'purchased.plan.customerId': {$ne: 'cus_9maalqDOFTrvqx'}}, + ], + $or: + [ + {'purchased.plan.dateTerminated': null}, + {'purchased.plan.dateTerminated': ''}, + ], + }, + { + fields: { + memberCount: 1, + 'purchased.plan': 1, + }, + } + ).each(async (group, {close, pause, resume}) => { // eslint-disable-line no-unused-vars + pause(); + groupPlanCount++; + + const canonicalMemberCount = await dbUsers.count( + { + $or: + [ + {'party._id': group._id}, + {guilds: group._id}, + ], + } + ); + const incorrectMemberCount = group.memberCount !== canonicalMemberCount; + + const isMonthlyPlan = group.purchased.plan.planId === 'group_monthly'; + const quantityMismatch = group.purchased.plan.quantity !== group.memberCount + 2; + const incorrectQuantity = isMonthlyPlan && quantityMismatch; + + if (!incorrectMemberCount && !incorrectQuantity) { + resume(); + return; + } + + console.info(`${group._id}, ${group.purchased.plan.customerId}, ${group.purchased.plan.planId}, ${group.purchased.plan.quantity}, ${group.memberCount}, ${canonicalMemberCount}`); + + const groupUpdate = await dbGroups.update( + { _id: group._id }, + { + $set: { + memberCount: canonicalMemberCount, + }, + } + ); + + if (!groupUpdate) return; + + fixedGroupCount++; + if (group.purchased.plan.paymentMethod === 'Stripe') { + await stripePayments.chargeForAdditionalGroupMember(group); + await dbGroups.update( + {_id: group._id}, + {$set: {'purchased.plan.quantity': canonicalMemberCount + 2}} + ); + } + + if (incorrectQuantity) { + await dbGroups.update( + {_id: group._id}, + {$set: {'purchased.plan.quantity': canonicalMemberCount + 2}} + ); + } + + resume(); + }).then(() => { + console.info(`Fixed ${fixedGroupCount} out of ${groupPlanCount} active Group Plans`); + return process.exit(0); + }).catch((err) => { + console.log(err); + return process.exit(1); + }); +} + +module.exports = fixGroupPlanMembers; diff --git a/migrations/tasks/tasks-set-everyX.js b/migrations/tasks/tasks-set-everyX.js index f3bd5408d6..c507f80356 100644 --- a/migrations/tasks/tasks-set-everyX.js +++ b/migrations/tasks/tasks-set-everyX.js @@ -7,7 +7,7 @@ let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done */ let monk = require('monk'); -let connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true'; +let connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; let dbTasks = monk(connectionString).get('tasks', { castIds: false }); function processTasks (lastId) { diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 7e88d487b1..b5f4ab5d8c 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -196,6 +196,7 @@ api.createGroupPlan = { // @TODO: Change message if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate')); + group.memberCount = await User.count({ $or: [{ 'party._id': group._id }, { guilds: group._id }] }).exec(); group.leader = user._id; user.guilds.push(group._id);