PSA/packages/billing/tests/clientContractEffectiveRenewalSettings.test.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

471 lines
14 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
computeDaysUntilDate,
computeEvergreenDecisionDueDate,
computeEvergreenCycleBounds,
computeNextEvergreenReviewAnchorDate,
dedupeClientContractsByRenewalCycle,
normalizeClientContract,
} from '../../../shared/billingClients/clientContracts';
describe('client contract effective renewal settings normalization', () => {
it('applies tenant defaults when use_tenant_renewal_defaults is true', () => {
const normalized = normalizeClientContract({
contract_id: 'contract-1',
client_contract_id: 'cc-1',
client_id: 'client-1',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
use_tenant_renewal_defaults: true,
renewal_mode: 'auto',
notice_period_days: 10,
tenant_default_renewal_mode: 'manual',
tenant_default_notice_period_days: 45,
});
expect(normalized.use_tenant_renewal_defaults).toBe(true);
expect(normalized.effective_renewal_mode).toBe('manual');
expect(normalized.effective_notice_period_days).toBe(45);
expect(normalized.assignment_status).toBe('active');
});
it('uses explicit values when tenant defaults are disabled', () => {
const normalized = normalizeClientContract({
contract_id: 'contract-2',
client_contract_id: 'cc-2',
client_id: 'client-2',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'auto',
notice_period_days: 60,
tenant_default_renewal_mode: 'manual',
tenant_default_notice_period_days: 30,
});
expect(normalized.use_tenant_renewal_defaults).toBe(false);
expect(normalized.effective_renewal_mode).toBe('auto');
expect(normalized.effective_notice_period_days).toBe(60);
});
it('falls back deterministically when explicit override values are partially missing', () => {
const normalized = normalizeClientContract({
contract_id: 'contract-3',
client_contract_id: 'cc-3',
client_id: 'client-3',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: null,
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: null,
notice_period_days: undefined,
tenant_default_renewal_mode: 'manual',
tenant_default_notice_period_days: 20,
});
expect(normalized.effective_renewal_mode).toBe('manual');
expect(normalized.effective_notice_period_days).toBe(20);
});
it('computes fixed-term decision_due_date from end_date minus effective notice period', () => {
const normalized = normalizeClientContract({
contract_id: 'contract-4',
client_contract_id: 'cc-4',
client_id: 'client-4',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
notice_period_days: 45,
});
expect(normalized.effective_notice_period_days).toBe(45);
expect(normalized.decision_due_date).toBe('2026-11-16');
expect(typeof normalized.days_until_due).toBe('number');
});
it('normalizes timestamp end_date to date-only semantics before fixed-term due-date math', () => {
const normalized = normalizeClientContract({
contract_id: 'contract-4b',
client_contract_id: 'cc-4b',
client_id: 'client-4b',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31T23:59:59.999Z',
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
notice_period_days: 1,
});
expect(normalized.decision_due_date).toBe('2026-12-30');
});
it('recomputes fixed-term decision_due_date when end_date changes', () => {
const baseAssignment = {
contract_id: 'contract-4c',
client_contract_id: 'cc-4c',
client_id: 'client-4c',
tenant: 'tenant-1',
start_date: '2026-01-01',
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
notice_period_days: 30,
};
const before = normalizeClientContract({
...baseAssignment,
end_date: '2026-12-31',
});
const after = normalizeClientContract({
...baseAssignment,
end_date: '2027-01-31',
});
expect(before.decision_due_date).toBe('2026-12-01');
expect(after.decision_due_date).toBe('2027-01-01');
});
it('recomputes fixed-term decision_due_date when notice period changes', () => {
const baseAssignment = {
contract_id: 'contract-4d',
client_contract_id: 'cc-4d',
client_id: 'client-4d',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
};
const shortNotice = normalizeClientContract({
...baseAssignment,
notice_period_days: 15,
});
const longNotice = normalizeClientContract({
...baseAssignment,
notice_period_days: 45,
});
expect(shortNotice.decision_due_date).toBe('2026-12-16');
expect(longNotice.decision_due_date).toBe('2026-11-16');
});
it('recomputes decision_due_date when renewal mode changes between none/manual/auto', () => {
const baseAssignment = {
contract_id: 'contract-4e',
client_contract_id: 'cc-4e',
client_id: 'client-4e',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
use_tenant_renewal_defaults: false,
notice_period_days: 30,
};
const noneMode = normalizeClientContract({
...baseAssignment,
renewal_mode: 'none',
});
const manualMode = normalizeClientContract({
...baseAssignment,
renewal_mode: 'manual',
});
const autoMode = normalizeClientContract({
...baseAssignment,
renewal_mode: 'auto',
});
expect(noneMode.decision_due_date).toBeUndefined();
expect(manualMode.decision_due_date).toBe('2026-12-01');
expect(autoMode.decision_due_date).toBe('2026-12-01');
});
it('skips decision_due_date generation only when the assignment itself is inactive', () => {
const inactiveAssignment = normalizeClientContract({
contract_id: 'contract-4f',
client_contract_id: 'cc-4f',
client_id: 'client-4f',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: false,
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
notice_period_days: 30,
});
const terminatedContract = normalizeClientContract({
contract_id: 'contract-4g',
client_contract_id: 'cc-4g',
client_id: 'client-4g',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
contract_status: 'terminated',
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
notice_period_days: 30,
});
expect(inactiveAssignment.decision_due_date).toBeUndefined();
expect(inactiveAssignment.evergreen_review_anchor_date).toBeUndefined();
expect(terminatedContract.decision_due_date).toBe('2026-12-01');
expect(terminatedContract.evergreen_review_anchor_date).toBeUndefined();
});
it('creates one renewal_cycle_key per computed contract cycle for deduplication', () => {
const fixedTerm = normalizeClientContract({
contract_id: 'contract-4h',
client_contract_id: 'cc-4h',
client_id: 'client-4h',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
notice_period_days: 30,
});
const evergreen = normalizeClientContract({
contract_id: 'contract-4i',
client_contract_id: 'cc-4i',
client_id: 'client-4i',
tenant: 'tenant-1',
start_date: '2024-10-04',
end_date: null,
is_active: true,
use_tenant_renewal_defaults: true,
tenant_default_renewal_mode: 'manual',
tenant_default_notice_period_days: 30,
});
const noneMode = normalizeClientContract({
contract_id: 'contract-4j',
client_contract_id: 'cc-4j',
client_id: 'client-4j',
tenant: 'tenant-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'none',
notice_period_days: 30,
});
expect(fixedTerm.renewal_cycle_key).toBe('fixed-term:2026-12-31');
expect(fixedTerm.renewal_cycle_start).toBe('2026-01-01');
expect(fixedTerm.renewal_cycle_end).toBe('2026-12-31');
expect(evergreen.renewal_cycle_key).toMatch(/^evergreen:\d{4}-\d{2}-\d{2}$/);
expect(evergreen.renewal_cycle_start).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(evergreen.renewal_cycle_end).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(noneMode.renewal_cycle_key).toBeUndefined();
});
it('deduplicates active rows by tenant + client_contract_id + renewal_cycle_key', () => {
const duplicateRows = dedupeClientContractsByRenewalCycle([
{
tenant: 'tenant-1',
client_contract_id: 'cc-dup',
client_id: 'client-1',
contract_id: 'contract-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
renewal_cycle_key: 'fixed-term:2026-12-31',
},
{
tenant: 'tenant-1',
client_contract_id: 'cc-dup',
client_id: 'client-1',
contract_id: 'contract-1',
start_date: '2026-01-01',
end_date: '2026-12-31',
is_active: true,
renewal_cycle_key: 'fixed-term:2026-12-31',
},
{
tenant: 'tenant-1',
client_contract_id: 'cc-dup',
client_id: 'client-1',
contract_id: 'contract-1',
start_date: '2026-01-01',
end_date: '2027-12-31',
is_active: true,
renewal_cycle_key: 'fixed-term:2027-12-31',
},
] as any);
expect(duplicateRows).toHaveLength(2);
expect(duplicateRows.map((row: any) => row.renewal_cycle_key)).toEqual([
'fixed-term:2026-12-31',
'fixed-term:2027-12-31',
]);
});
it('computes next evergreen review anchor date using contract anniversary rules', () => {
expect(
computeNextEvergreenReviewAnchorDate({
startDate: '2024-05-10',
now: '2026-05-01',
})
).toBe('2026-05-10');
expect(
computeNextEvergreenReviewAnchorDate({
startDate: '2024-05-10',
now: '2026-05-11',
})
).toBe('2027-05-10');
});
it('computes evergreen decision_due_date from annual anchor minus notice period', () => {
expect(
computeEvergreenDecisionDueDate({
startDate: '2024-05-10',
now: '2026-05-01',
noticePeriodDays: 20,
})
).toBe('2026-04-20');
});
it('recomputes evergreen decision_due_date when anniversary anchor basis changes', () => {
const now = '2026-01-01';
const noticePeriodDays = 20;
const marchAnniversary = computeEvergreenDecisionDueDate({
startDate: '2024-03-10',
now,
noticePeriodDays,
});
const septemberAnniversary = computeEvergreenDecisionDueDate({
startDate: '2024-09-10',
now,
noticePeriodDays,
});
expect(marchAnniversary).toBe('2026-02-18');
expect(septemberAnniversary).toBe('2026-08-21');
});
it('respects evergreen-specific notice period overrides when tenant defaults are disabled', () => {
const now = '2026-01-01';
const startDate = '2024-09-10';
const withTenantDefaults = normalizeClientContract({
contract_id: 'contract-4k',
client_contract_id: 'cc-4k',
client_id: 'client-4k',
tenant: 'tenant-1',
start_date: startDate,
end_date: null,
is_active: true,
use_tenant_renewal_defaults: true,
tenant_default_renewal_mode: 'manual',
tenant_default_notice_period_days: 30,
notice_period_days: 10,
} as any);
const withContractOverride = normalizeClientContract({
contract_id: 'contract-4l',
client_contract_id: 'cc-4l',
client_id: 'client-4l',
tenant: 'tenant-1',
start_date: startDate,
end_date: null,
is_active: true,
use_tenant_renewal_defaults: false,
renewal_mode: 'manual',
tenant_default_renewal_mode: 'manual',
tenant_default_notice_period_days: 30,
notice_period_days: 10,
} as any);
const expectedTenantDueDate = computeEvergreenDecisionDueDate({
startDate,
now,
noticePeriodDays: 30,
});
const expectedOverrideDueDate = computeEvergreenDecisionDueDate({
startDate,
now,
noticePeriodDays: 10,
});
expect(expectedTenantDueDate).toBe('2026-08-11');
expect(expectedOverrideDueDate).toBe('2026-08-31');
expect(withTenantDefaults.decision_due_date).toBe(expectedTenantDueDate);
expect(withContractOverride.decision_due_date).toBe(expectedOverrideDueDate);
expect(withContractOverride.decision_due_date).not.toBe(withTenantDefaults.decision_due_date);
});
it('computes evergreen cycle_start and cycle_end boundaries per annual cycle', () => {
expect(
computeEvergreenCycleBounds({
startDate: '2024-05-10',
now: '2026-05-01',
})
).toEqual({
cycleStart: '2025-05-10',
cycleEnd: '2026-05-10',
});
});
it('computes days_until_due as a date-only integer day delta', () => {
expect(
computeDaysUntilDate({
targetDate: '2026-05-10',
now: '2026-05-01',
})
).toBe(9);
expect(
computeDaysUntilDate({
targetDate: '2026-05-01',
now: '2026-05-10',
})
).toBe(-9);
expect(
computeDaysUntilDate({
targetDate: '1900-01-01',
now: '2026-05-10',
})
).toBe(-36500);
expect(
computeDaysUntilDate({
targetDate: '2200-01-01',
now: '2026-05-10',
})
).toBe(36500);
});
it('exposes evergreen_review_anchor_date on active evergreen assignments', () => {
const normalized = normalizeClientContract({
contract_id: 'contract-5',
client_contract_id: 'cc-5',
client_id: 'client-5',
tenant: 'tenant-1',
start_date: '2024-10-04',
end_date: null,
is_active: true,
use_tenant_renewal_defaults: true,
tenant_default_renewal_mode: 'manual',
tenant_default_notice_period_days: 30,
});
expect(normalized.evergreen_review_anchor_date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(normalized.decision_due_date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
});