From 6395070eb6f56191d6047a1c16bf0b9a58b9bc01 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 27 Nov 2019 12:10:56 -0600 Subject: [PATCH] Award mystery items at cron (#11519) * feat(subscriber): award mystery items at cron * fix(mystery): backfill skipped months * fix(mystery): adjust subscriber text * fix(mystery): correct moment logic and remove irrelevant tests * fix(mystery): build set one month in advance * fix(mystery): don't add empty set to Time Travelers * refactor(mystery): readability --- test/api/unit/libs/cron.test.js | 22 ++ test/api/unit/libs/payments/payments.test.js | 25 -- website/common/locales/en/generic.json | 2 +- website/common/script/content/mystery-sets.js | 294 +----------------- .../common/script/content/time-travelers.js | 1 + website/server/libs/cron.js | 4 + website/server/libs/payments/subscriptions.js | 21 +- 7 files changed, 59 insertions(+), 310 deletions(-) diff --git a/test/api/unit/libs/cron.test.js b/test/api/unit/libs/cron.test.js index 5f6c6c95d8..e923cb0f9c 100644 --- a/test/api/unit/libs/cron.test.js +++ b/test/api/unit/libs/cron.test.js @@ -88,6 +88,28 @@ describe('cron', () => { user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate(); }); + it('awards current mystery items to subscriber', () => { + user.purchased.plan.dateUpdated = new Date('2018-12-11'); + clock = sinon.useFakeTimers(new Date('2019-01-29')); + cron({ + user, tasksByType, daysMissed, analytics, + }); + expect(user.purchased.plan.mysteryItems.length).to.eql(2); + expect(user.notifications.length).to.eql(3); + expect(user.notifications[0].type).to.eql('NEW_MYSTERY_ITEMS'); + }); + + it('awards multiple mystery item sets if user skipped months between logins', () => { + user.purchased.plan.dateUpdated = new Date('2018-11-11'); + clock = sinon.useFakeTimers(new Date('2019-01-29')); + cron({ + user, tasksByType, daysMissed, analytics, + }); + expect(user.purchased.plan.mysteryItems.length).to.eql(4); + expect(user.notifications.length).to.eql(3); + expect(user.notifications[0].type).to.eql('NEW_MYSTERY_ITEMS'); + }); + it('resets plan.gemsBought on a new month', () => { user.purchased.plan.gemsBought = 10; cron({ diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js index 76d0d3d00e..fc87f9dfe2 100644 --- a/test/api/unit/libs/payments/payments.test.js +++ b/test/api/unit/libs/payments/payments.test.js @@ -439,31 +439,6 @@ describe('payments/index', () => { fakeClock.restore(); }); - it('does not awards mystery items when not within the timeframe for a mystery item', async () => { - const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016 - const fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe); - data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } }; - - await api.createSubscription(data); - - expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0); - - fakeClock.restore(); - }); - - it('does not add a notification for mystery items if none was awarded', async () => { - const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016 - const fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe); - data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } }; - - await api.createSubscription(data); - - expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0); - expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.be.undefined; - - fakeClock.restore(); - }); - it('does not award mystery item when user already owns the item', async () => { const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016 const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe); diff --git a/website/common/locales/en/generic.json b/website/common/locales/en/generic.json index e5b7c3f485..9473a10e87 100644 --- a/website/common/locales/en/generic.json +++ b/website/common/locales/en/generic.json @@ -67,7 +67,7 @@ "newGroupTitle": "New Group", "subscriberItem": "Mystery Item", "newSubscriberItem": "You have new Mystery Items", - "subscriberItemText": "Each month, subscribers will receive a mystery item. This is usually released about one week before the end of the month. See the wiki's 'Mystery Item' page for more information.", + "subscriberItemText": "Each month, subscribers will receive a mystery item. It becomes available at the beginning of the month. See the wiki's 'Mystery Item' page for more information.", "all": "All", "none": "None", "more": "<%= count %> more", diff --git a/website/common/script/content/mystery-sets.js b/website/common/script/content/mystery-sets.js index 21e33b1bcf..3e3378e610 100644 --- a/website/common/script/content/mystery-sets.js +++ b/website/common/script/content/mystery-sets.js @@ -1,287 +1,8 @@ import each from 'lodash/each'; +import moment from 'moment'; import t from './translation'; const mysterySets = { - 201402: { - start: '2014-02-22', - end: '2014-02-28', - }, - 201403: { - start: '2014-03-24', - end: '2014-04-02', - }, - 201404: { - start: '2014-04-24', - end: '2014-05-02', - }, - 201405: { - start: '2014-05-21', - end: '2014-06-02', - }, - 201406: { - start: '2014-06-23', - end: '2014-07-02', - }, - 201407: { - start: '2014-07-23', - end: '2014-08-02', - }, - 201408: { - start: '2014-08-23', - end: '2014-09-02', - }, - 201409: { - start: '2014-09-24', - end: '2014-10-02', - }, - 201410: { - start: '2014-10-24', - end: '2014-11-02', - }, - 201411: { - start: '2014-11-24', - end: '2014-12-02', - }, - 201412: { - start: '2014-12-25', - end: '2015-01-02', - }, - 201501: { - start: '2015-01-26', - end: '2015-02-02', - }, - 201502: { - start: '2015-02-24', - end: '2015-03-02', - }, - 201503: { - start: '2015-03-25', - end: '2015-04-02', - }, - 201504: { - start: '2015-04-24', - end: '2015-05-02', - }, - 201505: { - start: '2015-05-25', - end: '2015-06-02', - }, - 201506: { - start: '2015-06-25', - end: '2015-07-02', - }, - 201507: { - start: '2015-07-24', - end: '2015-08-02', - }, - 201508: { - start: '2015-08-23', - end: '2015-09-02', - }, - 201509: { - start: '2015-09-24', - end: '2015-10-02', - }, - 201510: { - start: '2015-10-26', - end: '2015-11-02', - }, - 201511: { - start: '2015-11-25', - end: '2015-12-02', - }, - 201512: { - start: '2015-12-23', - end: '2016-01-02', - }, - 201601: { - start: '2016-01-26', - end: '2016-02-02', - }, - 201602: { - start: '2016-02-24', - end: '2016-03-02', - }, - 201603: { - start: '2016-03-24', - end: '2016-04-02', - }, - 201604: { - start: '2016-04-25', - end: '2016-05-02', - }, - 201605: { - start: '2016-05-25', - end: '2016-06-02', - }, - 201606: { - start: '2016-06-23', - end: '2016-07-02', - }, - 201607: { - start: '2016-07-26', - end: '2016-08-02', - }, - 201608: { - start: '2016-08-23', - end: '2016-09-02', - }, - 201609: { - start: '2016-09-22', - end: '2016-10-02', - }, - 201610: { - start: '2016-10-25', - end: '2016-11-02', - }, - 201611: { - start: '2016-11-22', - end: '2016-12-02', - }, - 201612: { - start: '2016-12-21', - end: '2017-01-02', - }, - 201701: { - start: '2017-01-24', - end: '2017-02-02', - }, - 201702: { - start: '2017-02-21', - end: '2017-03-02', - }, - 201703: { - start: '2017-03-23', - end: '2017-04-02', - }, - 201704: { - start: '2017-04-25', - end: '2017-05-02', - }, - 201705: { - start: '2017-05-23', - end: '2017-06-02', - }, - 201706: { - start: '2017-06-22', - end: '2017-07-02', - }, - 201707: { - start: '2017-07-25', - end: '2017-08-02', - }, - 201708: { - start: '2017-08-17', - end: '2017-09-02', - }, - 201709: { - start: '2017-09-19', - end: '2017-10-07', - }, - 201710: { - start: '2017-10-24', - end: '2017-11-02', - }, - 201711: { - start: '2017-11-17', - end: '2017-12-02', - }, - 201712: { - start: '2017-12-21', - end: '2018-01-02', - }, - 201801: { - start: '2018-01-23', - end: '2018-02-02', - }, - 201802: { - start: '2018-02-22', - end: '2018-03-02', - }, - 201803: { - start: '2018-03-22', - end: '2018-04-02', - }, - 201804: { - start: '2018-04-24', - end: '2018-05-02', - }, - 201805: { - start: '2018-05-24', - end: '2018-06-02', - }, - 201806: { - start: '2018-06-21', - end: '2018-07-02', - }, - 201807: { - start: '2018-07-24', - end: '2018-08-02', - }, - 201808: { - start: '2018-08-23', - end: '2018-09-02', - }, - 201809: { - start: '2018-09-25', - end: '2018-10-02', - }, - 201810: { - start: '2018-10-25', - end: '2018-11-02', - }, - 201811: { - start: '2018-11-28', - end: '2018-12-4', - }, - 201812: { - start: '2018-12-19', - end: '2019-01-02', - }, - 201901: { - start: '2019-01-28', - end: '2019-02-02', - }, - 201902: { - start: '2019-02-25', - end: '2019-03-02', - }, - 201903: { - start: '2019-03-26', - end: '2019-04-02', - }, - 201904: { - start: '2019-04-25', - end: '2019-05-02', - }, - 201905: { - start: '2019-05-28', - end: '2019-06-02', - }, - 201906: { - start: '2019-06-25', - end: '2019-07-03', - }, - 201907: { - start: '2019-07-25', - end: '2019-08-02', - }, - 201908: { - start: '2019-08-27', - end: '2019-09-02', - }, - 201909: { - start: '2019-09-25', - end: '2019-10-02', - }, - 201910: { - start: '2019-10-24', - end: '2019-11-02', - }, - 201911: { - start: '2019-11-21', - end: '2019-12-02', - }, 301404: { start: '3014-03-24', end: '3014-04-02', @@ -304,6 +25,19 @@ const mysterySets = { end: '2014-04-01', }, }; +const FIRST_MYSTERY_SET = moment('2014-02-01'); + +for ( + let mysteryMonth = FIRST_MYSTERY_SET; + moment(mysteryMonth).startOf('month').isSameOrBefore(moment().add(1, 'months')); + mysteryMonth = moment(mysteryMonth).add(1, 'months') +) { + const setKey = moment(mysteryMonth).format('YYYYMM'); + mysterySets[setKey] = { + start: moment(mysteryMonth).startOf('month').format('YYYY-MM-DD'), + end: moment(mysteryMonth).endOf('month').format('YYYY-MM-DD'), + }; +} each(mysterySets, (value, key) => { value.key = key; diff --git a/website/common/script/content/time-travelers.js b/website/common/script/content/time-travelers.js index 4a83330399..49f0aa58e2 100644 --- a/website/common/script/content/time-travelers.js +++ b/website/common/script/content/time-travelers.js @@ -13,6 +13,7 @@ each(mystery, (v, k) => { v.items = filter(gear.flat, { mystery: k, }); + if (v.items.length === 0) delete mystery[k]; }); const timeTravelerStore = user => { diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index b0882b9ab7..d11853a190 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -6,6 +6,7 @@ import { model as User } from '../models/user'; import common from '../../common'; import { preenUserHistory } from './preening'; import sleep from './sleep'; +import { revealMysteryItems } from './payments/subscriptions'; const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true'; const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true'; @@ -75,6 +76,9 @@ function grantEndOfTheMonthPerks (user, now) { count: 0, offset: 0, trinkets: 0, gemCapExtra: 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; diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index c02d15f7fc..b795ad4ecf 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -21,14 +21,13 @@ import { sendNotification as sendPushNotification } from '../pushNotifications'; // @TODO: Abstract to shared/constant const JOINED_GROUP_PLAN = 'joined group plan'; -function revealMysteryItems (user) { +function _findMysteryItems (user, dateMoment) { const pushedItems = []; - _.each(shared.content.gear.flat, item => { if ( item.klass === 'mystery' - && moment().isAfter(shared.content.mystery[item.mystery].start) - && moment().isBefore(shared.content.mystery[item.mystery].end) + && dateMoment.isSameOrAfter(shared.content.mystery[item.mystery].start) + && dateMoment.isSameOrBefore(moment(shared.content.mystery[item.mystery].end).endOf('day')) && !user.items.gear.owned[item.key] && user.purchased.plan.mysteryItems.indexOf(item.key) === -1 ) { @@ -36,6 +35,19 @@ function revealMysteryItems (user) { pushedItems.push(item.key); } }); + return pushedItems; +} + +function revealMysteryItems (user, elapsedMonths = 1) { + let monthsToCheck = elapsedMonths; + let pushedItems = []; + + do { + monthsToCheck -= 1; + pushedItems = pushedItems.concat(_findMysteryItems(user, moment().subtract(monthsToCheck, 'months'))); + } + while (monthsToCheck > 0); + if (pushedItems.length > 0) { user.addNotification('NEW_MYSTERY_ITEMS', { items: pushedItems }); } @@ -324,4 +336,5 @@ async function cancelSubscription (data) { export { createSubscription, cancelSubscription, + revealMysteryItems, };