Files
edr-platform/apps/edr-freight-api/src/migrations/3450000000000-BookingShippingLine.ts
marshalyordanos 9fff469ffa feat: implement shipping line bookings management
- Add ShippingLineBookingsPage for listing and managing shipping line bookings.
- Create ShippingLineDocumentsModal for document uploads related to bookings.
- Introduce ShippingLineInitiateModal for initiating new shipping line bookings.
- Implement booking document state management with booking-doc-state utility.
- Add shipping line bookings service for API interactions.
- Update index to export new components and services.
- Enhance types for freight to include shipping line credits.
2026-08-13 15:54:40 +03:00

118 lines
4.8 KiB
TypeScript

import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Shipping lines book rail capacity directly, without a contract.
*
* A booking has always been owned by `company_id` (a customer `companies` row),
* but a shipping line is a `shipping_line_companies` row and deliberately NOT a
* company — it carries no TIN, licence or operational profiles. So it gets its
* own nullable owner column rather than a synthetic company row.
*
* Exactly one of the two is set: `company_id` for a customer booking,
* `shipping_line_company_id` for a shipping-line one. Existing rows keep
* `company_id` and a NULL `shipping_line_company_id`, so nothing needs
* backfilling and every customer query filtering on `company_id` behaves
* exactly as before. Government bookings already bill to a seeded government
* company, so they satisfy the CHECK unchanged.
*
* NOTE: not to be confused with the existing `bookings.shipping_line_id`, which
* is cargo metadata naming the carrier line that moves the goods
* (`freight.shipping_lines`, reference data). This column points at
* `freight.shipping_line_companies` — the portal account — and is unrelated.
*/
export class BookingShippingLine3450000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS shipping_line_company_id uuid
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_bookings_shipping_line_company_id
ON freight.bookings (shipping_line_company_id)
`);
// `company_id` / `company_profile_id` are NOT NULL and point at the customer
// tables, so a shipping-line booking could not be inserted at all. Relax
// them to nullable; their foreign keys are left in place and keep validating
// every non-NULL value, so a customer booking is constrained exactly as
// before. The CHECK below is what now guarantees an owner is present.
await queryRunner.query(`
ALTER TABLE freight.bookings ALTER COLUMN company_id DROP NOT NULL
`);
await queryRunner.query(`
ALTER TABLE freight.bookings ALTER COLUMN company_profile_id DROP NOT NULL
`);
// Route and service are inherited from the contract on a customer booking.
// A shipping line initiates before any of that is known — the bare booking
// exists only to hang documents off — so these are relaxed too and filled
// in when the booking is completed. Existing rows all have values, and the
// customer paths still always set them.
await queryRunner.query(`
ALTER TABLE freight.bookings ALTER COLUMN origin_yard_id DROP NOT NULL
`);
await queryRunner.query(`
ALTER TABLE freight.bookings ALTER COLUMN destination_yard_id DROP NOT NULL
`);
await queryRunner.query(`
ALTER TABLE freight.bookings ALTER COLUMN service_type_id DROP NOT NULL
`);
await queryRunner.query(`
ALTER TABLE freight.bookings ALTER COLUMN freight_type DROP NOT NULL
`);
// No FK: kept consistent with how the column is populated at the service
// layer, and avoids a lock on shipping_line_companies during deploy.
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS chk_bookings_single_owner
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD CONSTRAINT chk_bookings_single_owner
CHECK (
(company_id IS NOT NULL AND shipping_line_company_id IS NULL)
OR (company_id IS NULL AND shipping_line_company_id IS NOT NULL)
)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS chk_bookings_single_owner
`);
// Only reinstate NOT NULL if no shipping-line booking exists; those rows
// have a NULL company_id by design and would make the ALTER fail. Leaving
// the columns nullable is the safe outcome — the constraint is additive.
const [{ count }] = (await queryRunner.query(`
SELECT COUNT(*)::int AS count FROM freight.bookings
WHERE shipping_line_company_id IS NOT NULL
`)) as Array<{ count: number }>;
if (count === 0) {
for (const column of [
"company_id",
"company_profile_id",
"origin_yard_id",
"destination_yard_id",
"service_type_id",
"freight_type",
]) {
await queryRunner.query(`
ALTER TABLE freight.bookings ALTER COLUMN ${column} SET NOT NULL
`);
}
}
await queryRunner.query(`
DROP INDEX IF EXISTS freight.idx_bookings_shipping_line_company_id
`);
await queryRunner.query(`
ALTER TABLE freight.bookings DROP COLUMN IF EXISTS shipping_line_company_id
`);
}
}