From 930d875ae9d518b0b504ec97638e94c7296ad388 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Thu, 8 Aug 2024 10:36:50 +0200 Subject: [PATCH] begin refactoring --- test/api/unit/libs/cron.test.js | 40 ++++++------- test/api/unit/libs/payments/payments.test.js | 37 ++++++------ website/common/script/cron.js | 14 ----- website/server/libs/cron.js | 57 +------------------ website/server/libs/payments/subscriptions.js | 28 ++++----- website/server/models/subscriptionPlan.js | 37 +++--------- website/server/models/transaction.js | 2 +- 7 files changed, 61 insertions(+), 154 deletions(-) diff --git a/test/api/unit/libs/cron.test.js b/test/api/unit/libs/cron.test.js index 53d732b8ad..71c478563f 100644 --- a/test/api/unit/libs/cron.test.js +++ b/test/api/unit/libs/cron.test.js @@ -197,16 +197,14 @@ describe('cron', async () => { user.purchased.plan.dateTerminated = moment(new Date()).subtract({ days: 1 }); user.purchased.plan.consecutive.gemCapExtra = 20; user.purchased.plan.consecutive.count = 5; - user.purchased.plan.consecutive.offset = 1; await cron({ user, tasksByType, daysMissed, analytics, }); expect(user.purchased.plan.customerId).to.not.exist; - expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0); + expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(20); expect(user.purchased.plan.consecutive.count).to.equal(0); - expect(user.purchased.plan.consecutive.offset).to.equal(0); }); describe('for a 1-month recurring subscription', async () => { @@ -229,7 +227,7 @@ describe('cron', async () => { user1.purchased.plan.planId = 'basic'; user1.purchased.plan.consecutive.count = 0; user1.purchased.plan.perkMonthCount = 0; - user1.purchased.plan.consecutive.trinkets = 0; + user1.purchased.plan.consecutive.trinkets = 1; user1.purchased.plan.consecutive.gemCapExtra = 0; }); @@ -256,7 +254,7 @@ describe('cron', async () => { user: user1, tasksByType, daysMissed, analytics, }); expect(user1.purchased.plan.consecutive.count).to.equal(10); - expect(user1.purchased.plan.consecutive.trinkets).to.equal(10); + expect(user1.purchased.plan.consecutive.trinkets).to.equal(11); expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(20); }); }); @@ -281,7 +279,7 @@ describe('cron', async () => { user3.purchased.plan.perkMonthCount = 0; user3.purchased.plan.consecutive.count = 0; user3.purchased.plan.consecutive.trinkets = 1; - user3.purchased.plan.consecutive.gemCapExtra = 5; + user3.purchased.plan.consecutive.gemCapExtra = 0; }); it('increments consecutive benefits', async () => { @@ -304,7 +302,7 @@ describe('cron', async () => { user: user3, tasksByType, daysMissed, analytics, }); expect(user3.purchased.plan.consecutive.count).to.equal(10); - expect(user3.purchased.plan.consecutive.trinkets).to.equal(10); + expect(user3.purchased.plan.consecutive.trinkets).to.equal(11); expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20); }); }); @@ -328,8 +326,8 @@ describe('cron', async () => { user6.purchased.plan.planId = 'google_6mo'; user6.purchased.plan.perkMonthCount = 0; user6.purchased.plan.consecutive.count = 0; - user6.purchased.plan.consecutive.trinkets = 2; - user6.purchased.plan.consecutive.gemCapExtra = 10; + user6.purchased.plan.consecutive.trinkets = 1; + user6.purchased.plan.consecutive.gemCapExtra = 0; }); it('increments benefits', async () => { @@ -362,8 +360,8 @@ describe('cron', async () => { user12.purchased.plan.dateUpdated = moment().toDate(); user12.purchased.plan.planId = 'basic_12mo'; user12.purchased.plan.consecutive.count = 0; - user12.purchased.plan.consecutive.trinkets = 4; - user12.purchased.plan.consecutive.gemCapExtra = 20; + user12.purchased.plan.consecutive.trinkets = 1; + user12.purchased.plan.consecutive.gemCapExtra = 26; it('increments consecutive benefits the month after the second paid period has started', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') @@ -373,7 +371,7 @@ describe('cron', async () => { user: user12, tasksByType, daysMissed, analytics, }); expect(user12.purchased.plan.consecutive.count).to.equal(1); - expect(user12.purchased.plan.consecutive.trinkets).to.equal(13); + expect(user12.purchased.plan.consecutive.trinkets).to.equal(2); expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(26); }); @@ -385,8 +383,8 @@ describe('cron', async () => { user: user12, tasksByType, daysMissed, analytics, }); expect(user12.purchased.plan.consecutive.count).to.equal(10); - expect(user12.purchased.plan.consecutive.trinkets).to.equal(22); - expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(65); + expect(user12.purchased.plan.consecutive.trinkets).to.equal(11); + expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(26); }); }); @@ -410,7 +408,7 @@ describe('cron', async () => { user3g.purchased.plan.planId = null; user3g.purchased.plan.consecutive.count = 0; user3g.purchased.plan.consecutive.trinkets = 1; - user3g.purchased.plan.consecutive.gemCapExtra = 5; + user3g.purchased.plan.consecutive.gemCapExtra = 0; it('increments benefits', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') @@ -419,10 +417,9 @@ describe('cron', async () => { await cron({ user: user3g, tasksByType, daysMissed, analytics, }); - expect(user3g.purchased.plan.consecutive.count).to.equal(3); - expect(user3g.purchased.plan.consecutive.offset).to.equal(0); - expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1); - expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5); + expect(user3g.purchased.plan.consecutive.count).to.equal(1); + expect(user3g.purchased.plan.consecutive.trinkets).to.equal(2); + expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(2); }); it('does not increment consecutive benefits in the month after the gift subscription has ended', async () => { @@ -434,9 +431,8 @@ describe('cron', async () => { }); // subscription has been erased by now expect(user3g.purchased.plan.consecutive.count).to.equal(0); - expect(user3g.purchased.plan.consecutive.offset).to.equal(0); - expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1); - expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(0); // erased + expect(user3g.purchased.plan.consecutive.trinkets).to.equal(2); + expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(2); }); }); }); diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js index 5fc08e9f19..62bb67b51d 100644 --- a/test/api/unit/libs/payments/payments.test.js +++ b/test/api/unit/libs/payments/payments.test.js @@ -13,7 +13,7 @@ import { import * as worldState from '../../../../../website/server/libs/worldState'; import { TransactionModel } from '../../../../../website/server/models/transaction'; -describe('payments/index', () => { +describe.only('payments/index', () => { let user; let group; let data; @@ -108,10 +108,6 @@ describe('payments/index', () => { }); it('add a transaction entry to the recipient', async () => { - recipient.purchased.plan = plan; - - expect(recipient.purchased.plan.extraMonths).to.eql(0); - await api.createSubscription(data); expect(recipient.purchased.plan.extraMonths).to.eql(3); @@ -199,13 +195,13 @@ describe('payments/index', () => { it('gives user 1 hourglass if they have no active subscription', async () => { await api.createSubscription(data); - expect(recipient.purchased.plan.trinket).to.eql(1); + expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1); }); - it('does not giec any hourglasses if they have an active subscription', async () => { + it('does not give any hourglasses if they have an active subscription', async () => { recipient.purchased.plan = plan; await api.createSubscription(data); - expect(recipient.purchased.plan.trinket).to.eql(plan.trinket); + expect(recipient.purchased.plan.consecutive.trinkets).to.eql(plan.consecutive.trinkets); }); it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => { @@ -666,6 +662,15 @@ describe('payments/index', () => { expect(user.purchased.plan.consecutive.trinkets).to.eql(1); }); + it('adds 1 plan.consecutive.trinkets for 12 month block if they had promo', async () => { + user.purchased.plan.hourGlassPromoReceived = new Date(); + data.sub.key = 'basic_12mo'; + + await api.createSubscription(data); + + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + }); + it('adds 12 plan.consecutive.trinkets for 12 month block', async () => { data.sub.key = 'basic_12mo'; @@ -944,7 +949,7 @@ describe('payments/index', () => { expect(user.purchased.plan.consecutive.trinkets).to.eql(6); }); - it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => { + it('Adds 11 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => { expect(user.purchased.plan.planId).to.not.exist; await api.createSubscription(data); @@ -958,17 +963,17 @@ describe('payments/index', () => { clock = sinon.useFakeTimers(new Date('2022-03-03')); await api.createSubscription(data); expect(user.purchased.plan.planId).to.eql('basic_12mo'); - expect(user.purchased.plan.consecutive.trinkets).to.eql(5); + expect(user.purchased.plan.consecutive.trinkets).to.eql(12); }); - it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => { + it('Adds 11 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => { data.sub.key = 'basic_earned'; expect(user.purchased.plan.planId).to.not.exist; await api.createSubscription(data); expect(user.purchased.plan.planId).to.eql('basic_earned'); - expect(user.purchased.plan.consecutive.trinkets).to.eql(0); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); data.sub.key = 'basic_6mo'; data.updatedFrom.key = 'basic_earned'; @@ -976,17 +981,17 @@ describe('payments/index', () => { clock = sinon.useFakeTimers(new Date('2022-05-28')); await api.createSubscription(data); expect(user.purchased.plan.planId).to.eql('basic_6mo'); - expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + expect(user.purchased.plan.consecutive.trinkets).to.eql(12); }); - it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => { + it('Adds 11 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => { data.sub.key = 'basic_6mo'; expect(user.purchased.plan.planId).to.not.exist; await api.createSubscription(data); expect(user.purchased.plan.planId).to.eql('basic_6mo'); - expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); data.sub.key = 'basic_12mo'; data.updatedFrom.key = 'basic_6mo'; @@ -994,7 +999,7 @@ describe('payments/index', () => { clock = sinon.useFakeTimers(new Date('2023-05-28')); await api.createSubscription(data); expect(user.purchased.plan.planId).to.eql('basic_12mo'); - expect(user.purchased.plan.consecutive.trinkets).to.eql(6); + expect(user.purchased.plan.consecutive.trinkets).to.eql(12); }); it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => { diff --git a/website/common/script/cron.js b/website/common/script/cron.js index e66cd114e0..bf76e0e160 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -8,7 +8,6 @@ import defaults from 'lodash/defaults'; import invert from 'lodash/invert'; import moment from 'moment'; import 'moment-recur'; -import subscriptionBlocks from './content/subscriptionBlocks'; export const DAY_MAPPING = { 0: 'su', @@ -287,23 +286,10 @@ export function getPlanContext (user, now) { const dateUpdatedMoment = moment(plan.dateUpdated).startOf('month'); const elapsedMonths = moment(subscriptionEndDate).diff(dateUpdatedMoment, 'months'); - const planMonths = subscriptionBlocks[plan.planId] ? subscriptionBlocks[plan.planId].months : 1; - let monthsTillNextHourglass; - if (planMonths > 1) { - monthsTillNextHourglass = Number(plan.consecutive.offset) + 1; - } else { - monthsTillNextHourglass = 3 - plan.perkMonthCount; - } - - const possibleNextHourglassDate = moment(plan.dateUpdated) - .add(monthsTillNextHourglass, 'months'); - return { plan, subscriptionEndDate, dateUpdatedMoment, elapsedMonths, - offset: plan.consecutive.offset, // months until the new hourglass is added - nextHourglassDate: possibleNextHourglassDate, }; } diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index 39d548de2f..07a669840c 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -15,7 +15,6 @@ const { shouldDo, i18n, getPlanContext, - getPlanMonths, } = common; const { scoreTask } = common.ops; const { loginIncentives } = common.content; @@ -67,59 +66,11 @@ async function grantEndOfTheMonthPerks (user, now) { if (elapsedMonths > 0) { plan.dateUpdated = now; - // For every month, inc their "consecutive months" counter. - // Give perks based on consecutive blocks - // If they already got perks for those blocks (eg, 6mo subscription, - // subscription gifts, etc) - then dec the offset until it hits 0 - // Award mystery items revealMysteryItems(user, elapsedMonths); - // 1 for one-month recurring or gift subscriptions; later set to 3 for 3-month recurring, etc. - let planMonthsLength = 1; - - for (let i = 0; i < elapsedMonths; i += 1) { - plan.consecutive.count += 1; - - plan.consecutive.offset -= 1; - // If offset is now greater than 0, the user is within a period - // for which they have already been given the consecutive months perks. - // - // If offset now equals 0, this is the final month for which - // the user has already been given the consecutive month perks. - // We do not give them more perks yet because they might cancel - // the subscription before the next payment is taken. - // - // If offset is now less than 0, the user EITHER has - // a single-month recurring subscription and MIGHT be due for perks, - // OR has a multi-month subscription that renewed some time - // in the previous calendar month and so they are due for a new set of perks - // (strictly speaking, they should have been given the perks - // at the time that next payment was taken, but we don't have support for - // tracking payments like that - giving the perks when offset is < 0 is a workaround). - - if (plan.consecutive.offset < 0) { - if (plan.planId) { - planMonthsLength = getPlanMonths(plan); - } - - if (planMonthsLength === 1) { - plan.consecutive.offset = 0; // allow the same logic to be run next month - } else { - // User has a multi-month recurring subscription - // and it renewed in the previous calendar month. - // don't need to check for perks again for this many months - // (subtract 1 because we should have run this when the payment was taken last month) - plan.consecutive.offset = planMonthsLength - 1; - } - if (!plan.gift && plan.customerId.indexOf('Gift') === -1) { - // Don't process gifted subs here, since they already got their perks. - - // eslint-disable-next-line no-await-in-loop - await plan.incrementPerkCounterAndReward(user._id, planMonthsLength); - } - } - } + plan.consecutive.count += elapsedMonths; + await plan.rewardPerks(user._id, elapsedMonths); } } @@ -135,8 +86,6 @@ function removeTerminatedSubscription (user) { _.merge(plan.consecutive, { count: 0, - offset: 0, - gemCapExtra: 0, }); user.markModified('purchased.plan'); @@ -283,8 +232,6 @@ export async function cron (options = {}) { if (user.isSubscribed()) { await grantEndOfTheMonthPerks(user, now); - } if (!user.isSubscribed() && user.purchased.plan.perkMonthCount > 0) { - user.purchased.plan.perkMonthCount = 0; } const { plan } = user.purchased; diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 33dd3421e7..6223cc473e 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -249,24 +249,7 @@ async function createSubscription (data) { itemPurchased, purchaseType, emailType, - isNewSubscription, } = await prepareSubscriptionValues(data); - - // Block sub perks - if (months > 1 && (!data.gift || !isNewSubscription)) { - if (!data.gift && !groupId) { - plan.consecutive.offset = block.months; - } - } else if (months === 1) { - plan.consecutive.offset = 0; - } - if (months > 1 || data.gift) { - await plan.incrementPerkCounterAndReward(recipient._id, months); - } else { - // Make sure the perkMonthCount field is initialized. - await plan.incrementPerkCounterAndReward(recipient._id, 0); - } - if (recipient !== group) { recipient.items.pets['Jackalope-RoyalPurple'] = 5; recipient.markModified('items.pets'); @@ -278,6 +261,17 @@ async function createSubscription (data) { txnEmail(data.user, emailType); } + if (months === 12) { + plan.consecutive.gemCapExtra = 26; + } + + if (months === 12 && autoRenews && !recipient.purchased.plan.hourglassPromoReceived) { + recipient.purchased.plan.hourglassPromoReceived = new Date(); + await plan.updateHourglasses(recipient._id, 12, '12_month_subscription'); + } else if (!data.gift || (data.gift && !recipient.isSubscribed())) { + await plan.updateHourglasses(recipient._id, 1, 'subscribed'); + } + if (!group && !data.promo) data.user.purchased.txnCount += 1; if (!data.promo) { diff --git a/website/server/models/subscriptionPlan.js b/website/server/models/subscriptionPlan.js index a6c540a10c..ed1e110d1e 100644 --- a/website/server/models/subscriptionPlan.js +++ b/website/server/models/subscriptionPlan.js @@ -3,9 +3,6 @@ import validator from 'validator'; import baseModel from '../libs/baseModel'; import { TransactionModel as Transaction } from './transaction'; -// multi-month subscriptions are for multiples of 3 months -const SUBSCRIPTION_BASIC_BLOCK_LENGTH = 3; - export const schema = new mongoose.Schema({ planId: String, subscriptionId: String, @@ -18,7 +15,6 @@ export const schema = new mongoose.Schema({ dateUpdated: Date, dateCurrentTypeCreated: Date, extraMonths: { $type: Number, default: 0 }, - perkMonthCount: { $type: Number, default: -1 }, gemsBought: { $type: Number, default: 0 }, mysteryItems: { $type: Array, default: () => [] }, lastReminderDate: Date, // indicates the last time a subscription reminder was sent @@ -28,6 +24,7 @@ export const schema = new mongoose.Schema({ // indicates when the queue server should process this subscription again. nextPaymentProcessing: Date, nextBillingDate: Date, // Next time google will bill this user. + hourglassPromoReceived: Date, consecutive: { count: { $type: Number, default: 0 }, // when gifted subs, offset++ for each month. offset-- each new-month (cron). @@ -50,36 +47,18 @@ schema.plugin(baseModel, { _id: false, }); -schema.methods.incrementPerkCounterAndReward = async function incrementPerkCounterAndReward +schema.methods.rewardPerks = async function rewardPerks (userID, adding) { - let addingNumber = adding; + let perks = adding; if (typeof adding === 'string' || adding instanceof String) { - addingNumber = parseInt(adding, 10); + perks = parseInt(adding, 10); } - const isSingleMonthPlan = this.planId === 'basic_earned' || this.planId === 'group_plan_auto' || this.planId === 'group_monthly'; - // if perkMonthCount wasn't used before, initialize it. - if (this.perkMonthCount === undefined || this.perkMonthCount === -1) { - if (isSingleMonthPlan && this.consecutive.count > 0) { - this.perkMonthCount = (this.consecutive.count - 1) % SUBSCRIPTION_BASIC_BLOCK_LENGTH; - } else { - this.perkMonthCount = 0; - } - } else if (isSingleMonthPlan) { - const expectedPerkMonthCount = (this.consecutive.count - 1) % SUBSCRIPTION_BASIC_BLOCK_LENGTH; - if (this.perkMonthCount === (expectedPerkMonthCount - 1)) { - // User was affected by a bug that makes their perkMonthCount off by one - this.perkMonthCount += 1; - } - } - this.perkMonthCount += addingNumber; - const perks = Math.floor(this.perkMonthCount / 3); if (perks > 0) { - this.consecutive.gemCapExtra += 5 * perks; // 5 extra Gems every 3 months - // cap it at 50 (hard 25 limit + extra 25) - if (this.consecutive.gemCapExtra > 25) this.consecutive.gemCapExtra = 25; - this.perkMonthCount -= (perks * 3); - // one Hourglass every 3 months + this.consecutive.gemCapExtra += 2 * perks; // 2 extra Gems every month + // cap it at 50 (hard 24 limit + extra 26) + if (this.consecutive.gemCapExtra > 26) this.consecutive.gemCapExtra = 26; + // one Hourglass every month await this.updateHourglasses(userID, perks, 'subscription_perks'); // eslint-disable-line no-await-in-loop } }; diff --git a/website/server/models/transaction.js b/website/server/models/transaction.js index 608ddcae77..d97111588d 100644 --- a/website/server/models/transaction.js +++ b/website/server/models/transaction.js @@ -5,7 +5,7 @@ import baseModel from '../libs/baseModel'; const { Schema } = mongoose; export const currencies = ['gems', 'hourglasses']; -export const transactionTypes = ['buy_money', 'buy_gold', 'spend', 'gift_send', 'gifted_with_money', 'gift_receive', 'debug', 'create_challenge', 'create_bank_challenge', 'create_guild', 'change_class', 'rebirth', 'release_pets', 'release_mounts', 'reroll', 'contribution', 'subscription_perks', 'admin_update_balance', 'admin_update_hourglasses']; +export const transactionTypes = ['buy_money', 'buy_gold', 'spend', 'gift_send', 'gifted_with_money', 'gift_receive', 'debug', 'create_challenge', 'create_bank_challenge', 'create_guild', 'change_class', 'rebirth', 'release_pets', 'release_mounts', 'reroll', 'contribution', 'subscription_perks', 'subscribed', '12_month_subscription', 'admin_update_balance', 'admin_update_hourglasses']; export const schema = new Schema({ currency: { $type: String, enum: currencies, required: true },