import { MigrationInterface, QueryRunner } from 'typeorm'; /** * Data backfill for the contract–booking separation (docs/new-doc.md §17). * * For every legacy `booking_type = 'GENERAL_CONTRACT'` booking we synthesise a * `freight.contracts` row from its contract-phase columns, copy its routes * (contract_route_lines → contract_routes, dropping quantity), and point the * contract + every child shipment booking (linked via booking_orders) at it. * * Per §19 item 1, historical ONE_TIME bookings that went through the full * contract flow get a contract parent inserted and `contract_id` set on the same * booking row (no row split). * * Idempotent: skips bookings that already have `contract_id` set, and matches a * synthesised contract by a deterministic `CTR-` reference. */ export class BackfillContractsFromBookings1823000000000 implements MigrationInterface { name = 'BackfillContractsFromBookings1823000000000'; public async up(queryRunner: QueryRunner): Promise { // 1. One contract per GENERAL_CONTRACT booking, carrying the contract-phase // columns. Reference is derived from the source booking id so re-runs are // idempotent (ON CONFLICT DO NOTHING on the unique reference). await queryRunner.query(` INSERT INTO freight.contracts ( reference, company_id, company_profile_id, is_government, government_institution, contract_kind, trade_direction, freight_type, service_type_id, payment_currency, customs_clearing_enabled, customs_clearing_agent, equipment_return, first_mile_pickup_address, first_mile_pickup_lat, first_mile_pickup_lng, last_mile_delivery_address, last_mile_delivery_lat, last_mile_delivery_lng, is_hazardous, is_reefer, estimated_shipment_date, contract_validity_days, contract_valid_from, contract_valid_until, expires_at, status, clearance_status, clearance_cycle_number, pricing_breakdown, contract_type, contract_template_key, contract_generated_at, contract_summary, version_number, approved_by_staff_id, approved_by_staff_at, signed_by_director_id, signed_by_director_at, signed_by_ceo_id, signed_by_ceo_at, customer_signed_at, fully_executed_at, created_at, updated_at ) SELECT 'CTR-' || b.id::text, b.company_id, b.company_profile_id, b.is_government, b.government_institution, 'GENERAL', b.trade_direction, b.freight_type, b.service_type_id, b.payment_currency, b.customs_clearing_enabled, b.customs_clearing_agent, b.equipment_return, b.first_mile_pickup_address, b.first_mile_pickup_lat, b.first_mile_pickup_lng, b.last_mile_delivery_address, b.last_mile_delivery_lat, b.last_mile_delivery_lng, b.is_hazardous, b.is_reefer, b.estimated_shipment_date, b.contract_validity_days, b.contract_valid_from, b.contract_valid_until, b.expires_at, CASE WHEN b.status IN ('CONTRACT_ACTIVE') THEN 'CONTRACT_ACTIVE' WHEN b.status IN ('CONTRACT_CLOSED') THEN 'CONTRACT_CLOSED' WHEN b.status IN ('EXPIRED') THEN 'EXPIRED' WHEN b.status IN ('CANCELLED') THEN 'CANCELLED' WHEN b.status IN ('REJECTED') THEN 'REJECTED' ELSE 'CONTRACT_ACTIVE' END, CASE WHEN b.customs_clearing_enabled THEN 'NOT_APPLICABLE' ELSE 'NOT_APPLICABLE' END, 0, b.pricing_breakdown, b.contract_type, b.contract_template_key, b.contract_generated_at, b.contract_summary, COALESCE(b.version_number, 1), b.approved_by_staff_id, b.approved_by_staff_at, b.signed_by_director_id, b.signed_by_director_at, b.signed_by_ceo_id, b.signed_by_ceo_at, b.customer_signed_at, b.fully_executed_at, b.created_at, b.updated_at FROM freight.bookings b WHERE b.booking_type = 'GENERAL_CONTRACT' ON CONFLICT (reference) DO NOTHING; `); // 2. Copy each general contract's route lines into contract_routes (no qty). await queryRunner.query(` INSERT INTO freight.contract_routes (contract_id, origin_yard_id, destination_yard_id, km, sort_order, created_at, updated_at) SELECT c.id, crl.origin_yard_id, crl.destination_yard_id, crl.km, 0, now(), now() FROM freight.contract_route_lines crl JOIN freight.contracts c ON c.reference = 'CTR-' || crl.contract_booking_id::text ON CONFLICT (contract_id, origin_yard_id, destination_yard_id) DO NOTHING; `); // 3. Point the general-contract booking itself at its new contract, and stamp // the denormalized contract_kind for the active-booking index. await queryRunner.query(` UPDATE freight.bookings b SET contract_id = c.id, contract_kind = 'GENERAL', created_by_role = 'CUSTOMER' FROM freight.contracts c WHERE c.reference = 'CTR-' || b.id::text AND b.booking_type = 'GENERAL_CONTRACT' AND b.contract_id IS NULL; `); // 4. Point each child shipment booking (spawned via booking_orders) at the // same contract as its parent general contract. await queryRunner.query(` UPDATE freight.bookings child SET contract_id = c.id, contract_kind = 'GENERAL', created_by_role = 'CUSTOMER' FROM freight.booking_orders bo JOIN freight.contracts c ON c.reference = 'CTR-' || bo.contract_booking_id::text WHERE child.id = bo.booking_id AND child.contract_id IS NULL; `); // 5. Historical ONE_TIME bookings that completed the contract flow: synthesise // a contract parent and point the same booking row at it (no row split). await queryRunner.query(` INSERT INTO freight.contracts ( reference, company_id, company_profile_id, is_government, government_institution, contract_kind, trade_direction, freight_type, service_type_id, payment_currency, customs_clearing_enabled, customs_clearing_agent, equipment_return, first_mile_pickup_address, first_mile_pickup_lat, first_mile_pickup_lng, last_mile_delivery_address, last_mile_delivery_lat, last_mile_delivery_lng, is_hazardous, is_reefer, estimated_shipment_date, contract_validity_days, contract_valid_from, contract_valid_until, status, clearance_status, clearance_cycle_number, pricing_breakdown, contract_type, contract_template_key, contract_generated_at, contract_summary, version_number, approved_by_staff_id, approved_by_staff_at, signed_by_director_id, signed_by_director_at, signed_by_ceo_id, signed_by_ceo_at, customer_signed_at, fully_executed_at, created_at, updated_at ) SELECT 'CTR-' || b.id::text, b.company_id, b.company_profile_id, b.is_government, b.government_institution, 'ONE_TIME', b.trade_direction, b.freight_type, b.service_type_id, b.payment_currency, b.customs_clearing_enabled, b.customs_clearing_agent, b.equipment_return, b.first_mile_pickup_address, b.first_mile_pickup_lat, b.first_mile_pickup_lng, b.last_mile_delivery_address, b.last_mile_delivery_lat, b.last_mile_delivery_lng, b.is_hazardous, b.is_reefer, b.estimated_shipment_date, b.contract_validity_days, b.contract_valid_from, b.contract_valid_until, 'FULLY_EXECUTED', 'NOT_APPLICABLE', 0, b.pricing_breakdown, b.contract_type, b.contract_template_key, b.contract_generated_at, b.contract_summary, COALESCE(b.version_number, 1), b.approved_by_staff_id, b.approved_by_staff_at, b.signed_by_director_id, b.signed_by_director_at, b.signed_by_ceo_id, b.signed_by_ceo_at, b.customer_signed_at, b.fully_executed_at, b.created_at, b.updated_at FROM freight.bookings b WHERE COALESCE(b.booking_type, 'ONE_TIME') = 'ONE_TIME' AND b.contract_id IS NULL AND b.contract_generated_at IS NOT NULL ON CONFLICT (reference) DO NOTHING; `); await queryRunner.query(` UPDATE freight.bookings b SET contract_id = c.id, contract_kind = 'ONE_TIME', created_by_role = 'CUSTOMER' FROM freight.contracts c WHERE c.reference = 'CTR-' || b.id::text AND COALESCE(b.booking_type, 'ONE_TIME') = 'ONE_TIME' AND b.contract_id IS NULL; `); // 6. Build a single route per ONE_TIME contract from the booking's own // origin/destination (general contracts already got their routes in step 2). await queryRunner.query(` INSERT INTO freight.contract_routes (contract_id, origin_yard_id, destination_yard_id, sort_order, created_at, updated_at) SELECT c.id, b.origin_yard_id, b.destination_yard_id, 0, now(), now() FROM freight.bookings b JOIN freight.contracts c ON c.id = b.contract_id AND c.contract_kind = 'ONE_TIME' WHERE b.origin_yard_id IS NOT NULL AND b.destination_yard_id IS NOT NULL ON CONFLICT (contract_id, origin_yard_id, destination_yard_id) DO NOTHING; `); } public async down(queryRunner: QueryRunner): Promise { // Unlink bookings and drop the synthesised contracts (and their cascaded routes). await queryRunner.query(` UPDATE freight.bookings SET contract_id = NULL, contract_route_id = NULL WHERE contract_id IN (SELECT id FROM freight.contracts WHERE reference LIKE 'CTR-%'); `); await queryRunner.query(`DELETE FROM freight.contracts WHERE reference LIKE 'CTR-%';`); } }