feat(companies): add Transit Agent service linked to transit-agent roster; forwarder/agent onboarding step, assigned-bookings tab, booking assignment + notify, drop agent validity window

This commit is contained in:
marshal
2026-09-06 21:41:29 +00:00
parent 6d4a919f39
commit 1eb9f10354
67 changed files with 2528 additions and 302 deletions

View File

@@ -0,0 +1,75 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Links a freight-forwarder company to the transit agent it is registered as.
*
* An Ethiopian transit agent and a freight forwarder are the same business seen
* from two sides: the roster GL uses to assign an officer, and the customer
* that signs contracts on other companies' behalf. Until now nothing tied the
* two rows together, so a forwarder could onboard under any name and staff had
* no way to tell which roster entry it was.
*
* Two changes:
* - `transit_agents.country` — the roster was Djibouti-only, so every existing
* row defaults to `DJ`. Only `ET` agents are offered to a forwarder onboarding.
* - `companies.transit_agent_id` — nullable: importers and exporters have no
* agent, and pre-existing forwarders stay unlinked until they are edited.
*/
export class CompanyTransitAgentLink3930000000000 implements MigrationInterface {
name = 'CompanyTransitAgentLink3930000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.transit_agents
ADD COLUMN IF NOT EXISTS country varchar(2) NOT NULL DEFAULT 'DJ'
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_transit_agents_country"
ON freight.transit_agents (country)
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS transit_agent_id uuid
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_companies_transit_agent_id"
ON freight.companies (transit_agent_id)
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'FK_companies_transit_agent'
) THEN
ALTER TABLE freight.companies
ADD CONSTRAINT "FK_companies_transit_agent"
FOREIGN KEY (transit_agent_id)
REFERENCES freight.transit_agents (id)
ON DELETE SET NULL;
END IF;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
DROP CONSTRAINT IF EXISTS "FK_companies_transit_agent"
`);
await queryRunner.query(`
DROP INDEX IF EXISTS freight."IDX_companies_transit_agent_id"
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS transit_agent_id
`);
await queryRunner.query(`
DROP INDEX IF EXISTS freight."IDX_transit_agents_country"
`);
await queryRunner.query(`
ALTER TABLE freight.transit_agents
DROP COLUMN IF EXISTS country
`);
}
}

View File

@@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Drops the transit-agent validity window.
*
* `valid_from` / `valid_to` gated which officers GL could assign, on top of the
* `is_active` switch. In practice the dates were never maintained — an agent
* whose window lapsed was simply suspended — and with Ethiopian agents now
* doubling as the roster a freight forwarder registers itself against, a
* date range that silently hides a live business is worse than no range.
* `is_active` is the single switch from here on.
*
* `down()` re-adds the columns as nullable: the dates themselves are gone.
*/
export class DropTransitAgentValidity3940000000000 implements MigrationInterface {
name = 'DropTransitAgentValidity3940000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.transit_agents
DROP COLUMN IF EXISTS valid_from,
DROP COLUMN IF EXISTS valid_to
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.transit_agents
ADD COLUMN IF NOT EXISTS valid_from date,
ADD COLUMN IF NOT EXISTS valid_to date
`);
}
}

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Reference sequence for the new `transit_agent` operational profile.
*
* `company_profiles.type` is a plain varchar, so the role itself needs no DDL;
* what it needs is its own reference series (`TA-A00001`, …), minted on
* approval exactly like IM/EX/FF. Mirrors the baseline's sequences.
*/
export class TransitAgentProfileSequence3950000000000 implements MigrationInterface {
name = "TransitAgentProfileSequence3950000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_ta
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP SEQUENCE IF EXISTS freight.seq_company_profile_ta`,
);
}
}