mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 21:57:22 +01:00
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 commiteb2fc40d24Author: 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 commitd3eea86bd7Author: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 24 15:00:09 2024 -0500 fix(subs): fix typeError commite3ae9a2d67Author: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 24 13:57:27 2024 -0500 fix(subs): also redirect to subs after gift sub commit690163a0deAuthor: Phillip Thelen <phillip@habitica.com> Date: Wed Oct 23 16:42:38 2024 +0200 fix test commit2ad7541fc0Author: Phillip Thelen <phillip@habitica.com> Date: Wed Oct 23 16:34:52 2024 +0200 fix test commit7e337a9e59Author: Phillip Thelen <phillip@habitica.com> Date: Wed Oct 23 11:54:15 2024 +0200 remove only commit7462b8a57fAuthor: Phillip Thelen <phillip@habitica.com> Date: Wed Oct 23 11:51:25 2024 +0200 fix bug with incorrectly giving HG bonus commitacd6183e95Author: Kalista Payne <sabrecat@gmail.com> Date: Mon Oct 21 17:22:26 2024 -0500 fix(subs): unhovery and un-12-monthy commit935e9fd6ecAuthor: Kalista Payne <sabrecat@gmail.com> Date: Fri Oct 18 14:50:17 2024 -0500 fix(subs): try again on gifts commit6e1fb7df38Author: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 17 18:19:20 2024 -0500 fix(lint): do negate object ig commit71d434b94eAuthor: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 17 18:15:11 2024 -0500 fix(lint): unnecessary ternary commitb90b0bb9c3Author: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 17 17:34:24 2024 -0500 fix(subs): gifts DON't renew commit19469304c5Author: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 17 17:13:29 2024 -0500 fix(subs): pass autoRenews through Stripe commit6819e7b7e5Author: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 17 16:03:25 2024 -0500 fix(subscriptions): minor visual updates commit74633b5e5eAuthor: Kalista Payne <sabrecat@gmail.com> Date: Wed Oct 16 17:27:09 2024 -0500 fix(subscriptions): more gift layout revisions commita90ccb89deAuthor: Kalista Payne <sabrecat@gmail.com> Date: Wed Oct 16 15:37:50 2024 -0500 fix(subscription): update layout when gifting commitc24b2db8dcAuthor: Phillip Thelen <phillip@habitica.com> Date: Mon Oct 14 16:11:46 2024 +0200 fix issue with promo hourglasses commit7a61c72b47Author: 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 commitf14cb09026Author: Phillip Thelen <phillip@habitica.com> Date: Mon Oct 14 10:38:01 2024 +0200 Admin panel display fixes commitf4cff698cfAuthor: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 3 17:58:59 2024 -0500 fix(stripe): correct redirect after success commitc468b58f3fAuthor: Kalista Payne <sabrecat@gmail.com> Date: Thu Oct 3 17:35:37 2024 -0500 fix(subs): correct border-radius and redirect commit78fb9e31d6Author: Kalista Payne <sabrecat@gmail.com> Date: Wed Oct 2 17:41:49 2024 -0500 fix(css): correct and refactor heights and selection states commite2babe8053Author: Kalista Payne <sabrecat@gmail.com> Date: Mon Sep 30 16:45:29 2024 -0500 feat(subscription): max Gems progress readout commit61af8302a3Author: Phillip Thelen <phillip@habitica.com> Date: Fri Sep 27 15:11:22 2024 +0200 fix test commitef8ff0ea9eAuthor: Phillip Thelen <phillip@habitica.com> Date: Fri Sep 27 14:14:44 2024 +0200 show date for hourglass bonus if it was received commit4bafafdc8dAuthor: Phillip Thelen <phillip@habitica.com> Date: Fri Sep 27 14:12:52 2024 +0200 add new field for cumulative subscription count commit30096247b7Author: Phillip Thelen <phillip@habitica.com> Date: Fri Sep 27 13:39:49 2024 +0200 fix missing transaction type commit70872651b0Author: Phillip Thelen <phillip@habitica.com> Date: Fri Sep 27 13:31:40 2024 +0200 fix admin panel strings commitf3398db65fAuthor: Kalista Payne <sabrecat@gmail.com> Date: Thu Sep 26 23:11:16 2024 -0500 WIP(subs): extant Stripe state commitc6b2020109Author: Phillip Thelen <phillip@habitica.com> Date: Thu Sep 26 11:41:55 2024 +0200 fix admin panel display commitd9afc96d2dAuthor: Phillip Thelen <phillip@habitica.com> Date: Thu Sep 26 11:40:16 2024 +0200 Fix hourglass logic for upgrades commit6e2c8eeb64Author: Phillip Thelen <phillip@habitica.com> Date: Wed Sep 25 17:48:54 2024 +0200 fix hourglass count commitcd752fbdceAuthor: Kalista Payne <sabrecat@gmail.com> Date: Fri Sep 20 12:24:21 2024 -0500 WIP(frontend): draft of main subs page view commit0102b29d59Author: Kalista Payne <sabe@habitica.com> Date: Wed Sep 18 15:29:08 2024 -0500 fix(admin): correct logic and style for shrimple subs commit5469a5c5c3Author: Kalista Payne <sabe@habitica.com> Date: Wed Sep 18 15:07:36 2024 -0500 fix(test): short circuit this. commit526193ee6cAuthor: Phillip Thelen <phillip@habitica.com> Date: Wed Sep 18 14:42:06 2024 +0200 fix gem limit commit19cf1636aaAuthor: Phillip Thelen <phillip@habitica.com> Date: Tue Aug 13 17:00:40 2024 +0200 return nextHourglassDate again commiteea36e3ed5Author: Phillip Thelen <phillip@habitica.com> Date: Tue Aug 13 13:11:22 2024 +0200 subscription test improvements commitca78e74330Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 12 15:46:15 2024 +0200 add more subscription tests commitf4c4f93a08Author: Phillip Thelen <phillip@habitica.com> Date: Fri Aug 9 13:35:22 2024 +0200 finish basic implementation of new logic commite036742048Author: Phillip Thelen <phillip@habitica.com> Date: Fri Aug 9 11:37:44 2024 +0200 cleanup commit6431865688Author: Phillip Thelen <phillip@habitica.com> Date: Wed Aug 7 05:41:18 2024 -0400 update cron tests commit930d875ae9Author: Phillip Thelen <phillip@habitica.com> Date: Thu Aug 8 10:36:50 2024 +0200 begin refactoring commit96623608d0Author: Phillip Thelen <phillip@habitica.com> Date: Tue Aug 6 16:28:16 2024 +0200 begin removing obsolete tests
302 lines
11 KiB
JavaScript
302 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,
|
|
};
|
|
}
|