import { MigrationInterface, QueryRunner } from 'typeorm'; /** * Customer-requested validity extension of an EXPIRED contract. * * Flow: the customer asks from the portal (`extension_requested_at` is stamped, * the reason lands in contract_review_notes as EXTENSION_REQUESTED), then staff * add days on the backoffice detail page and the contract returns to the status * it held before it lapsed. Both expiry paths (nightly sweep + lazy flip on * read) now stash that status in `status_before_expiry`, mirroring * `status_before_suspension`; rows expired before this column existed fall * back to the kind's resting status on extension. * * Also seeds `edr_freight_app:contracts:extend`. `FreightPositionsSeeder` * resolves every registry key against `iam.permissions` at boot and throws on * a missing row, so the catalog row must exist wherever the registry ships. * The grant is copied from whoever already holds `contracts:suspend` — the * registry places both keys on the same desk (marketing), and the position * seeder only re-syncs presets when SEED_EDR_ORG is set. */ export class ContractExtensionRequest3920000000000 implements MigrationInterface { name = 'ContractExtensionRequest3920000000000'; private static readonly KEY = 'edr_freight_app:contracts:extend'; private static readonly ID = 'a3000001-0001-4000-8000-00000000001d'; private static readonly SIBLING_KEY = 'edr_freight_app:contracts:suspend'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE freight.contracts ADD COLUMN IF NOT EXISTS extension_requested_at timestamptz NULL `); await queryRunner.query(` ALTER TABLE freight.contracts ADD COLUMN IF NOT EXISTS status_before_expiry varchar(40) NULL `); await queryRunner.query( `INSERT INTO iam.permissions (id, key, name, application_id) SELECT $2::uuid, $1::varchar, '{"am": "Extend an expired contract", "en": "Extend an expired contract"}'::jsonb, a.id FROM iam.application a WHERE a.key = 'edr_freight_app' AND NOT EXISTS (SELECT 1 FROM iam.permissions p WHERE p.key = $1::varchar)`, [ContractExtensionRequest3920000000000.KEY, ContractExtensionRequest3920000000000.ID], ); // Grant wherever suspend is already granted (positions and roles alike). await queryRunner.query( `INSERT INTO iam.position_permissions (position_id, permission_id) SELECT pp.position_id, np.id FROM iam.position_permissions pp JOIN iam.permissions sp ON sp.id = pp.permission_id AND sp.key = $2::varchar JOIN iam.permissions np ON np.key = $1::varchar WHERE NOT EXISTS ( SELECT 1 FROM iam.position_permissions x WHERE x.position_id = pp.position_id AND x.permission_id = np.id )`, [ContractExtensionRequest3920000000000.KEY, ContractExtensionRequest3920000000000.SIBLING_KEY], ); await queryRunner.query( `INSERT INTO iam.role_permissions (role_id, permission_id) SELECT rp.role_id, np.id FROM iam.role_permissions rp JOIN iam.permissions sp ON sp.id = rp.permission_id AND sp.key = $2::varchar JOIN iam.permissions np ON np.key = $1::varchar WHERE NOT EXISTS ( SELECT 1 FROM iam.role_permissions x WHERE x.role_id = rp.role_id AND x.permission_id = np.id )`, [ContractExtensionRequest3920000000000.KEY, ContractExtensionRequest3920000000000.SIBLING_KEY], ); } /** Grants go first, or the delete trips the permission foreign keys. */ public async down(queryRunner: QueryRunner): Promise { await queryRunner.query( `DELETE FROM iam.position_permissions WHERE permission_id IN (SELECT id FROM iam.permissions WHERE key = $1)`, [ContractExtensionRequest3920000000000.KEY], ); await queryRunner.query( `DELETE FROM iam.role_permissions WHERE permission_id IN (SELECT id FROM iam.permissions WHERE key = $1)`, [ContractExtensionRequest3920000000000.KEY], ); await queryRunner.query(`DELETE FROM iam.permissions WHERE key = $1`, [ ContractExtensionRequest3920000000000.KEY, ]); await queryRunner.query( `ALTER TABLE freight.contracts DROP COLUMN IF EXISTS status_before_expiry`, ); await queryRunner.query( `ALTER TABLE freight.contracts DROP COLUMN IF EXISTS extension_requested_at`, ); } }