mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 00:08:18 +00:00
Merge branch 'dev' into freight/feat/fixes-v1
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Bookings no longer run an approval chain — accepting an intake approves the
|
||||
* booking outright and generates its contract. The approval chain is now a
|
||||
* contract-only concern, so `freight.approval_rules` is read by contracts alone.
|
||||
*
|
||||
* Also widens the role columns: chain steps now reference IAM position-type
|
||||
* keys (`iam.position_types.key`), and real keys run past the old varchar(30)
|
||||
* (e.g. '-marketing-manager-/-general-manager' is 38 chars), which would fail
|
||||
* on insert.
|
||||
*/
|
||||
export class DropBookingApprovalWidenRoles2410000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'DropBookingApprovalWidenRoles2410000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP TABLE IF EXISTS freight.booking_approval_step;`,
|
||||
);
|
||||
|
||||
for (const [table, column] of [
|
||||
['approval_rules', 'required_role'],
|
||||
['approval_rules', 'blocks_role'],
|
||||
['contract_approval_steps', 'required_role'],
|
||||
['contract_approval_steps', 'blocks_role'],
|
||||
] as const) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.${table} ALTER COLUMN ${column} TYPE varchar(64);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op: the booking approval chain is retired, so re-creating the table
|
||||
* would leave dead schema behind. Narrowing the role columns again would
|
||||
* truncate any position-type key already stored.
|
||||
*/
|
||||
public async down(): Promise<void> {
|
||||
// intentionally empty
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Audit trail for contract document edits. The document stays editable through
|
||||
* the whole approval chain (each approver may edit on their turn), so the
|
||||
* contract itself only ever holds the current snapshot — this table records who
|
||||
* changed which article, and when.
|
||||
*/
|
||||
export class CreateContractDocumentRevisions2420000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'CreateContractDocumentRevisions2420000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.contract_document_revisions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
contract_id uuid NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
|
||||
actor_id uuid,
|
||||
actor_role varchar(64),
|
||||
step_id uuid,
|
||||
summary varchar(255),
|
||||
changes jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_contract_document_revisions_contract
|
||||
ON freight.contract_document_revisions (contract_id, created_at DESC);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP TABLE IF EXISTS freight.contract_document_revisions;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Locomotive names must be unique so staff can identify a unit by name alone
|
||||
* (the card view leads with `name`, falling back to `code`). Uniqueness is:
|
||||
*
|
||||
* - case/whitespace-insensitive — "MTL1", "mtl1" and " MTL1 " are one name;
|
||||
* - scoped to live rows — a decommissioned (soft-deleted) locomotive must not
|
||||
* hold its name hostage, matching how the fleet reuses yard codes;
|
||||
* - skipped for blank names — `name` stays optional, and NULL/'' rows are
|
||||
* excluded rather than colliding with each other.
|
||||
*
|
||||
* A partial expression index gives all three; a plain UNIQUE column cannot.
|
||||
*/
|
||||
export class UniqueLocomotiveName2430000000000 implements MigrationInterface {
|
||||
name = 'UniqueLocomotiveName2430000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Pre-existing duplicates would abort CREATE UNIQUE INDEX. Suffix every
|
||||
// copy after the oldest (…-2, …-3) so the index can build; the oldest row
|
||||
// keeps the original name. Deterministic on created_at, then id.
|
||||
await queryRunner.query(`
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
row_number() OVER (
|
||||
PARTITION BY lower(btrim(name))
|
||||
ORDER BY created_at, id
|
||||
) AS rn
|
||||
FROM "freight"."locomotives"
|
||||
WHERE deleted_at IS NULL
|
||||
AND name IS NOT NULL
|
||||
AND btrim(name) <> ''
|
||||
)
|
||||
UPDATE "freight"."locomotives" AS l
|
||||
SET name = btrim(ranked.name) || '-' || ranked.rn
|
||||
FROM ranked
|
||||
WHERE l.id = ranked.id
|
||||
AND ranked.rn > 1
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_locomotives_name_active"
|
||||
ON "freight"."locomotives" (lower(btrim("name")))
|
||||
WHERE "deleted_at" IS NULL
|
||||
AND "name" IS NOT NULL
|
||||
AND btrim("name") <> ''
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "freight"."UQ_locomotives_name_active"`,
|
||||
);
|
||||
// The de-duplicating renames are not reversed: the original names are no
|
||||
// longer recoverable, and restoring them would re-introduce the conflict.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user