fix(billing): compute plan expiry with Jalali months
Server/plugin/support charges used Gregorian Date#setMonth, so 3 months from 1405/06/01 became 1405/09/02. Now add N Jalali months minus one day (1405/08/30).
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
/** Shared Iran timezone helpers for SuperAdmin API. */
|
||||
|
||||
import { toGregorian, jalaaliMonthLength } from 'jalaali-js';
|
||||
|
||||
export const IRAN_TZ = 'Asia/Tehran';
|
||||
|
||||
/** Gregorian YYYY-MM-DD for the calendar day in Asia/Tehran. */
|
||||
@@ -38,3 +40,71 @@ export function iranDaysUntil(expiresAt: Date, now: Date = new Date()): number {
|
||||
const end = Date.parse(`${iranYmd(expiresAt)}T12:00:00+03:30`);
|
||||
return Math.round((end - start) / 86_400_000);
|
||||
}
|
||||
|
||||
export type IranJalaliYmd = { year: number; month: number; day: number };
|
||||
|
||||
/** Jalali Y/M/D for a Date in Asia/Tehran. */
|
||||
export function iranJalaliParts(date: Date): IranJalaliYmd {
|
||||
const parts = new Intl.DateTimeFormat('en-u-ca-persian', {
|
||||
timeZone: IRAN_TZ,
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
}).formatToParts(date);
|
||||
|
||||
const num = (type: Intl.DateTimeFormatPartTypes) => {
|
||||
const raw = parts.find((p) => p.type === type)?.value ?? '';
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) {
|
||||
throw new Error(`Cannot read Jalali ${type} from ${date.toISOString()}`);
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
return { year: num('year'), month: num('month'), day: num('day') };
|
||||
}
|
||||
|
||||
/** Noon Iran (+03:30) for a Jalali calendar day. */
|
||||
export function iranDateFromJalali(year: number, month: number, day: number): Date {
|
||||
const g = toGregorian(year, month, day);
|
||||
const iso = `${pad2(g.gy)}-${pad2(g.gm)}-${pad2(g.gd)}T12:00:00+03:30`;
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
throw new Error(`Invalid Jalali date ${year}/${month}/${day}`);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/** Add calendar days on the Iran civil day (noon Tehran). */
|
||||
export function addIranDays(from: Date, days: number): Date {
|
||||
const ymd = iranYmd(from);
|
||||
const base = Date.parse(`${ymd}T12:00:00+03:30`);
|
||||
return new Date(base + days * 86_400_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Jalali months (same day-of-month, clamped to month length).
|
||||
* Example: 1405/06/01 + 3 → 1405/09/01.
|
||||
*/
|
||||
export function addIranMonths(from: Date, months: number): Date {
|
||||
const { year, month, day } = iranJalaliParts(from);
|
||||
const total = year * 12 + (month - 1) + months;
|
||||
const nextYear = Math.floor(total / 12);
|
||||
const nextMonth = (total % 12) + 1;
|
||||
const maxDay = jalaaliMonthLength(nextYear, nextMonth);
|
||||
const nextDay = Math.min(day, maxDay);
|
||||
return iranDateFromJalali(nextYear, nextMonth, nextDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan expiry for server / plugin / support charges.
|
||||
* Start day + N Jalali months, then minus 1 day (last inclusive day of the period).
|
||||
* Example: 1405/06/01 + 3 months → expires 1405/08/30.
|
||||
*/
|
||||
export function addIranPlanExpiry(from: Date, months: number): Date {
|
||||
return addIranDays(addIranMonths(from, months), -1);
|
||||
}
|
||||
|
||||
function pad2(n: number) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user