Files
edr-platform/apps/edr-freight-api/src/migrations/1823000000000-BackfillContractsFromBookings.ts
2026-06-26 23:24:48 +00:00

176 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Data backfill for the contractbooking 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-<bookingId>` reference.
*/
export class BackfillContractsFromBookings1823000000000
implements MigrationInterface
{
name = 'BackfillContractsFromBookings1823000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// 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<void> {
// 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-%';`);
}
}