mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 07:51:02 +00:00
176 lines
9.2 KiB
TypeScript
176 lines
9.2 KiB
TypeScript
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-<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-%';`);
|
||
}
|
||
}
|