Files
edr-platform/apps/edr-freight-api/src/migrations/3730000000000-WagonDetachRequests.ts
Marshal b926a3116e feat: enhance train scheduling and contract management features
- Added StationWorkControls to manage loading/unloading phases in TrainScheduleV2DetailPage.
- Implemented API endpoints for recording station work and managing wagon detach requests.
- Updated contract templates to include Ethiopian customs handling options.
- Enhanced shipment forms to collect customs clearing agent details for without-customs bookings.
- Introduced NUMBER_OF_WAGONS as a unit of measure for bulk cargo, allowing customers to specify wagon counts.
- Improved validation for customs clearing agent information in shipment forms.
- Updated various components and services to accommodate new features and ensure data integrity.
2026-08-25 21:44:21 +00:00

75 lines
3.1 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Approval gate for detaching a wagon (or sending it to maintenance) from a
* train whose run is already SCHEDULED.
*
* Before scheduling, the consist is the builder's to edit. After scheduling,
* pulling a wagon changes a departure customers booked against, so it becomes
* a two-person action: one staffer files a request with a reason, another
* staffer (with trains:approve_wagon_detach) approves it — approval executes
* the detach on the spot. Rows are never deleted; decided rows are the audit
* trail of who asked, who decided, and why.
*
* One PENDING row per (train, wagon) at a time — a second request while one is
* undecided is a coordination failure, not a workflow (partial unique index).
*/
export class WagonDetachRequests3730000000000 implements MigrationInterface {
name = 'WagonDetachRequests3730000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DO $$ BEGIN
CREATE TYPE freight.wagon_detach_requests_status_enum
AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
EXCEPTION WHEN duplicate_object THEN NULL; END $$
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.wagon_detach_requests (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
train_id uuid NOT NULL REFERENCES freight.trains (id),
wagon_id uuid NOT NULL REFERENCES freight.wagons (id),
-- Snapshot: the audit trail must still read correctly after the wagon
-- is renumbered or deleted.
wagon_number varchar(50) NOT NULL,
action varchar(20) NOT NULL,
reason varchar(500) NOT NULL,
status freight.wagon_detach_requests_status_enum NOT NULL DEFAULT 'PENDING',
-- Who asked and who decided. Both recorded: the point of the gate is
-- that they are different people.
requested_by uuid,
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_wagon_detach_requests_train
ON freight.wagon_detach_requests (train_id)
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_wagon_detach_requests_train_status
ON freight.wagon_detach_requests (train_id, status)
`);
// The workflow invariant, enforced where it cannot race: at most one
// undecided request per wagon per train.
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS uq_wagon_detach_requests_one_pending
ON freight.wagon_detach_requests (train_id, wagon_id)
WHERE status = 'PENDING' AND deleted_at IS NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_detach_requests`);
await queryRunner.query(`DROP TYPE IF EXISTS freight.wagon_detach_requests_status_enum`);
}
}