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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
471 lines
14 KiB
TypeScript
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}$/);
|
|
});
|
|
});
|