mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- Add migration for consolidation approvals table and status enum - Create ConsolidationApprovalService to handle approval logic - Implement repository for managing consolidation approvals - Add entity for consolidation approval with necessary fields - Develop frontend components for displaying and managing consolidation approvals - Create tests for consolidation approval service to ensure correct behavior
90 lines
3.8 KiB
TypeScript
90 lines
3.8 KiB
TypeScript
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
|
|
/**
|
|
* Approval gate for consolidated (shared-wagon) bookings.
|
|
*
|
|
* A booking that fills its own wagons goes straight from GL completion to the
|
|
* operations queue. A CONSOLIDATED booking does not: it shares one physical
|
|
* wagon with another customer's booking, which means two customers' cargo, two
|
|
* invoices and two liabilities riding the same wagon. That pairing is a
|
|
* commercial decision, so it is reviewed by a person before Operations sees it.
|
|
*
|
|
* The pair is approved as a UNIT — one row covers both halves (booking_id +
|
|
* partner_booking_id) so an approver can never approve one side of a shared
|
|
* wagon and leave the other pending. Rows are never deleted; decided rows are
|
|
* the audit trail of who approved which pairing and when.
|
|
*
|
|
* One PENDING row per booking at a time (partial unique index on each side of
|
|
* the pair): a second request while one is undecided is a coordination failure,
|
|
* not a workflow.
|
|
*/
|
|
export class ConsolidationApprovals3570000000000 implements MigrationInterface {
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
DO $$ BEGIN
|
|
CREATE TYPE freight.consolidation_approvals_status_enum
|
|
AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE TABLE IF NOT EXISTS freight.consolidation_approvals (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
booking_id uuid NOT NULL REFERENCES freight.bookings (id),
|
|
partner_booking_id uuid NOT NULL REFERENCES freight.bookings (id),
|
|
status freight.consolidation_approvals_status_enum NOT NULL DEFAULT 'PENDING',
|
|
-- Who put the pairing up for review (the GL user who completed it) and
|
|
-- who decided it. Both are recorded: the point of the gate is that they
|
|
-- are different people.
|
|
requested_by uuid,
|
|
requested_at timestamptz NOT NULL DEFAULT now(),
|
|
decided_by uuid,
|
|
decided_at timestamptz,
|
|
decision_note varchar(500),
|
|
-- Snapshot of what was approved, so the audit trail still reads
|
|
-- correctly after the bookings themselves move on.
|
|
scheduled_date timestamptz,
|
|
booking_reference varchar(50),
|
|
partner_booking_reference varchar(50),
|
|
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_consolidation_approvals_booking_status
|
|
ON freight.consolidation_approvals (booking_id, status)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE INDEX IF NOT EXISTS idx_consolidation_approvals_status
|
|
ON freight.consolidation_approvals (status)
|
|
`);
|
|
|
|
// The workflow invariant, enforced where it cannot race: at most one
|
|
// undecided request per booking — on EITHER side of the pair, so the same
|
|
// wagon can never collect two pending requests from its two halves.
|
|
await queryRunner.query(`
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_consolidation_approvals_one_pending
|
|
ON freight.consolidation_approvals (booking_id)
|
|
WHERE status = 'PENDING' AND deleted_at IS NULL
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_consolidation_approvals_one_pending_partner
|
|
ON freight.consolidation_approvals (partner_booking_id)
|
|
WHERE status = 'PENDING' AND deleted_at IS NULL
|
|
`);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(
|
|
`DROP TABLE IF EXISTS freight.consolidation_approvals`,
|
|
);
|
|
await queryRunner.query(
|
|
`DROP TYPE IF EXISTS freight.consolidation_approvals_status_enum`,
|
|
);
|
|
}
|
|
}
|