/** 财务账单日 / 账期一律按中国北京时间(Asia/Shanghai,无夏令时) */ export const SHANGHAI_TIME_ZONE = 'Asia/Shanghai'; function pad2(n: number): string { return String(n).padStart(2, '0'); } export function shanghaiYmd(d: Date): string { return d.toLocaleDateString('sv-SE', { timeZone: SHANGHAI_TIME_ZONE }); } export function parseShanghaiYmd(ymd: string): Date { return new Date(`${ymd}T00:00:00+08:00`); } export function startOfShanghaiDay(d: Date = new Date()): Date { return parseShanghaiYmd(shanghaiYmd(d)); } export function addShanghaiDays(d: Date, days: number): Date { const [y, m, day] = shanghaiYmd(startOfShanghaiDay(d)).split('-').map(Number); const utc = new Date(Date.UTC(y, m - 1, day + days)); const ymd = `${utc.getUTCFullYear()}-${pad2(utc.getUTCMonth() + 1)}-${pad2(utc.getUTCDate())}`; return new Date(`${ymd}T00:00:00+08:00`); } /** 门店 T+1:核销窗口 [昨日 00:00, 今日 00:00);出账日 = 今天 */ export function shanghaiT1DayWindow(anchor = new Date()) { const billDate = startOfShanghaiDay(anchor); const start = addShanghaiDays(billDate, -1); return { start, end: billDate, billDate }; } /** * 酒厂日滞后窗口(旧:每天出账只收 lagDays 前那一天)。 * 现网酒厂已改为「每 periodDays 天出一期」,见 shanghaiWineryPeriodWindow。 */ export function shanghaiLaggedIssueWindow(anchor = new Date(), lagDays: number) { const billDate = startOfShanghaiDay(anchor); const start = addShanghaiDays(billDate, -lagDays); const end = addShanghaiDays(start, 1); return { start, end, billDate }; } /** 两个北京自然日之间相差天数(b − a;可为负) */ export function shanghaiCalendarDaysBetween(a: Date, b: Date): number { const sa = startOfShanghaiDay(a).getTime(); const sb = startOfShanghaiDay(b).getTime(); return Math.round((sb - sa) / (24 * 60 * 60 * 1000)); } /** 是否酒厂出账日:相对 epoch,每 periodDays 天一次(首张 = epoch + periodDays) */ export function isWineryIssueDay(anchor: Date, periodDays: number, epochYmd: string): boolean { const billDate = startOfShanghaiDay(anchor); const epoch = parseShanghaiYmd(epochYmd); const diff = shanghaiCalendarDaysBetween(epoch, billDate); return diff >= periodDays && diff % periodDays === 0; } /** * 酒厂 T+N 期账单窗口:出账日 = anchor 当天;纳入 [出账日 − periodDays, 出账日) 内完成的订单。 * 调用方应先用 isWineryIssueDay 判断是否出账日。 */ export function shanghaiWineryPeriodWindow(anchor = new Date(), periodDays: number) { const billDate = startOfShanghaiDay(anchor); const start = addShanghaiDays(billDate, -periodDays); return { start, end: billDate, billDate }; } /** 订单完成日 → 所属酒厂出账日(落入下一期出账日) */ export function wineryIssueDateFromCompletedAt( completedAt: Date, periodDays: number, epochYmd: string, ) { const c = startOfShanghaiDay(completedAt); const epoch = parseShanghaiYmd(epochYmd); const daysFromEpoch = shanghaiCalendarDaysBetween(epoch, c); if (daysFromEpoch < 0) { return addShanghaiDays(epoch, periodDays); } const k = Math.floor(daysFromEpoch / periodDays) + 1; return addShanghaiDays(epoch, k * periodDays); } /** 枚举 [from, to] 内(含)的全部酒厂出账日 */ export function listWineryIssueDays(from: Date, to: Date, periodDays: number, epochYmd: string): Date[] { const start = startOfShanghaiDay(from); const end = startOfShanghaiDay(to); const out: Date[] = []; for (let d = start; d.getTime() <= end.getTime(); d = addShanghaiDays(d, 1)) { if (isWineryIssueDay(d, periodDays, epochYmd)) out.push(d); } return out; } export function shanghaiMonthRange(year: number, month: number): { start: Date; endExclusive: Date } { const start = new Date(`${year}-${pad2(month)}-01T00:00:00+08:00`); const endMonth = month === 12 ? 1 : month + 1; const endYear = month === 12 ? year + 1 : year; const endExclusive = new Date(`${endYear}-${pad2(endMonth)}-01T00:00:00+08:00`); return { start, endExclusive }; } export function shanghaiMonthLastInstant(year: number, month: number): Date { return new Date(shanghaiMonthRange(year, month).endExclusive.getTime() - 1); } export function previousShanghaiMonth(anchor = new Date()): { year: number; month: number } { const [y, m] = shanghaiYmd(anchor).split('-').map(Number); if (m === 1) return { year: y - 1, month: 12 }; return { year: y, month: m - 1 }; } export function shanghaiYearMonth(anchor = new Date()): { year: number; month: number } { const [y, m] = shanghaiYmd(anchor).split('-').map(Number); return { year: y, month: m }; } /** 账期展示:以 periodStart 所在北京月的首末日,避免 UTC 23:59:59 滚到下月 */ export function shanghaiPeriodYmds(periodStart: Date): { periodStart: string; periodEnd: string } { const [y, m] = shanghaiYmd(periodStart).split('-').map(Number); const { start, endExclusive } = shanghaiMonthRange(y, m); return { periodStart: shanghaiYmd(start), periodEnd: shanghaiYmd(new Date(endExclusive.getTime() - 1)), }; }