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

272 lines
8.4 KiB
TypeScript

import type {
BillingCycleType,
DuePosition,
IRecurringDueSelectionInput,
IRecurringObligationRef,
IRecurringRunExecutionWindowIdentity,
ISO8601String,
RecurringRunExecutionWindowKind,
} from '@alga-psa/types';
import { Temporal } from '@js-temporal/polyfill';
import {
generateAnnualContractCadenceServicePeriods,
generateMonthlyContractCadenceServicePeriods,
generateQuarterlyContractCadenceServicePeriods,
generateSemiAnnualContractCadenceServicePeriods,
resolveContractCadenceInvoiceWindowForServicePeriod,
} from './contractCadenceServicePeriods';
export interface RecurringRunSelectionIdentity {
executionIdentityKeys: string[];
selectionKey: string;
retryKey: string;
}
export type ContractCadenceSchedulableFrequency = Extract<
BillingCycleType,
'monthly' | 'quarterly' | 'semi-annually' | 'annually'
>;
export interface ContractCadenceRecurringRunTarget {
executionWindow: IRecurringRunExecutionWindowIdentity;
selectorInput: IRecurringDueSelectionInput;
servicePeriodStart: ISO8601String;
servicePeriodEnd: ISO8601String;
}
function compactIdentitySegments(segments: Array<unknown>): string[] {
return segments
.map((segment) => {
if (segment == null) {
return undefined;
}
return String(segment).trim();
})
.filter((segment): segment is string => Boolean(segment && segment.length > 0));
}
export function buildRecurringRunExecutionIdentityKey(
window: Omit<IRecurringRunExecutionWindowIdentity, 'identityKey'>,
): string {
return compactIdentitySegments([
window.kind,
window.cadenceOwner,
window.clientId,
window.scheduleKey ?? undefined,
window.periodKey ?? undefined,
window.contractId ?? undefined,
window.contractLineId ?? undefined,
window.windowStart ?? undefined,
window.windowEnd ?? undefined,
]).join(':');
}
export function buildClientCadenceExecutionWindow(input: {
clientId: string;
scheduleKey: string;
periodKey: string;
windowStart: string;
windowEnd: string;
}): IRecurringRunExecutionWindowIdentity {
const baseWindow = {
kind: 'client_cadence_window' as const,
cadenceOwner: 'client' as const,
clientId: input.clientId,
scheduleKey: input.scheduleKey,
periodKey: input.periodKey,
windowStart: input.windowStart,
windowEnd: input.windowEnd,
};
return {
...baseWindow,
identityKey: buildRecurringRunExecutionIdentityKey(baseWindow),
};
}
export function buildContractCadenceExecutionWindow(input: {
clientId: string;
windowStart: string;
windowEnd: string;
contractId?: string | null;
contractLineId?: string | null;
}): IRecurringRunExecutionWindowIdentity {
const baseWindow = {
kind: 'contract_cadence_window' as const,
cadenceOwner: 'contract' as const,
clientId: input.clientId,
contractId: input.contractId ?? null,
contractLineId: input.contractLineId ?? null,
windowStart: input.windowStart,
windowEnd: input.windowEnd,
};
return {
...baseWindow,
identityKey: buildRecurringRunExecutionIdentityKey(baseWindow),
};
}
export function listRecurringRunExecutionWindowKinds(
windows: Array<Pick<IRecurringRunExecutionWindowIdentity, 'kind'>>,
): RecurringRunExecutionWindowKind[] {
return Array.from(new Set(windows.map((window) => window.kind))).sort() as RecurringRunExecutionWindowKind[];
}
export function buildRecurringRunSelectionIdentity(
windows: Array<Pick<IRecurringRunExecutionWindowIdentity, 'identityKey'>>,
): RecurringRunSelectionIdentity {
const executionIdentityKeys = Array.from(
new Set(windows.map((window) => window.identityKey).filter(Boolean)),
).sort();
const keyBody = executionIdentityKeys.join('|');
return {
executionIdentityKeys,
selectionKey: `recurring-run-selection:${keyBody}`,
retryKey: `recurring-run-retry:${keyBody}`,
};
}
export function buildClientCadenceDueSelectionInput(input: {
clientId: string;
scheduleKey: string;
periodKey: string;
windowStart: string;
windowEnd: string;
}): IRecurringDueSelectionInput {
return {
clientId: input.clientId,
windowStart: input.windowStart,
windowEnd: input.windowEnd,
executionWindow: buildClientCadenceExecutionWindow({
clientId: input.clientId,
scheduleKey: input.scheduleKey,
periodKey: input.periodKey,
windowStart: input.windowStart,
windowEnd: input.windowEnd,
}),
};
}
export function buildContractCadenceDueSelectionInput(input: {
clientId: string;
windowStart: string;
windowEnd: string;
contractId?: string | null;
contractLineId?: string | null;
}): IRecurringDueSelectionInput {
return {
clientId: input.clientId,
windowStart: input.windowStart,
windowEnd: input.windowEnd,
executionWindow: buildContractCadenceExecutionWindow({
clientId: input.clientId,
contractId: input.contractId ?? null,
contractLineId: input.contractLineId ?? null,
windowStart: input.windowStart,
windowEnd: input.windowEnd,
}),
};
}
const toPlainDate = (value: ISO8601String) => Temporal.PlainDate.from(value.slice(0, 10));
function rangesOverlap(
left: { windowStart: ISO8601String; windowEnd: ISO8601String },
right: { windowStart: ISO8601String; windowEnd: ISO8601String },
): boolean {
return (
Temporal.PlainDate.compare(toPlainDate(left.windowStart), toPlainDate(right.windowEnd)) < 0 &&
Temporal.PlainDate.compare(toPlainDate(right.windowStart), toPlainDate(left.windowEnd)) < 0
);
}
function getContractCadenceFrequencyDefinition(
frequency: ContractCadenceSchedulableFrequency,
): {
monthsPerPeriod: number;
generate: (input: {
rangeStart: ISO8601String;
rangeEnd: ISO8601String;
sourceObligation: IRecurringObligationRef;
duePosition: DuePosition;
anchorDate: ISO8601String;
}) => ReturnType<typeof generateMonthlyContractCadenceServicePeriods>;
} {
switch (frequency) {
case 'monthly':
return { monthsPerPeriod: 1, generate: generateMonthlyContractCadenceServicePeriods };
case 'quarterly':
return { monthsPerPeriod: 3, generate: generateQuarterlyContractCadenceServicePeriods };
case 'semi-annually':
return { monthsPerPeriod: 6, generate: generateSemiAnnualContractCadenceServicePeriods };
case 'annually':
return { monthsPerPeriod: 12, generate: generateAnnualContractCadenceServicePeriods };
}
}
export function selectContractCadenceRecurringRunTargets(input: {
clientId: string;
frequency: ContractCadenceSchedulableFrequency;
duePosition: DuePosition;
anchorDate: ISO8601String;
rangeStart: ISO8601String;
rangeEnd: ISO8601String;
sourceObligation: IRecurringObligationRef;
contractId?: string | null;
contractLineId?: string | null;
}): ContractCadenceRecurringRunTarget[] {
const definition = getContractCadenceFrequencyDefinition(input.frequency);
const servicePeriodSearchStart = `${toPlainDate(input.rangeStart).subtract({ months: definition.monthsPerPeriod }).toString()}T00:00:00Z` as ISO8601String;
const queryRange = {
windowStart: input.rangeStart,
windowEnd: input.rangeEnd,
};
return definition
.generate({
rangeStart: servicePeriodSearchStart,
rangeEnd: input.rangeEnd,
sourceObligation: input.sourceObligation,
duePosition: input.duePosition,
anchorDate: input.anchorDate,
})
.map((servicePeriod) => {
const invoiceWindow = resolveContractCadenceInvoiceWindowForServicePeriod({
servicePeriod,
anchorDate: input.anchorDate,
monthsPerPeriod: definition.monthsPerPeriod,
});
const selectorInput = buildContractCadenceDueSelectionInput({
clientId: input.clientId,
contractId: input.contractId ?? null,
contractLineId: input.contractLineId ?? null,
windowStart: invoiceWindow.start,
windowEnd: invoiceWindow.end,
});
return {
executionWindow: selectorInput.executionWindow,
selectorInput,
servicePeriodStart: servicePeriod.start,
servicePeriodEnd: servicePeriod.end,
};
})
.filter((target) =>
rangesOverlap(
{
windowStart: target.selectorInput.windowStart,
windowEnd: target.selectorInput.windowEnd,
},
queryRange,
),
)
.sort((left, right) => {
if (left.selectorInput.windowStart !== right.selectorInput.windowStart) {
return left.selectorInput.windowStart.localeCompare(right.selectorInput.windowStart);
}
return left.executionWindow.identityKey.localeCompare(right.executionWindow.identityKey);
});
}