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
163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
import type {
|
|
CadenceOwner,
|
|
IRecurringServicePeriodRegenerationDecision,
|
|
IRecurringServicePeriodRegenerationTriggerInput,
|
|
} from '@alga-psa/types';
|
|
|
|
const CONTRACT_LINE_REGENERATION_FIELDS = new Set([
|
|
'billing_frequency',
|
|
'billing_timing',
|
|
'start_date',
|
|
'end_date',
|
|
'service_start_date',
|
|
'service_end_date',
|
|
'cadence_owner',
|
|
]);
|
|
|
|
const CONTRACT_ASSIGNMENT_REGENERATION_FIELDS = new Set([
|
|
'assignment_start_date',
|
|
'assignment_end_date',
|
|
'service_start_date',
|
|
'service_end_date',
|
|
'start_date',
|
|
'end_date',
|
|
]);
|
|
|
|
const BILLING_SCHEDULE_REGENERATION_FIELDS = new Set([
|
|
'billing_frequency',
|
|
'billing_day_of_month',
|
|
'billing_month',
|
|
'billing_anchor_date',
|
|
'billing_cycle_anchor',
|
|
'next_billing_date',
|
|
]);
|
|
|
|
function uniqueCadenceOwners(owners: Array<CadenceOwner | undefined>) {
|
|
return Array.from(new Set(owners.filter((owner): owner is CadenceOwner => Boolean(owner))));
|
|
}
|
|
|
|
function collectMatchingFields(changedFields: string[], triggerFields: Set<string>) {
|
|
return changedFields.filter((field) => triggerFields.has(field));
|
|
}
|
|
|
|
export function resolveRecurringServicePeriodRegenerationDecision(
|
|
input: IRecurringServicePeriodRegenerationTriggerInput,
|
|
): IRecurringServicePeriodRegenerationDecision {
|
|
const changedFields = [...new Set(input.changedFields)];
|
|
const cadenceOwnerChanged = Boolean(
|
|
input.cadenceOwnerBefore
|
|
&& input.cadenceOwnerAfter
|
|
&& input.cadenceOwnerBefore !== input.cadenceOwnerAfter,
|
|
);
|
|
|
|
if (input.source === 'contract_line_edit' && cadenceOwnerChanged) {
|
|
return {
|
|
shouldRegenerate: true,
|
|
triggerKind: 'cadence_owner_change',
|
|
regenerationReasonCode: 'cadence_owner_changed',
|
|
scope: 'replace_schedule_identity',
|
|
changedFields,
|
|
affectedCadenceOwners: uniqueCadenceOwners([
|
|
input.cadenceOwnerBefore,
|
|
input.cadenceOwnerAfter,
|
|
]),
|
|
preserveEditedRows: true,
|
|
preserveBilledHistory: true,
|
|
reason: 'Changing cadence owner replaces the future schedule identity instead of mutating billed history in place.',
|
|
notes: [
|
|
'Supersede untouched future rows on the prior schedule key.',
|
|
'Materialize future rows on the new cadence-owner schedule key.',
|
|
'Preserve edited, locked, and billed rows under the existing override and immutability rules.',
|
|
],
|
|
};
|
|
}
|
|
|
|
if (input.source === 'contract_line_edit') {
|
|
const triggerFields = collectMatchingFields(changedFields, CONTRACT_LINE_REGENERATION_FIELDS);
|
|
if (triggerFields.length > 0) {
|
|
return {
|
|
shouldRegenerate: true,
|
|
triggerKind: 'contract_line_edit',
|
|
regenerationReasonCode: 'source_rule_changed',
|
|
scope: 'obligation_schedule_only',
|
|
changedFields,
|
|
affectedCadenceOwners: uniqueCadenceOwners([
|
|
input.cadenceOwnerAfter,
|
|
input.cadenceOwnerBefore,
|
|
]),
|
|
preserveEditedRows: true,
|
|
preserveBilledHistory: true,
|
|
reason: `Contract-line recurrence fields changed (${triggerFields.join(', ')}), so future service-period and invoice-window candidates must be rebuilt for that obligation.`,
|
|
notes: [
|
|
'Only recurrence-shaping fields trigger service-period regeneration.',
|
|
'Pure pricing changes still affect billing amounts later, but do not rebuild the persisted schedule.',
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
if (input.source === 'contract_assignment_edit') {
|
|
const triggerFields = collectMatchingFields(changedFields, CONTRACT_ASSIGNMENT_REGENERATION_FIELDS);
|
|
if (triggerFields.length > 0) {
|
|
return {
|
|
shouldRegenerate: true,
|
|
triggerKind: 'contract_assignment_edit',
|
|
regenerationReasonCode: 'activity_window_changed',
|
|
scope: 'obligation_schedule_only',
|
|
changedFields,
|
|
affectedCadenceOwners: uniqueCadenceOwners([
|
|
input.cadenceOwnerAfter,
|
|
input.cadenceOwnerBefore,
|
|
]),
|
|
preserveEditedRows: true,
|
|
preserveBilledHistory: true,
|
|
reason: `Assignment activity-window fields changed (${triggerFields.join(', ')}), so future coverage clipping must be regenerated for that obligation.`,
|
|
notes: [
|
|
'This changes activity-window intersection and may alter partial first or final coverage.',
|
|
'Billed history remains immutable and edited future overrides stay preserved.',
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
if (input.source === 'billing_schedule_edit') {
|
|
const triggerFields = collectMatchingFields(changedFields, BILLING_SCHEDULE_REGENERATION_FIELDS);
|
|
if (triggerFields.length > 0) {
|
|
return {
|
|
shouldRegenerate: true,
|
|
triggerKind: 'billing_schedule_change',
|
|
regenerationReasonCode: 'billing_schedule_changed',
|
|
scope: 'client_cadence_dependents',
|
|
changedFields,
|
|
affectedCadenceOwners: ['client'],
|
|
preserveEditedRows: true,
|
|
preserveBilledHistory: true,
|
|
reason: `Client billing-schedule fields changed (${triggerFields.join(', ')}), so client-cadence obligations that depend on that schedule need regenerated future invoice windows.`,
|
|
notes: [
|
|
'Only client-cadence obligations are in scope for a billing-schedule trigger.',
|
|
'Contract-cadence schedules keep their own anniversary-owned window identity.',
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
shouldRegenerate: false,
|
|
triggerKind: null,
|
|
regenerationReasonCode: null,
|
|
scope: null,
|
|
changedFields,
|
|
affectedCadenceOwners: uniqueCadenceOwners([
|
|
input.cadenceOwnerAfter,
|
|
input.cadenceOwnerBefore,
|
|
]),
|
|
preserveEditedRows: true,
|
|
preserveBilledHistory: true,
|
|
reason: 'No service-period or invoice-window shaping fields changed, so regeneration is not required.',
|
|
notes: [
|
|
'Pricing-only changes do not rebuild persisted future periods.',
|
|
'Override-preservation and immutability rules still apply if a later regeneration is triggered by a different source change.',
|
|
],
|
|
};
|
|
}
|