habitica-self-host/website/common/script/cron.js
Kalista Payne fbf69a4a34 Squashed commit of the following:
commit dd0a410fa6c3741dc0d6793283cf4df3c37790a5
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Mon Nov 4 14:24:30 2024 -0600

    fix(subs): center next hourglass message

commit 72d92ffd76bb43fee8ba2bbabd211e595afbd664
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Fri Nov 1 14:17:59 2024 -0500

    fix(subs): don't hide HG preview entirely

commit ea0ecb0c3d519ed3d5c42266367eaaa7283ac5de
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Fri Nov 1 13:01:06 2024 -0500

    fix(subs): Google wording and HG escape

commit 2bd2c69e18e37c8c8c7106c62f186c372d25c5d2
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Fri Nov 1 09:25:30 2024 -0500

    fix(layout): tighten cancellation note

commit eb2fc40d241b18d4ffff04c35e744f05e6e9ff52
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 24 15:41:43 2024 -0500

    fix(g1g1): don't try to find Gems promo during bogo

commit d3eea86bd773c5236e8a0f619639e49db846c2ba
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 24 15:00:09 2024 -0500

    fix(subs): fix typeError

commit e3ae9a2d6736f238c6aaaec37a5bf38d64afafe8
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 24 13:57:27 2024 -0500

    fix(subs): also redirect to subs after gift sub

commit 690163a0dec3a45329062905c90454c7cd7c83fd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Oct 23 16:42:38 2024 +0200

    fix test

commit 2ad7541fc0de429c152e6824f65d2b11b84a9809
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Oct 23 16:34:52 2024 +0200

    fix test

commit 7e337a9e591f2e8b27684567290a70f1b2d58aa0
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Oct 23 11:54:15 2024 +0200

    remove only

commit 7462b8a57f852ecfc52e74fb50d6cff1751bef74
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Oct 23 11:51:25 2024 +0200

    fix bug with incorrectly giving HG bonus

commit acd6183e95a5783dfa29e6c2b142f965c3c67411
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Mon Oct 21 17:22:26 2024 -0500

    fix(subs): unhovery and un-12-monthy

commit 935e9fd6ec2688ac7339c56ce0ff03bfdae30c77
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Fri Oct 18 14:50:17 2024 -0500

    fix(subs): try again on gifts

commit 6e1fb7df38d90e5c3ccebee9bb86dbb8f8a4678f
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 17 18:19:20 2024 -0500

    fix(lint): do negate object ig

commit 71d434b94ea3b1a2c9381fd70f2e637473e00cac
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 17 18:15:11 2024 -0500

    fix(lint): unnecessary ternary

commit b90b0bb9c39b931714526a9d20910968b055038d
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 17 17:34:24 2024 -0500

    fix(subs): gifts DON't renew

commit 19469304c5a5881329ea1682e2070f9666d49ee4
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 17 17:13:29 2024 -0500

    fix(subs): pass autoRenews through Stripe

commit 6819e7b7e518969c58ebab4400f3147f0ddea1b3
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 17 16:03:25 2024 -0500

    fix(subscriptions): minor visual updates

commit 74633b5e5ea71d66681ad0e84873f3080ab5d361
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Wed Oct 16 17:27:09 2024 -0500

    fix(subscriptions): more gift layout revisions

commit a90ccb89de36a85acc214bb0b88479e0b78f1660
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Wed Oct 16 15:37:50 2024 -0500

    fix(subscription): update layout when gifting

commit c24b2db8dc6642669068f0a79d9b0990d43decb9
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Oct 14 16:11:46 2024 +0200

    fix issue with promo hourglasses

commit 7a61c72b47cd3403fe0f3edf91522277738068cc
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Oct 14 15:59:40 2024 +0200

    don’t give additional HG for new sub if they already got one this month

commit f14cb090265ed830eb76c7f452e806257312370e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Oct 14 10:38:01 2024 +0200

    Admin panel display fixes

commit f4cff698cfb80f9ad2da7ecb626f84277f97eb7c
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 3 17:58:59 2024 -0500

    fix(stripe): correct redirect after success

commit c468b58f3f783c58e9b48f9698b45473b526d3d4
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Oct 3 17:35:37 2024 -0500

    fix(subs): correct border-radius and redirect

commit 78fb9e31d64f25aa091e24f95f25dc6dedc844a6
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Wed Oct 2 17:41:49 2024 -0500

    fix(css): correct and refactor heights and selection states

commit e2babe8053a778b64d51bd3d18866e69fb326a3c
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Mon Sep 30 16:45:29 2024 -0500

    feat(subscription): max Gems progress readout

commit 61af8302a349f70d60886492b3d4f05dd5463a51
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Sep 27 15:11:22 2024 +0200

    fix test

commit ef8ff0ea9eebcbd682a34fd7f52722b92fdfae16
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Sep 27 14:14:44 2024 +0200

    show date for hourglass bonus if it was received

commit 4bafafdc8d493aad960dcf0d4957d3dad2d5e8da
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Sep 27 14:12:52 2024 +0200

    add new field for cumulative subscription count

commit 30096247b73bdb76aa5b10dd4c964a78d2511e69
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Sep 27 13:39:49 2024 +0200

    fix missing transaction type

commit 70872651b09613a8fe1a19ee2e19dac398b3134d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Sep 27 13:31:40 2024 +0200

    fix admin panel strings

commit f3398db65f26db558f38ecce8fe4795ff73650cb
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Thu Sep 26 23:11:16 2024 -0500

    WIP(subs): extant Stripe state

commit c6b2020109b2cdbc7dd8579c884c65f81e757c25
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Sep 26 11:41:55 2024 +0200

    fix admin panel display

commit d9afc96d2db8021db7e6310a009c15004ccc5c38
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Sep 26 11:40:16 2024 +0200

    Fix hourglass logic for upgrades

commit 6e2c8eeb649481afc349e6eb7741bcc82909c3c4
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Sep 25 17:48:54 2024 +0200

    fix hourglass count

commit cd752fbdce79f24bbdbaf6fd9558f207754c5cc3
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Fri Sep 20 12:24:21 2024 -0500

    WIP(frontend): draft of main subs page view

commit 0102b29d599e47192d7346180ecd549c79177156
Author: Kalista Payne <sabe@habitica.com>
Date:   Wed Sep 18 15:29:08 2024 -0500

    fix(admin): correct logic and style for shrimple subs

commit 5469a5c5c3fddcf611018c1de077de3499df787a
Author: Kalista Payne <sabe@habitica.com>
Date:   Wed Sep 18 15:07:36 2024 -0500

    fix(test): short circuit this.

commit 526193ee6c9d07915d0373d07bb8ee0554fe2614
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Sep 18 14:42:06 2024 +0200

    fix gem limit

commit 19cf1636aa1371147ea92478485a653d612d9755
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Aug 13 17:00:40 2024 +0200

    return nextHourglassDate again

commit eea36e3ed54633c345d628d1d3d08e03a3e416a3
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Aug 13 13:11:22 2024 +0200

    subscription test improvements

commit ca78e7433031e79c61aba67235481e0b1c569a55
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Aug 12 15:46:15 2024 +0200

    add more subscription tests

commit f4c4f93a081a89d4c79aec1e87dac97d90c1d587
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Aug 9 13:35:22 2024 +0200

    finish basic implementation of new logic

commit e036742048b92c2e2f29724fb02462f117d91aea
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Aug 9 11:37:44 2024 +0200

    cleanup

commit 643186568866ddea0a234b68d37ad4ab634bd147
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Aug 7 05:41:18 2024 -0400

    update cron tests

commit 930d875ae9d518b0b504ec97638e94c7296ad388
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Aug 8 10:36:50 2024 +0200

    begin refactoring

commit 96623608d064b94cfa40e5da736f13c696995df9
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Aug 6 16:28:16 2024 +0200

    begin removing obsolete tests
2024-11-14 12:31:57 -06:00

301 lines
11 KiB
JavaScript

// TODO what can be moved to /website/server?
/*
------------------------------------------------------
Cron and time / day functions
------------------------------------------------------
*/
import defaults from 'lodash/defaults';
import invert from 'lodash/invert';
import moment from 'moment';
import 'moment-recur';
export const DAY_MAPPING = {
0: 'su',
1: 'm',
2: 't',
3: 'w',
4: 'th',
5: 'f',
6: 's',
};
export const DAY_MAPPING_STRING_TO_NUMBER = invert(DAY_MAPPING);
/*
Each time we perform date maths (cron, task-due-days, etc), we need to consider user preferences.
Specifically {dayStart} (custom day start) and {timezoneOffset}.
This function sanitizes / defaults those values.
{now} is also passed in for various purposes,
one example being the test scripts scripts testing different "now" times.
*/
function sanitizeOptions (o) {
const ref = Number(o.dayStart || 0);
const dayStart = !Number.isNaN(ref) && ref >= 0 && ref <= 24 ? ref : 0;
let timezoneUtcOffset;
const timezoneUtcOffsetDefault = moment().utcOffset();
if (Number.isFinite(o.timezoneUtcOffset)) {
// Options were already sanitized
timezoneUtcOffset = o.timezoneUtcOffset;
} else if (Number.isFinite(o.timezoneUtcOffsetOverride)) {
timezoneUtcOffset = o.timezoneUtcOffsetOverride;
} else if (Number.isFinite(o.timezoneOffset)) {
timezoneUtcOffset = -o.timezoneOffset;
} else {
timezoneUtcOffset = timezoneUtcOffsetDefault;
}
if (timezoneUtcOffset < -720 || timezoneUtcOffset > 840) {
// timezones range from -12 (offset -720) to +14 (offset 840)
timezoneUtcOffset = timezoneUtcOffsetDefault;
}
const now = moment(o.now).utcOffset(timezoneUtcOffset);
// return a new object, we don't want to add "now" to user object
return {
dayStart,
timezoneUtcOffset,
now,
};
}
export function startOfWeek (options = {}) {
const o = sanitizeOptions(options);
return moment(o.now).startOf('week');
}
/*
This is designed for use with any date that has an important time portion
(e.g., when comparing the current date-time with the previous cron's date-time
for determining if cron should run now).
It changes the time portion of the date-time to be the Custom Day Start hour,
so that the date-time is now the user's correct start of day.
It SUBTRACTS a day if the date-time's original hour is before CDS
(e.g., if your CDS is 5am and it's currently 4am, it's still the previous day).
This is NOT suitable for manipulating any dates that are displayed to the user
as a date with no time portion, such as a Daily's Start Dates
(e.g., a Start Date of today shows only the date,
so it should be considered to be today even if the hidden time portion is before CDS).
*/
export function startOfDay (options = {}) {
const o = sanitizeOptions(options);
const dayStart = moment(o.now).startOf('day').add({ hours: o.dayStart });
if (o.now.hour() < o.dayStart) {
dayStart.subtract({ days: 1 });
}
return dayStart;
}
/*
Absolute diff from "yesterday" till now
*/
export function daysSince (yesterday, options = {}) {
const o = sanitizeOptions(options);
const startOfNow = startOfDay(defaults({ now: o.now }, o));
const startOfYesterday = startOfDay(defaults({ now: yesterday }, o));
return startOfNow.diff(startOfYesterday, 'days');
}
/*
Should the user do this task on this date,
given the task's repeat options and user.preferences.dayStart?
*/
export function shouldDo (day, dailyTask, options = {}) {
if (dailyTask.type !== 'daily' || dailyTask.startDate === null || dailyTask.everyX < 1 || dailyTask.everyX > 9999) {
return false;
}
const o = sanitizeOptions(options);
const startOfDayWithCDSTime = startOfDay(defaults({ now: day }, o));
// The time portion of the Start Date is never visible to
// or modifiable by the user so we must ignore it.
// Therefore, we must also ignore the time portion of the user's day start
// (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times.
// NB: The user's day start date has already been converted to the PREVIOUS
// day's date if the time portion was before CDS.
const startDate = moment(dailyTask.startDate).utcOffset(o.timezoneUtcOffset).startOf('day');
if (startDate > startOfDayWithCDSTime.startOf('day') && !options.nextDue) {
return false; // Daily starts in the future
}
const daysOfTheWeek = [];
if (dailyTask.repeat) {
for (const [repeatDay, active] of Object.entries(dailyTask.repeat)) {
if (!Number.isFinite(parseInt(DAY_MAPPING_STRING_TO_NUMBER[repeatDay], 10))) continue; // eslint-disable-line no-continue, max-len
if (active) daysOfTheWeek.push(parseInt(DAY_MAPPING_STRING_TO_NUMBER[repeatDay], 10));
}
}
if (dailyTask.frequency === 'daily') {
if (!dailyTask.everyX) return false; // error condition
const schedule = moment(startDate).recur()
.every(dailyTask.everyX).days();
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i += 1) {
const calcDate = moment(startDate).add(dailyTask.everyX * i, 'days');
if (calcDate > startOfDayWithCDSTime) filteredDates.push(calcDate);
}
return filteredDates;
}
return schedule.matches(startOfDayWithCDSTime);
} if (dailyTask.frequency === 'weekly') {
let schedule = moment(startDate).recur();
const differenceInWeeks = moment(startOfDayWithCDSTime).diff(moment(startDate), 'week');
const matchEveryX = differenceInWeeks % dailyTask.everyX === 0;
if (daysOfTheWeek.length === 0) return false;
schedule = schedule.every(daysOfTheWeek).daysOfWeek();
if (options.nextDue) {
const filteredDates = [];
for (let i = 0; filteredDates.length < 6; i += 1) {
for (let j = 0; j < daysOfTheWeek.length && filteredDates.length < 6; j += 1) {
const calcDate = moment(startDate).day(daysOfTheWeek[j]).add(dailyTask.everyX * i, 'weeks');
if (calcDate > startOfDayWithCDSTime) filteredDates.push(calcDate);
}
}
const sortedDates = filteredDates.sort((date1, date2) => {
if (date1.toDate() > date2.toDate()) return 1;
if (date2.toDate() > date1.toDate()) return -1;
return 0;
});
return sortedDates;
}
return schedule.matches(startOfDayWithCDSTime) && matchEveryX;
} if (dailyTask.frequency === 'monthly') {
let schedule = moment(startDate).recur();
// Use startOf to ensure that we are always comparing month
// to the next rather than a month from the day
const differenceInMonths = moment(startOfDayWithCDSTime).startOf('month')
.diff(moment(startDate).startOf('month'), 'month', true);
const matchEveryX = differenceInMonths % dailyTask.everyX === 0;
if (dailyTask.weeksOfMonth && dailyTask.weeksOfMonth.length > 0) {
if (daysOfTheWeek.length === 0) return false;
schedule = schedule.every(daysOfTheWeek).daysOfWeek()
.every(dailyTask.weeksOfMonth).weeksOfMonthByDay();
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i += 1) {
const recurDate = moment(startDate).add(dailyTask.everyX * i, 'months');
const calcDate = recurDate.clone();
calcDate.day(daysOfTheWeek[0]);
const startDateWeek = Math.ceil(moment(startDate).date() / 7);
let calcDateWeek = Math.ceil(calcDate.date() / 7);
// adjust week since weeks will rollover to other months
if (calcDate.month() < recurDate.month()) calcDate.add(1, 'weeks');
else if (calcDate.month() > recurDate.month()) calcDate.subtract(1, 'weeks');
else if (calcDateWeek > startDateWeek) calcDate.subtract(1, 'weeks');
else if (calcDateWeek < startDateWeek) calcDate.add(1, 'weeks');
calcDateWeek = Math.ceil(calcDate.date() / 7);
if (
calcDate >= startOfDayWithCDSTime
&& calcDateWeek === startDateWeek
&& calcDate.month() === recurDate.month()
) filteredDates.push(calcDate);
}
return filteredDates;
}
return schedule.matches(startOfDayWithCDSTime) && matchEveryX;
} if (dailyTask.daysOfMonth && dailyTask.daysOfMonth.length > 0) {
schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth();
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i += 1) {
const calcDate = moment(startDate).add(dailyTask.everyX * i, 'months');
if (calcDate >= startOfDayWithCDSTime) filteredDates.push(calcDate);
}
return filteredDates;
}
}
return schedule.matches(startOfDayWithCDSTime) && matchEveryX;
} if (dailyTask.frequency === 'yearly') {
let schedule = moment(startDate).recur();
schedule = schedule.every(dailyTask.everyX).years();
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i += 1) {
const calcDate = moment(startDate).add(dailyTask.everyX * i, 'years');
if (calcDate > startOfDayWithCDSTime) filteredDates.push(calcDate);
}
return filteredDates;
}
return schedule.matches(startOfDayWithCDSTime);
}
return false;
}
export function getPlanMonths (plan) {
// NB gift subscriptions don't have a planID
// (which doesn't matter because we don't need to reapply perks
// for them and by this point they should have expired anyway)
if (!plan.planId) return 1;
const planIdRegExp = /_([0-9]+)mo/; // e.g., matches 'google_6mo' / 'basic_12mo' and captures '6' / '12'
const match = plan.planId.match(planIdRegExp);
if (match !== null && match[0] !== null) {
// 3 for 3-month recurring subscription, etc
return match[1]; // eslint-disable-line prefer-destructuring
}
return 1;
}
/*
* This is a helper method to get all the needed informations of the plan
*
* currently used in cron and the "next hourglass in" feature
*/
export function getPlanContext (user, now) {
const { plan } = user.purchased;
defaults(plan.consecutive, {
count: 0, offset: 0, trinkets: 0, gemCapExtra: 0,
});
const nowMoment = moment(now);
const subscriptionEndDate = moment(plan.dateTerminated).isBefore()
? moment(plan.dateTerminated).startOf('month')
: nowMoment.startOf('month');
const dateUpdatedMoment = moment(plan.dateUpdated).startOf('month');
const elapsedMonths = moment(subscriptionEndDate).diff(dateUpdatedMoment, 'months');
let nextHourglassDate = moment(nowMoment).add(1, 'month');
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(nextHourglassDate)) {
nextHourglassDate = null;
}
return {
plan,
subscriptionEndDate,
dateUpdatedMoment,
elapsedMonths,
nextHourglassDate,
};
}