Skip to content
355 changes: 355 additions & 0 deletions lib/utils/payday.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
import { getPayday, countDaysBeforePayday, countDaysAfterPayday, countDaysAfterBlock } from './payday';
import { WorkspaceDBScheme } from '@hawk.so/types';
import { ObjectId } from 'mongodb';

/**
* Mock the Date constructor to allow controlling "now"
*/
let mockedNow: number | null = null;

const setMockedNow = (date: Date): void => {
mockedNow = date.getTime();
};

const resetMockedNow = (): void => {
mockedNow = null;
};

// Override Date constructor
const RealDate = Date;
global.Date = class extends RealDate {
/**
* Constructor for mocked Date class
* @param args - arguments passed to Date constructor
*/
constructor(...args: unknown[]) {
if (args.length === 0 && mockedNow !== null) {
super(mockedNow);
} else {
super(...(args as []));
}
}

public static now(): number {
return mockedNow !== null ? mockedNow : RealDate.now();
}
} as DateConstructor;

describe('Payday utility functions', () => {
afterEach(() => {
resetMockedNow();
});

describe('getPayday', () => {
it('should return paidUntil date when provided', () => {
const lastChargeDate = new Date('2025-11-01');
const paidUntil = new Date('2025-12-15');

const result = getPayday(lastChargeDate, paidUntil);

expect(result).toEqual(paidUntil);
});

it('should calculate payday as one month after lastChargeDate when paidUntil is not provided', () => {
const lastChargeDate = new Date('2025-11-01');

const result = getPayday(lastChargeDate);

expect(result.getFullYear()).toBe(2025);
expect(result.getMonth()).toBe(11); // December (0-indexed)
expect(result.getDate()).toBe(1);
});

it('should handle year transition correctly', () => {
const lastChargeDate = new Date('2025-12-15');

const result = getPayday(lastChargeDate);

expect(result.getFullYear()).toBe(2026);
expect(result.getMonth()).toBe(0); // January (0-indexed)
expect(result.getDate()).toBe(15);
});

it('should add one day when isDebug is true', () => {
const lastChargeDate = new Date('2025-12-01');

const result = getPayday(lastChargeDate, null, true);

expect(result.getFullYear()).toBe(2025);
expect(result.getMonth()).toBe(11); // December (0-indexed)
expect(result.getDate()).toBe(2);
});

it('should prioritize paidUntil over debug mode', () => {
const lastChargeDate = new Date('2025-11-01');
const paidUntil = new Date('2025-12-15');

const result = getPayday(lastChargeDate, paidUntil, true);

expect(result).toEqual(paidUntil);
});

it('should handle end of month dates correctly', () => {
const lastChargeDate = new Date('2025-01-31');

const result = getPayday(lastChargeDate);

// JavaScript will adjust to the last day of February
expect(result.getFullYear()).toBe(2025);
expect(result.getMonth()).toBe(2); // March (0-indexed)
expect(result.getDate()).toBe(3); // Adjusted from Feb 31 to Mar 3
});
});

describe('countDaysBeforePayday', () => {
it('should return positive days when payday is in the future', () => {
const now = new Date('2025-12-01');
const lastChargeDate = new Date('2025-11-20');

setMockedNow(now);

const result = countDaysBeforePayday(lastChargeDate);

expect(result).toBe(19); // Dec 20 - Dec 1 = 19 days
});

it('should return 0 when payday is today', () => {
// Payday is calculated as one month after lastChargeDate, so Dec 20 12pm
const now = new Date('2025-12-20T12:00:00.000Z');
const lastChargeDate = new Date('2025-11-20T12:00:00.000Z');

setMockedNow(now);

const result = countDaysBeforePayday(lastChargeDate);

expect(result).toBe(0);
});

it('should return negative days when payday has passed', () => {
const now = new Date('2025-12-25');
const lastChargeDate = new Date('2025-11-20');

setMockedNow(now);

const result = countDaysBeforePayday(lastChargeDate);

expect(result).toBe(-5); // Dec 20 - Dec 25 = -5 days
});

it('should use paidUntil when provided', () => {
const now = new Date('2025-12-01');
const lastChargeDate = new Date('2025-10-01');
const paidUntil = new Date('2025-12-15');

setMockedNow(now);

const result = countDaysBeforePayday(lastChargeDate, paidUntil);

expect(result).toBe(14); // Dec 15 - Dec 1 = 14 days
});

it('should work correctly in debug mode', () => {
const now = new Date('2025-12-01T00:00:00Z');
const lastChargeDate = new Date('2025-11-30T00:00:00Z');

setMockedNow(now);

const result = countDaysBeforePayday(lastChargeDate, null, true);

expect(result).toBe(0); // Next day is Dec 1, same as now
});

it('should handle cross-year payday correctly', () => {
const now = new Date('2025-12-20');
const lastChargeDate = new Date('2025-12-15');

setMockedNow(now);

const result = countDaysBeforePayday(lastChargeDate);

expect(result).toBe(26); // Jan 15, 2026 - Dec 20, 2025 = 26 days
});
});

describe('countDaysAfterPayday', () => {
it('should return 0 when payday is today', () => {
const now = new Date('2025-12-20T12:00:00Z');
const lastChargeDate = new Date('2025-11-20T00:00:00Z');

setMockedNow(now);

const result = countDaysAfterPayday(lastChargeDate);

expect(result).toBe(0);
});

it('should return positive days when payday has passed', () => {
const now = new Date('2025-12-25');
const lastChargeDate = new Date('2025-11-20');

setMockedNow(now);

const result = countDaysAfterPayday(lastChargeDate);

expect(result).toBe(5); // Dec 25 - Dec 20 = 5 days
});

it('should return negative days when payday is in the future', () => {
const now = new Date('2025-12-01');
const lastChargeDate = new Date('2025-11-20');

setMockedNow(now);

const result = countDaysAfterPayday(lastChargeDate);

expect(result).toBe(-19); // Dec 1 - Dec 20 = -19 days
});

it('should use paidUntil when provided', () => {
const now = new Date('2025-12-20');
const lastChargeDate = new Date('2025-10-01');
const paidUntil = new Date('2025-12-15');

setMockedNow(now);

const result = countDaysAfterPayday(lastChargeDate, paidUntil);

expect(result).toBe(5); // Dec 20 - Dec 15 = 5 days
});

it('should work correctly in debug mode', () => {
const now = new Date('2025-12-03T00:00:00Z');
const lastChargeDate = new Date('2025-12-01T00:00:00Z');

setMockedNow(now);

const result = countDaysAfterPayday(lastChargeDate, null, true);

expect(result).toBe(1); // Dec 3 - Dec 2 = 1 day
});

it('should be the inverse of countDaysBeforePayday', () => {
const now = new Date('2025-12-15');
const lastChargeDate = new Date('2025-11-20');

setMockedNow(now);

const daysBefore = countDaysBeforePayday(lastChargeDate);
const daysAfter = countDaysAfterPayday(lastChargeDate);

expect(daysBefore).toBe(-daysAfter);
});
});

describe('countDaysAfterBlock', () => {
it('should return undefined when blockedDate is not set', () => {
const workspace: WorkspaceDBScheme = {
_id: new ObjectId(),
name: 'Test Workspace',
inviteHash: 'test-hash',
tariffPlanId: new ObjectId(),
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
accountId: 'test-account',
balance: 0,
blockedDate: null,
};

const result = countDaysAfterBlock(workspace);

expect(result).toBeUndefined();
});

it('should return 0 when workspace was blocked today', () => {
const now = new Date('2025-12-18T12:00:00Z');
const blockedDate = new Date('2025-12-18T00:00:00Z');

setMockedNow(now);

const workspace: WorkspaceDBScheme = {
_id: new ObjectId(),
name: 'Test Workspace',
inviteHash: 'test-hash',
tariffPlanId: new ObjectId(),
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
accountId: 'test-account',
balance: 0,
blockedDate,
};

const result = countDaysAfterBlock(workspace);

expect(result).toBe(0);
});

it('should return correct number of days after block', () => {
const now = new Date('2025-12-18');
const blockedDate = new Date('2025-12-10');

setMockedNow(now);

const workspace: WorkspaceDBScheme = {
_id: new ObjectId(),
name: 'Test Workspace',
inviteHash: 'test-hash',
tariffPlanId: new ObjectId(),
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
accountId: 'test-account',
balance: 0,
blockedDate,
};

const result = countDaysAfterBlock(workspace);

expect(result).toBe(8); // Dec 18 - Dec 10 = 8 days
});

it('should handle cross-month blocks correctly', () => {
const now = new Date('2025-12-05');
const blockedDate = new Date('2025-11-28');

setMockedNow(now);

const workspace: WorkspaceDBScheme = {
_id: new ObjectId(),
name: 'Test Workspace',
inviteHash: 'test-hash',
tariffPlanId: new ObjectId(),
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
accountId: 'test-account',
balance: 0,
blockedDate,
};

const result = countDaysAfterBlock(workspace);

expect(result).toBe(7); // Dec 5 - Nov 28 = 7 days
});

it('should handle cross-year blocks correctly', () => {
const now = new Date('2026-01-05');
const blockedDate = new Date('2025-12-28');

setMockedNow(now);

const workspace: WorkspaceDBScheme = {
_id: new ObjectId(),
name: 'Test Workspace',
inviteHash: 'test-hash',
tariffPlanId: new ObjectId(),
billingPeriodEventsCount: 0,
lastChargeDate: new Date(),
accountId: 'test-account',
balance: 0,
blockedDate,
};

const result = countDaysAfterBlock(workspace);

expect(result).toBe(8); // Jan 5, 2026 - Dec 28, 2025 = 8 days
});
});
});
Loading
Loading