PSA/shared/billingClients/recurringDueWork.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

259 lines
8.9 KiB
TypeScript

import type {
IRecurringDueSelectionInput,
IRecurringDueWorkAttribution,
IRecurringDueWorkRow,
IRecurringRunExecutionWindowIdentity,
IRecurringServicePeriodRecord,
ISO8601String,
RecurringDueWorkCadenceSource,
} from '@alga-psa/types';
import {
buildClientCadenceDueSelectionInput,
buildContractCadenceDueSelectionInput,
buildRecurringRunSelectionIdentity,
} from './recurringRunExecutionIdentity';
export interface RecurringDueWorkIdentity {
rowKey: string;
executionIdentityKey: string;
selectionKey: string;
retryKey: string;
}
interface BuildRecurringDueWorkRowInput {
selectorInput: IRecurringDueSelectionInput;
cadenceSource: RecurringDueWorkCadenceSource;
duePosition: IRecurringDueWorkRow['duePosition'];
servicePeriodStart: ISO8601String;
servicePeriodEnd: ISO8601String;
clientName?: string | null;
canGenerate?: boolean;
asOf?: ISO8601String;
billingCycleId?: string | null;
scheduleKey?: string | null;
periodKey?: string | null;
recordId?: string | null;
lifecycleState?: IRecurringDueWorkRow['lifecycleState'];
contractName?: string | null;
contractLineName?: string | null;
purchaseOrderScopeKey?: string | null;
currencyCode?: string | null;
taxSource?: string | null;
exportShapeKey?: string | null;
attribution?: IRecurringDueWorkAttribution;
}
export interface ClientScheduleDueWorkWindowInput {
clientId: string;
clientName?: string | null;
scheduleKey: string;
periodKey: string;
servicePeriodStart: ISO8601String;
servicePeriodEnd: ISO8601String;
invoiceWindowStart?: ISO8601String;
invoiceWindowEnd?: ISO8601String;
asOf?: ISO8601String;
canGenerate?: boolean;
billingCycleId?: string | null;
}
export interface ServicePeriodDueWorkRecordInput {
clientId: string;
clientName?: string | null;
record: IRecurringServicePeriodRecord;
billingCycleId?: string | null;
contractId?: string | null;
contractLineId?: string | null;
contractName?: string | null;
contractLineName?: string | null;
attribution?: IRecurringDueWorkAttribution;
asOf?: ISO8601String;
canGenerate?: boolean;
}
function formatRangeLabel(start: ISO8601String, end: ISO8601String) {
return `${start} to ${end}`;
}
function normalizeDueWorkDate(value: unknown): ISO8601String {
if (value instanceof Date) {
return value.toISOString().slice(0, 10) as ISO8601String;
}
return String(value).slice(0, 10) as ISO8601String;
}
function buildRecurringDueWorkIdentity(
executionWindow: IRecurringRunExecutionWindowIdentity,
): RecurringDueWorkIdentity {
const selectionIdentity = buildRecurringRunSelectionIdentity([executionWindow]);
return {
rowKey: `recurring-due-row:${executionWindow.identityKey}`,
executionIdentityKey: executionWindow.identityKey,
selectionKey: selectionIdentity.selectionKey,
retryKey: selectionIdentity.retryKey,
};
}
function isEarlyInvoiceWindow(windowStart: ISO8601String, asOf?: ISO8601String) {
if (!asOf) {
return false;
}
return String(windowStart).slice(0, 10) > String(asOf).slice(0, 10);
}
function buildBaseRecurringDueWorkRow(input: BuildRecurringDueWorkRowInput): IRecurringDueWorkRow {
const executionWindow = input.selectorInput.executionWindow;
const identity = buildRecurringDueWorkIdentity(executionWindow);
const billingCycleId = input.billingCycleId ?? null;
const invoiceWindowStart = normalizeDueWorkDate(input.selectorInput.windowStart);
const invoiceWindowEnd = normalizeDueWorkDate(input.selectorInput.windowEnd);
const contractId = executionWindow.contractId ?? null;
const contractLineId = executionWindow.contractLineId ?? null;
const isEarly = isEarlyInvoiceWindow(invoiceWindowStart, input.asOf);
const servicePeriodStart = normalizeDueWorkDate(input.servicePeriodStart);
const servicePeriodEnd = normalizeDueWorkDate(input.servicePeriodEnd);
const canGenerate = (input.canGenerate ?? true) && !isEarly;
return {
...identity,
selectorInput: input.selectorInput,
executionWindow,
executionWindowKind: executionWindow.kind,
cadenceOwner: executionWindow.cadenceOwner,
cadenceSource: input.cadenceSource,
duePosition: input.duePosition,
dueState: isEarly ? 'early' : 'due',
isEarly,
canGenerate,
clientId: input.selectorInput.clientId,
clientName: input.clientName ?? null,
billingCycleId,
servicePeriodStart,
servicePeriodEnd,
servicePeriodLabel: formatRangeLabel(servicePeriodStart, servicePeriodEnd),
invoiceWindowStart,
invoiceWindowEnd,
invoiceWindowLabel: formatRangeLabel(invoiceWindowStart, invoiceWindowEnd),
scheduleKey: input.scheduleKey ?? null,
periodKey: input.periodKey ?? null,
recordId: input.recordId ?? null,
lifecycleState: input.lifecycleState ?? null,
contractId,
contractLineId,
contractName: input.contractName ?? null,
contractLineName: input.contractLineName ?? null,
purchaseOrderScopeKey: input.purchaseOrderScopeKey ?? null,
currencyCode: input.currencyCode ?? null,
taxSource: input.taxSource ?? null,
exportShapeKey: input.exportShapeKey ?? null,
attribution: input.attribution,
};
}
export function buildRecurringDueWorkRow(
input: BuildRecurringDueWorkRowInput,
): IRecurringDueWorkRow {
return buildBaseRecurringDueWorkRow(input);
}
export function sortRecurringDueWorkRows(rows: IRecurringDueWorkRow[]): IRecurringDueWorkRow[] {
return [...rows].sort((left, right) => {
if (left.invoiceWindowEnd !== right.invoiceWindowEnd) {
return right.invoiceWindowEnd.localeCompare(left.invoiceWindowEnd);
}
if (left.invoiceWindowStart !== right.invoiceWindowStart) {
return right.invoiceWindowStart.localeCompare(left.invoiceWindowStart);
}
if (left.servicePeriodEnd !== right.servicePeriodEnd) {
return right.servicePeriodEnd.localeCompare(left.servicePeriodEnd);
}
if (left.servicePeriodStart !== right.servicePeriodStart) {
return right.servicePeriodStart.localeCompare(left.servicePeriodStart);
}
if ((left.clientName ?? '') !== (right.clientName ?? '')) {
return (left.clientName ?? '').localeCompare(right.clientName ?? '');
}
return left.executionIdentityKey.localeCompare(right.executionIdentityKey);
});
}
export function buildClientScheduleDueWorkRow(
input: ClientScheduleDueWorkWindowInput,
): IRecurringDueWorkRow {
const servicePeriodStart = normalizeDueWorkDate(input.servicePeriodStart);
const servicePeriodEnd = normalizeDueWorkDate(input.servicePeriodEnd);
const invoiceWindowStart = normalizeDueWorkDate(input.invoiceWindowStart ?? servicePeriodStart);
const invoiceWindowEnd = normalizeDueWorkDate(input.invoiceWindowEnd ?? servicePeriodEnd);
const selectorInput = buildClientCadenceDueSelectionInput({
clientId: input.clientId,
scheduleKey: input.scheduleKey,
periodKey: input.periodKey,
windowStart: invoiceWindowStart,
windowEnd: invoiceWindowEnd,
});
return buildBaseRecurringDueWorkRow({
selectorInput,
cadenceSource: 'client_schedule',
duePosition: input.scheduleKey.includes(':arrears') ? 'arrears' : 'advance',
servicePeriodStart,
servicePeriodEnd,
clientName: input.clientName,
canGenerate: input.canGenerate,
asOf: input.asOf,
billingCycleId: input.billingCycleId ?? null,
scheduleKey: input.scheduleKey,
periodKey: input.periodKey,
});
}
export function buildServicePeriodRecurringDueWorkRow(
input: ServicePeriodDueWorkRecordInput,
): IRecurringDueWorkRow {
const { record } = input;
const invoiceWindowStart = normalizeDueWorkDate(record.invoiceWindow.start);
const invoiceWindowEnd = normalizeDueWorkDate(record.invoiceWindow.end);
const selectorInput = record.cadenceOwner === 'contract'
? buildContractCadenceDueSelectionInput({
clientId: input.clientId,
contractId: input.contractId ?? null,
contractLineId: input.contractLineId ?? null,
windowStart: invoiceWindowStart,
windowEnd: invoiceWindowEnd,
})
: buildClientCadenceDueSelectionInput({
clientId: input.clientId,
scheduleKey: record.scheduleKey,
periodKey: record.periodKey,
windowStart: invoiceWindowStart,
windowEnd: invoiceWindowEnd,
});
return buildBaseRecurringDueWorkRow({
selectorInput,
cadenceSource: record.cadenceOwner === 'contract'
? 'contract_anniversary'
: 'client_schedule',
duePosition: record.duePosition,
billingCycleId: input.billingCycleId ?? null,
servicePeriodStart: normalizeDueWorkDate(record.servicePeriod.start),
servicePeriodEnd: normalizeDueWorkDate(record.servicePeriod.end),
clientName: input.clientName,
canGenerate: input.canGenerate,
asOf: input.asOf,
scheduleKey: record.scheduleKey,
periodKey: record.periodKey,
recordId: record.recordId,
lifecycleState: record.lifecycleState,
contractName: input.contractName,
contractLineName: input.contractLineName,
attribution: input.attribution,
});
}
export { buildRecurringDueWorkIdentity };