PSA/server/migrations/20260318120000_create_recurring_service_periods.cjs
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

151 lines
5.7 KiB
JavaScript

const TABLE = 'recurring_service_periods';
const CHECK_CONSTRAINTS = {
cadenceOwner: `${TABLE}_cadence_owner_check`,
duePosition: `${TABLE}_due_position_check`,
lifecycleState: `${TABLE}_lifecycle_state_check`,
obligationType: `${TABLE}_obligation_type_check`,
chargeFamily: `${TABLE}_charge_family_check`,
provenanceKind: `${TABLE}_provenance_kind_check`,
revision: `${TABLE}_revision_check`,
servicePeriod: `${TABLE}_service_period_range_check`,
invoiceWindow: `${TABLE}_invoice_window_range_check`,
activityWindow: `${TABLE}_activity_window_check`,
supersedes: `${TABLE}_supersedes_record_check`,
};
/**
* Create the first persisted recurring service-period ledger table.
* F231 defined the logical contract; this migration lands the physical shape,
* integrity constraints, and lookup indexes that later materialization/runtime
* passes will consume.
*
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
const hasTable = await knex.schema.hasTable(TABLE);
if (!hasTable) {
await knex.schema.createTable(TABLE, (table) => {
table.uuid('record_id').notNullable().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('tenant').notNullable();
table.string('schedule_key', 255).notNullable();
table.string('period_key', 255).notNullable();
table.integer('revision').notNullable().defaultTo(1);
table.uuid('obligation_id').notNullable();
table.string('obligation_type', 40).notNullable();
table.string('charge_family', 20).notNullable();
table.string('cadence_owner', 16).notNullable();
table.string('due_position', 16).notNullable();
table.string('lifecycle_state', 20).notNullable().defaultTo('generated');
table.date('service_period_start').notNullable();
table.date('service_period_end').notNullable();
table.date('invoice_window_start').notNullable();
table.date('invoice_window_end').notNullable();
table.date('activity_window_start').nullable();
table.date('activity_window_end').nullable();
table.jsonb('timing_metadata').nullable();
table.string('provenance_kind', 20).notNullable();
table.string('source_rule_version', 255).notNullable();
table.string('reason_code', 100).nullable();
table.string('source_run_key', 255).nullable();
table.uuid('supersedes_record_id').nullable();
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
table.primary(['tenant', 'record_id']);
table.unique(
['tenant', 'schedule_key', 'period_key', 'revision'],
`${TABLE}_tenant_schedule_period_revision_uidx`,
);
table.index(
['tenant', 'schedule_key', 'service_period_start'],
`${TABLE}_tenant_schedule_start_idx`,
);
table.index(
['tenant', 'obligation_id', 'lifecycle_state'],
`${TABLE}_tenant_obligation_state_idx`,
);
table.index(
['tenant', 'lifecycle_state', 'invoice_window_start', 'invoice_window_end'],
`${TABLE}_tenant_due_selection_idx`,
);
});
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.cadenceOwner}
CHECK (cadence_owner IN ('client', 'contract'))
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.duePosition}
CHECK (due_position IN ('advance', 'arrears'))
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.lifecycleState}
CHECK (lifecycle_state IN ('generated', 'edited', 'skipped', 'locked', 'billed', 'superseded', 'archived'))
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.obligationType}
CHECK (obligation_type IN ('contract_line', 'client_contract_line', 'template_line', 'preset_line'))
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.chargeFamily}
CHECK (charge_family IN ('fixed', 'product', 'license', 'bucket', 'hourly', 'usage'))
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.provenanceKind}
CHECK (provenance_kind IN ('generated', 'user_edited', 'regenerated', 'repair'))
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.revision}
CHECK (revision >= 1)
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.servicePeriod}
CHECK (service_period_start < service_period_end)
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.invoiceWindow}
CHECK (invoice_window_start < invoice_window_end)
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.activityWindow}
CHECK (
(activity_window_start IS NULL AND activity_window_end IS NULL)
OR (
activity_window_start IS NOT NULL
AND activity_window_end IS NOT NULL
AND activity_window_start < activity_window_end
AND activity_window_start >= service_period_start
AND activity_window_end <= service_period_end
)
)
`);
await knex.raw(`
ALTER TABLE ${TABLE}
ADD CONSTRAINT ${CHECK_CONSTRAINTS.supersedes}
CHECK (supersedes_record_id IS NULL OR supersedes_record_id <> record_id)
`);
}
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
for (const constraint of Object.values(CHECK_CONSTRAINTS)) {
await knex.raw(`ALTER TABLE ${TABLE} DROP CONSTRAINT IF EXISTS ${constraint}`);
}
await knex.schema.dropTableIfExists(TABLE);
};