mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-14 19:47:03 +00: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
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
301 lines
11 KiB
JavaScript
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,
|
|
};
|
|
}
|