mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 01:20:55 +00:00
79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
import { MigrationInterface, QueryRunner } from "typeorm";
|
||
|
||
/**
|
||
* Maker–checker for manual actions on shipping-line credit invoices.
|
||
*
|
||
* A shipping-line credit invoice is normally settled by the CBE webhook. Two
|
||
* manual paths exist for finance: recording an offline payment (MARK_PAID)
|
||
* and voiding an invoice raised in error (CANCEL, which releases its credits
|
||
* back to the unbilled pool). Both erase or move real debt, so neither is a
|
||
* single-person action: one permission raises the request, a different
|
||
* permission — held by a chief, and never the requester themselves — approves
|
||
* or rejects it. Rows are never deleted; decided requests are the audit trail.
|
||
*
|
||
* One PENDING row per invoice at a time (partial unique index): a second
|
||
* request while one is undecided is a coordination failure, not a workflow.
|
||
*/
|
||
export class ShippingLineInvoiceApprovals3530000000000
|
||
implements MigrationInterface
|
||
{
|
||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||
await queryRunner.query(`
|
||
DO $$ BEGIN
|
||
CREATE TYPE freight.shipping_line_invoice_approvals_action_enum
|
||
AS ENUM ('MARK_PAID', 'CANCEL');
|
||
EXCEPTION WHEN duplicate_object THEN NULL; END $$
|
||
`);
|
||
|
||
await queryRunner.query(`
|
||
DO $$ BEGIN
|
||
CREATE TYPE freight.shipping_line_invoice_approvals_status_enum
|
||
AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
|
||
EXCEPTION WHEN duplicate_object THEN NULL; END $$
|
||
`);
|
||
|
||
await queryRunner.query(`
|
||
CREATE TABLE IF NOT EXISTS freight.shipping_line_invoice_approvals (
|
||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
invoice_id uuid NOT NULL REFERENCES freight.invoices (id),
|
||
action freight.shipping_line_invoice_approvals_action_enum NOT NULL,
|
||
status freight.shipping_line_invoice_approvals_status_enum NOT NULL DEFAULT 'PENDING',
|
||
requested_by uuid NOT NULL,
|
||
reason varchar(500) NOT NULL,
|
||
payment_reference varchar(255),
|
||
decided_by uuid,
|
||
decided_at timestamptz,
|
||
decision_note varchar(500),
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
deleted_at timestamptz
|
||
)
|
||
`);
|
||
|
||
await queryRunner.query(`
|
||
CREATE INDEX IF NOT EXISTS idx_sl_invoice_approvals_invoice_status
|
||
ON freight.shipping_line_invoice_approvals (invoice_id, status)
|
||
`);
|
||
|
||
// The workflow invariant, enforced where it cannot race: at most one
|
||
// undecided request per invoice.
|
||
await queryRunner.query(`
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uq_sl_invoice_approvals_one_pending
|
||
ON freight.shipping_line_invoice_approvals (invoice_id)
|
||
WHERE status = 'PENDING' AND deleted_at IS NULL
|
||
`);
|
||
}
|
||
|
||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||
await queryRunner.query(
|
||
`DROP TABLE IF EXISTS freight.shipping_line_invoice_approvals`,
|
||
);
|
||
await queryRunner.query(
|
||
`DROP TYPE IF EXISTS freight.shipping_line_invoice_approvals_status_enum`,
|
||
);
|
||
await queryRunner.query(
|
||
`DROP TYPE IF EXISTS freight.shipping_line_invoice_approvals_action_enum`,
|
||
);
|
||
}
|
||
}
|