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);