[200~feat: add CustomsPaymentsCard and PaymentsTab components for handling customs payments and payment summaries

This commit is contained in:
Marshal
2026-08-20 15:45:42 +00:00
parent 8d7551bb8e
commit 9e1d5ee9f2
58 changed files with 3324 additions and 810 deletions

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Per-schedule wagon yard plan — where THIS departure expects each consist
* wagon to board, independent of where the wagon physically stands today.
*
* `wagons.current_yard_id` is one physical fact shared by every schedule of a
* built train, so a train standing in Mojo could not be sold from Dire for a
* departure next week. The plan is a sparse jsonb map `{ wagonId: yardId }`
* on the schedule: a wagon missing from the map boards from its physical yard.
* Booking capacity, fleet availability and wagon pinning all read the plan;
* dispatch refuses to leave until the plan and the physical yards agree.
*/
export class SchedulePlannedWagonYards3620000000000 implements MigrationInterface {
name = 'SchedulePlannedWagonYards3620000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedules
ADD COLUMN IF NOT EXISTS planned_wagon_yards jsonb
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedules DROP COLUMN IF EXISTS planned_wagon_yards
`);
}
}

View File

@@ -0,0 +1,49 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* The customer now approves a clearance charge before it becomes an invoice:
* GL describes the price, SENDs it, the customer ACCEPTs (invoice issued, charge
* locked) or REJECTs with a note (GL revises and re-sends). Charges that were
* already sent as invoices under the old flow are carried over as ACCEPTED so
* their invoices stay payable.
*/
export class ClearanceChargeCustomerDecision3630000000000
implements MigrationInterface
{
name = 'ClearanceChargeCustomerDecision3630000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "freight"."booking_clearance_charge"
ADD COLUMN IF NOT EXISTS "description" text,
ADD COLUMN IF NOT EXISTS "customer_note" text,
ADD COLUMN IF NOT EXISTS "customer_decided_at" timestamptz,
ADD COLUMN IF NOT EXISTS "customer_decided_by" uuid
`);
await queryRunner.query(`
UPDATE "freight"."booking_clearance_charge"
SET "status" = 'ACCEPTED'
WHERE "status" = 'SENT' AND "invoice_id" IS NOT NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE "freight"."booking_clearance_charge"
SET "status" = 'SENT'
WHERE "status" = 'ACCEPTED'
`);
await queryRunner.query(`
UPDATE "freight"."booking_clearance_charge"
SET "status" = 'BILLED'
WHERE "status" = 'REJECTED'
`);
await queryRunner.query(`
ALTER TABLE "freight"."booking_clearance_charge"
DROP COLUMN IF EXISTS "description",
DROP COLUMN IF EXISTS "customer_note",
DROP COLUMN IF EXISTS "customer_decided_at",
DROP COLUMN IF EXISTS "customer_decided_by"
`);
}
}

View File

@@ -0,0 +1,69 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Ethiopian-side-only customs clearance:
*
* - service_types.includes_ethiopian_customs_only marks a customs service that
* EDR clears on the Ethiopian side only. Same clearance flow; only the fee
* differs — pricing looks up the ETHIOPIAN_CUSTOMS_CLEARANCE rate instead of
* CUSTOMS_CLEARANCE.
* - rates.trigger widens to 30 chars to fit the new trigger value.
* - CK_rates_yard_scope gains ETHIOPIAN_CUSTOMS_CLEARANCE in its yard-carrying
* branch: it is priced per origin → destination leg like customs clearance.
*/
export class EthiopianCustomsClearance3640000000000 implements MigrationInterface {
name = 'EthiopianCustomsClearance3640000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.service_types
ADD COLUMN IF NOT EXISTS includes_ethiopian_customs_only boolean NOT NULL DEFAULT false
`);
await queryRunner.query(
`ALTER TABLE freight.rates ALTER COLUMN trigger TYPE varchar(30)`,
);
await queryRunner.query(
`ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope"`,
);
await queryRunner.query(`
ALTER TABLE freight.rates ADD CONSTRAINT "CK_rates_yard_scope" CHECK (
deleted_at IS NOT NULL OR status = 'SUPERSEDED' OR
CASE
WHEN (trigger = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY'))
OR trigger IN ('CUSTOMS_CLEARANCE', 'ETHIOPIAN_CUSTOMS_CLEARANCE', 'WITH_RETURN', 'FUEL')
THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL
ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL
END
)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope"`,
);
await queryRunner.query(`
ALTER TABLE freight.rates ADD CONSTRAINT "CK_rates_yard_scope" CHECK (
deleted_at IS NOT NULL OR status = 'SUPERSEDED' OR
CASE
WHEN (trigger = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY'))
OR trigger IN ('CUSTOMS_CLEARANCE', 'WITH_RETURN', 'FUEL')
THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL
ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL
END
)
`);
// Rows on the new trigger would not fit varchar(20) — drop them first.
await queryRunner.query(
`DELETE FROM freight.rates WHERE trigger = 'ETHIOPIAN_CUSTOMS_CLEARANCE'`,
);
await queryRunner.query(
`ALTER TABLE freight.rates ALTER COLUMN trigger TYPE varchar(20)`,
);
await queryRunner.query(
`ALTER TABLE freight.service_types DROP COLUMN IF EXISTS includes_ethiopian_customs_only`,
);
}
}