| import { describe, expect, test } from "bun:test"; |
| import { |
| advanceToFutureDate, |
| calculateFirstScheduledDate, |
| calculateNextScheduledDate, |
| calculateUpcomingDates, |
| type RecurringInvoiceParams, |
| shouldMarkCompleted, |
| } from "../utils/invoice-recurring"; |
|
|
| describe("calculateNextScheduledDate", () => { |
| describe("weekly frequency", () => { |
| test("returns next occurrence of target weekday", () => { |
| |
| const currentDate = new Date("2025-01-07T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "weekly", |
| frequencyDay: 5, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getDay()).toBe(5); |
| expect(result > currentDate).toBe(true); |
| }); |
|
|
| test("skips to next week if target day is today or passed", () => { |
| |
| const currentDate = new Date("2025-01-10T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "weekly", |
| frequencyDay: 5, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getDay()).toBe(5); |
| expect(result > currentDate).toBe(true); |
| }); |
|
|
| test("handles Sunday (day 0) correctly", () => { |
| |
| const currentDate = new Date("2025-01-08T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "weekly", |
| frequencyDay: 0, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| expect(result.getDay()).toBe(0); |
| expect(result > currentDate).toBe(true); |
| }); |
| }); |
|
|
| describe("monthly_date frequency", () => { |
| test("returns same date next month", () => { |
| |
| const currentDate = new Date("2025-01-15T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "monthly_date", |
| frequencyDay: 15, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDate()).toBe(15); |
| }); |
|
|
| test("handles 31st in month with 30 days (April)", () => { |
| |
| const currentDate = new Date("2025-03-31T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "monthly_date", |
| frequencyDay: 31, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(3); |
| expect(result.getDate()).toBe(30); |
| }); |
|
|
| test("handles 31st in February (non-leap year)", () => { |
| |
| const currentDate = new Date("2025-01-31T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "monthly_date", |
| frequencyDay: 31, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDate()).toBe(28); |
| }); |
|
|
| test("handles 29th in February (leap year)", () => { |
| |
| const currentDate = new Date("2024-01-29T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "monthly_date", |
| frequencyDay: 29, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDate()).toBe(29); |
| }); |
| }); |
|
|
| describe("monthly_weekday frequency", () => { |
| test("returns nth occurrence of weekday", () => { |
| |
| const currentDate = new Date("2025-01-01T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "monthly_weekday", |
| frequencyDay: 5, |
| frequencyWeek: 1, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDay()).toBe(5); |
| expect(result.getDate()).toBe(7); |
| }); |
|
|
| test("handles 2nd Tuesday correctly", () => { |
| |
| const currentDate = new Date("2025-01-01T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "monthly_weekday", |
| frequencyDay: 2, |
| frequencyWeek: 2, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDay()).toBe(2); |
| expect(result.getDate()).toBe(11); |
| }); |
| }); |
|
|
| describe("custom frequency", () => { |
| test("returns current date plus interval days", () => { |
| |
| const currentDate = new Date("2025-01-01T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "custom", |
| frequencyDay: null, |
| frequencyWeek: null, |
| frequencyInterval: 14, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(0); |
| expect(result.getDate()).toBe(15); |
| }); |
|
|
| test("defaults to 1 day if interval not specified", () => { |
| const currentDate = new Date("2025-01-01T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "custom", |
| frequencyDay: null, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| expect(result.getDate()).toBe(2); |
| }); |
| }); |
|
|
| describe("quarterly frequency", () => { |
| test("returns same date 3 months later", () => { |
| |
| const currentDate = new Date("2025-01-15T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "quarterly", |
| frequencyDay: 15, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(3); |
| expect(result.getDate()).toBe(15); |
| }); |
|
|
| test("handles 31st in month with 30 days", () => { |
| |
| const currentDate = new Date("2025-01-31T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "quarterly", |
| frequencyDay: 31, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(3); |
| expect(result.getDate()).toBe(30); |
| }); |
|
|
| test("handles February edge case", () => { |
| |
| const currentDate = new Date("2024-11-30T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "quarterly", |
| frequencyDay: 30, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDate()).toBe(28); |
| }); |
| }); |
|
|
| describe("semi_annual frequency", () => { |
| test("returns same date 6 months later", () => { |
| |
| const currentDate = new Date("2025-01-15T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "semi_annual", |
| frequencyDay: 15, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(6); |
| expect(result.getDate()).toBe(15); |
| }); |
|
|
| test("handles month length differences", () => { |
| |
| const currentDate = new Date("2025-08-31T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "semi_annual", |
| frequencyDay: 31, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDate()).toBe(28); |
| expect(result.getFullYear()).toBe(2026); |
| }); |
| }); |
|
|
| describe("annual frequency", () => { |
| test("returns same date 12 months later", () => { |
| |
| const currentDate = new Date("2025-01-15T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "annual", |
| frequencyDay: 15, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getFullYear()).toBe(2026); |
| expect(result.getMonth()).toBe(0); |
| expect(result.getDate()).toBe(15); |
| }); |
|
|
| test("handles leap year to non-leap year (Feb 29)", () => { |
| |
| const currentDate = new Date("2024-02-29T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "annual", |
| frequencyDay: 29, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getFullYear()).toBe(2025); |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDate()).toBe(28); |
| }); |
|
|
| test("handles non-leap year to leap year (Feb 28)", () => { |
| |
| const currentDate = new Date("2023-02-28T12:00:00.000Z"); |
| const params: RecurringInvoiceParams = { |
| frequency: "annual", |
| frequencyDay: 28, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const result = calculateNextScheduledDate(params, currentDate); |
|
|
| |
| expect(result.getFullYear()).toBe(2024); |
| expect(result.getMonth()).toBe(1); |
| expect(result.getDate()).toBe(28); |
| }); |
| }); |
| }); |
|
|
| describe("shouldMarkCompleted", () => { |
| describe("endType: never", () => { |
| test("always returns false", () => { |
| expect(shouldMarkCompleted("never", null, null, 100, new Date())).toBe( |
| false, |
| ); |
|
|
| expect( |
| shouldMarkCompleted( |
| "never", |
| new Date("2020-01-01"), |
| null, |
| 100, |
| new Date(), |
| ), |
| ).toBe(false); |
| }); |
| }); |
|
|
| describe("endType: on_date", () => { |
| test("returns true when next scheduled date exceeds end date", () => { |
| const endDate = new Date("2025-06-01T00:00:00.000Z"); |
| const nextScheduledAt = new Date("2025-07-01T00:00:00.000Z"); |
|
|
| expect( |
| shouldMarkCompleted("on_date", endDate, null, 5, nextScheduledAt), |
| ).toBe(true); |
| }); |
|
|
| test("returns false when next scheduled date is before end date", () => { |
| const endDate = new Date("2025-06-01T00:00:00.000Z"); |
| const nextScheduledAt = new Date("2025-05-15T00:00:00.000Z"); |
|
|
| expect( |
| shouldMarkCompleted("on_date", endDate, null, 5, nextScheduledAt), |
| ).toBe(false); |
| }); |
|
|
| test("returns false when endDate is null", () => { |
| const nextScheduledAt = new Date("2025-07-01T00:00:00.000Z"); |
|
|
| expect( |
| shouldMarkCompleted("on_date", null, null, 5, nextScheduledAt), |
| ).toBe(false); |
| }); |
|
|
| test("returns false when nextScheduledAt is null", () => { |
| const endDate = new Date("2025-06-01T00:00:00.000Z"); |
|
|
| expect(shouldMarkCompleted("on_date", endDate, null, 5, null)).toBe( |
| false, |
| ); |
| }); |
| }); |
|
|
| describe("endType: after_count", () => { |
| test("returns true when invoices generated equals end count", () => { |
| expect(shouldMarkCompleted("after_count", null, 12, 12, new Date())).toBe( |
| true, |
| ); |
| }); |
|
|
| test("returns true when invoices generated exceeds end count", () => { |
| expect(shouldMarkCompleted("after_count", null, 12, 15, new Date())).toBe( |
| true, |
| ); |
| }); |
|
|
| test("returns false when invoices generated is less than end count", () => { |
| expect(shouldMarkCompleted("after_count", null, 12, 5, new Date())).toBe( |
| false, |
| ); |
| }); |
|
|
| test("returns false when endCount is null", () => { |
| expect( |
| shouldMarkCompleted("after_count", null, null, 100, new Date()), |
| ).toBe(false); |
| }); |
| }); |
| }); |
|
|
| describe("calculateUpcomingDates", () => { |
| const baseParams: RecurringInvoiceParams = { |
| frequency: "weekly", |
| frequencyDay: 5, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| test("returns correct number of preview dates", () => { |
| const startDate = new Date("2025-01-03T12:00:00.000Z"); |
| const result = calculateUpcomingDates( |
| baseParams, |
| startDate, |
| 100, |
| "USD", |
| "never", |
| null, |
| null, |
| 0, |
| 5, |
| ); |
|
|
| expect(result.invoices).toHaveLength(5); |
| }); |
|
|
| test("respects end date limit", () => { |
| const startDate = new Date("2025-01-03T12:00:00.000Z"); |
| const endDate = new Date("2025-01-17T12:00:00.000Z"); |
|
|
| const result = calculateUpcomingDates( |
| baseParams, |
| startDate, |
| 100, |
| "USD", |
| "on_date", |
| endDate, |
| null, |
| 0, |
| 10, |
| ); |
|
|
| |
| for (const invoice of result.invoices) { |
| expect(new Date(invoice.date) <= endDate).toBe(true); |
| } |
| }); |
|
|
| test("respects end count limit", () => { |
| const startDate = new Date("2025-01-03T12:00:00.000Z"); |
| const endCount = 3; |
|
|
| const result = calculateUpcomingDates( |
| baseParams, |
| startDate, |
| 100, |
| "USD", |
| "after_count", |
| null, |
| endCount, |
| 0, |
| 10, |
| ); |
|
|
| expect(result.invoices).toHaveLength(endCount); |
| }); |
|
|
| test("accounts for already generated invoices in count limit", () => { |
| const startDate = new Date("2025-01-03T12:00:00.000Z"); |
| const endCount = 5; |
| const alreadyGenerated = 2; |
|
|
| const result = calculateUpcomingDates( |
| baseParams, |
| startDate, |
| 100, |
| "USD", |
| "after_count", |
| null, |
| endCount, |
| alreadyGenerated, |
| 10, |
| ); |
|
|
| |
| expect(result.invoices).toHaveLength(3); |
| }); |
|
|
| test("calculates correct summary for after_count", () => { |
| const startDate = new Date("2025-01-03T12:00:00.000Z"); |
| const amount = 100; |
| const endCount = 12; |
|
|
| const result = calculateUpcomingDates( |
| baseParams, |
| startDate, |
| amount, |
| "USD", |
| "after_count", |
| null, |
| endCount, |
| 0, |
| 5, |
| ); |
|
|
| expect(result.summary.hasEndDate).toBe(true); |
| expect(result.summary.totalCount).toBe(12); |
| expect(result.summary.totalAmount).toBe(1200); |
| expect(result.summary.currency).toBe("USD"); |
| }); |
|
|
| test("calculates correct summary for never ending", () => { |
| const startDate = new Date("2025-01-03T12:00:00.000Z"); |
|
|
| const result = calculateUpcomingDates( |
| baseParams, |
| startDate, |
| 100, |
| "USD", |
| "never", |
| null, |
| null, |
| 0, |
| 5, |
| ); |
|
|
| expect(result.summary.hasEndDate).toBe(false); |
| expect(result.summary.totalCount).toBe(null); |
| expect(result.summary.totalAmount).toBe(null); |
| }); |
|
|
| test("includes correct date in each invoice", () => { |
| const startDate = new Date("2025-01-03T12:00:00.000Z"); |
| const result = calculateUpcomingDates( |
| baseParams, |
| startDate, |
| 100, |
| "USD", |
| "never", |
| null, |
| null, |
| 0, |
| 3, |
| ); |
|
|
| |
| for (const invoice of result.invoices) { |
| expect(invoice.date).toBeDefined(); |
| expect(new Date(invoice.date).toISOString()).toBe(invoice.date); |
| expect(invoice.amount).toBe(100); |
| } |
| }); |
| }); |
|
|
| describe("calculateFirstScheduledDate", () => { |
| const baseParams: RecurringInvoiceParams = { |
| frequency: "monthly_last_day", |
| frequencyDay: null, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| describe("issue date in the future", () => { |
| test("returns the issue date when it is in the future", () => { |
| |
| |
| const issueDate = new Date("2025-01-31T12:00:00.000Z"); |
| const now = new Date("2025-01-15T12:00:00.000Z"); |
|
|
| const result = calculateFirstScheduledDate(baseParams, issueDate, now); |
|
|
| |
| expect(result.toISOString()).toBe(issueDate.toISOString()); |
| }); |
|
|
| test("returns future issue date for different frequencies", () => { |
| const monthlyDateParams: RecurringInvoiceParams = { |
| frequency: "monthly_date", |
| frequencyDay: 15, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| |
| |
| const issueDate = new Date("2025-02-15T12:00:00.000Z"); |
| const now = new Date("2025-01-10T12:00:00.000Z"); |
|
|
| const result = calculateFirstScheduledDate( |
| monthlyDateParams, |
| issueDate, |
| now, |
| ); |
|
|
| expect(result.toISOString()).toBe(issueDate.toISOString()); |
| }); |
| }); |
|
|
| describe("issue date is today", () => { |
| test("returns now when issue date is today", () => { |
| |
| const issueDate = new Date("2025-01-15T09:00:00.000Z"); |
| const now = new Date("2025-01-15T14:30:00.000Z"); |
|
|
| const result = calculateFirstScheduledDate(baseParams, issueDate, now); |
|
|
| |
| expect(result.toISOString()).toBe(now.toISOString()); |
| }); |
|
|
| test("returns now when issue date is same day at start", () => { |
| |
| const issueDate = new Date("2025-01-15T00:00:00.000Z"); |
| const now = new Date("2025-01-15T18:00:00.000Z"); |
|
|
| const result = calculateFirstScheduledDate(baseParams, issueDate, now); |
|
|
| expect(result.toISOString()).toBe(now.toISOString()); |
| }); |
| }); |
|
|
| describe("issue date in the past", () => { |
| test("returns now when issue date is in the past", () => { |
| |
| |
| const issueDate = new Date("2025-01-10T12:00:00.000Z"); |
| const now = new Date("2025-01-15T12:00:00.000Z"); |
|
|
| const result = calculateFirstScheduledDate(baseParams, issueDate, now); |
|
|
| |
| expect(result.toISOString()).toBe(now.toISOString()); |
| }); |
|
|
| test("returns now when issue date is far in the past", () => { |
| |
| |
| const issueDate = new Date("2024-12-01T12:00:00.000Z"); |
| const now = new Date("2025-01-15T12:00:00.000Z"); |
|
|
| const result = calculateFirstScheduledDate(baseParams, issueDate, now); |
|
|
| expect(result.toISOString()).toBe(now.toISOString()); |
| }); |
| }); |
|
|
| describe("edge cases", () => { |
| test("handles issue date just one day in the future", () => { |
| |
| const issueDate = new Date("2025-01-16T12:00:00.000Z"); |
| const now = new Date("2025-01-15T12:00:00.000Z"); |
|
|
| const result = calculateFirstScheduledDate(baseParams, issueDate, now); |
|
|
| |
| expect(result.toISOString()).toBe(issueDate.toISOString()); |
| }); |
|
|
| test("handles issue date at midnight boundary", () => { |
| |
| |
| const issueDate = new Date("2025-01-16T00:00:00.000Z"); |
| const now = new Date("2025-01-15T23:59:59.000Z"); |
|
|
| const result = calculateFirstScheduledDate(baseParams, issueDate, now); |
|
|
| |
| expect(result.toISOString()).toBe(issueDate.toISOString()); |
| }); |
|
|
| test("uses default now when not provided", () => { |
| |
| const futureDate = new Date(); |
| futureDate.setFullYear(futureDate.getFullYear() + 1); |
|
|
| const result = calculateFirstScheduledDate(baseParams, futureDate); |
|
|
| |
| expect(result.toISOString()).toBe(futureDate.toISOString()); |
| }); |
| }); |
| }); |
|
|
| describe("advanceToFutureDate", () => { |
| describe("biweekly frequency - late scheduler scenario", () => { |
| const biweeklyParams: RecurringInvoiceParams = { |
| frequency: "biweekly", |
| frequencyDay: null, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| test("returns original date when already in future", () => { |
| |
| const scheduledDate = new Date("2025-01-15T12:00:00.000Z"); |
| const now = new Date("2025-01-10T12:00:00.000Z"); |
|
|
| const result = advanceToFutureDate(biweeklyParams, scheduledDate, now); |
|
|
| expect(result.date.toISOString()).toBe(scheduledDate.toISOString()); |
| expect(result.intervalsSkipped).toBe(0); |
| expect(result.hitSafetyLimit).toBe(false); |
| }); |
|
|
| test("advances one interval when scheduler runs late", () => { |
| |
| |
| |
| const scheduledDate = new Date("2025-01-15T12:00:00.000Z"); |
| const now = new Date("2025-01-21T12:00:00.000Z"); |
|
|
| const result = advanceToFutureDate(biweeklyParams, scheduledDate, now); |
|
|
| |
| expect(result.date.getDate()).toBe(29); |
| expect(result.date.getMonth()).toBe(0); |
| expect(result.intervalsSkipped).toBe(1); |
| expect(result.hitSafetyLimit).toBe(false); |
| }); |
|
|
| test("advances multiple intervals when scheduler runs very late", () => { |
| |
| |
| const scheduledDate = new Date("2025-01-15T12:00:00.000Z"); |
| const now = new Date("2025-02-15T12:00:00.000Z"); |
|
|
| const result = advanceToFutureDate(biweeklyParams, scheduledDate, now); |
|
|
| |
| expect(result.date > now).toBe(true); |
| expect(result.intervalsSkipped).toBe(3); |
| expect(result.hitSafetyLimit).toBe(false); |
| }); |
| }); |
|
|
| describe("custom frequency - late scheduler scenario", () => { |
| const customParams: RecurringInvoiceParams = { |
| frequency: "custom", |
| frequencyDay: null, |
| frequencyWeek: null, |
| frequencyInterval: 7, |
| timezone: "UTC", |
| }; |
|
|
| test("advances through missed intervals", () => { |
| |
| const scheduledDate = new Date("2025-01-08T12:00:00.000Z"); |
| const now = new Date("2025-01-25T12:00:00.000Z"); |
|
|
| const result = advanceToFutureDate(customParams, scheduledDate, now); |
|
|
| |
| expect(result.date.getDate()).toBe(29); |
| expect(result.intervalsSkipped).toBe(3); |
| expect(result.hitSafetyLimit).toBe(false); |
| }); |
| }); |
|
|
| describe("safety limit", () => { |
| test("falls back to now when hitting safety limit", () => { |
| const params: RecurringInvoiceParams = { |
| frequency: "custom", |
| frequencyDay: null, |
| frequencyWeek: null, |
| frequencyInterval: 1, |
| timezone: "UTC", |
| }; |
|
|
| |
| const scheduledDate = new Date("2025-01-01T12:00:00.000Z"); |
| const now = new Date("2025-01-15T12:00:00.000Z"); |
|
|
| const result = advanceToFutureDate(params, scheduledDate, now, 5); |
|
|
| |
| expect(result.hitSafetyLimit).toBe(true); |
| expect(result.intervalsSkipped).toBe(0); |
| expect(result.date > now).toBe(true); |
| }); |
| }); |
|
|
| describe("edge cases", () => { |
| test("handles date exactly equal to now", () => { |
| const params: RecurringInvoiceParams = { |
| frequency: "biweekly", |
| frequencyDay: null, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| const scheduledDate = new Date("2025-01-15T12:00:00.000Z"); |
| const now = new Date("2025-01-15T12:00:00.000Z"); |
|
|
| const result = advanceToFutureDate(params, scheduledDate, now); |
|
|
| |
| expect(result.date > now).toBe(true); |
| expect(result.intervalsSkipped).toBe(1); |
| }); |
|
|
| test("handles monthly frequencies correctly", () => { |
| const monthlyParams: RecurringInvoiceParams = { |
| frequency: "monthly_date", |
| frequencyDay: 15, |
| frequencyWeek: null, |
| frequencyInterval: null, |
| timezone: "UTC", |
| }; |
|
|
| |
| const scheduledDate = new Date("2025-02-15T12:00:00.000Z"); |
| const now = new Date("2025-03-20T12:00:00.000Z"); |
|
|
| const result = advanceToFutureDate(monthlyParams, scheduledDate, now); |
|
|
| |
| |
| |
| expect(result.date.getMonth()).toBe(3); |
| expect(result.date.getDate()).toBe(15); |
| expect(result.intervalsSkipped).toBe(2); |
| }); |
| }); |
| }); |
|
|