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
151 lines
5.7 KiB
JavaScript
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);
|
|
};
|