mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- Added StationWorkControls to manage loading/unloading phases in TrainScheduleV2DetailPage. - Implemented API endpoints for recording station work and managing wagon detach requests. - Updated contract templates to include Ethiopian customs handling options. - Enhanced shipment forms to collect customs clearing agent details for without-customs bookings. - Introduced NUMBER_OF_WAGONS as a unit of measure for bulk cargo, allowing customers to specify wagon counts. - Improved validation for customs clearing agent information in shipment forms. - Updated various components and services to accommodate new features and ensure data integrity.
196 lines
7.2 KiB
TypeScript
196 lines
7.2 KiB
TypeScript
import { BaseEntity } from "@edr/api-common";
|
|
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
|
|
|
import { CargoType } from "../../rule-engine/entities/cargo-type.entity";
|
|
|
|
/**
|
|
* The seeded container templates (import/export split by customs-clearing
|
|
* option — full, Ethiopian-only, none; intercity is domestic, crosses no
|
|
* border, so it has a single template). These are system rows: always
|
|
* present, never deletable.
|
|
*
|
|
* Bulk templates are NOT seeded — staff create them per bulk cargo type
|
|
* (`cargoTypeId`), trade direction (`tradeDirection`) and customs option
|
|
* (`withCustoms`), one template per combination. Their codes are generated by
|
|
* `bulkTemplateCode` below. The retired direction-keyed bulk codes remain
|
|
* listed so old frozen document snapshots still label correctly.
|
|
*
|
|
* Contracts store DOMESTIC for intercity movements; the template layer labels
|
|
* those INTERCITY to match the commercial vocabulary used on the printed
|
|
* documents.
|
|
*
|
|
* The `_CUSTOMS` variant is issued when the contract has customs clearing
|
|
* enabled (the Service Provider clears in Djibouti/Ethiopia on the Client's
|
|
* behalf); `_ETHIOPIAN_CUSTOMS` when the service type is Ethiopian-customs-only
|
|
* (the Service Provider clears the Ethiopian side only, Djibouti stays with the
|
|
* Client); `_NO_CUSTOMS` is the transport-only paper, where the Client handles
|
|
* its own declarations.
|
|
*/
|
|
export const CONTRACT_TEMPLATE_CODES = [
|
|
"IMPORT_BULK_CUSTOMS",
|
|
"IMPORT_BULK_ETHIOPIAN_CUSTOMS",
|
|
"IMPORT_BULK_NO_CUSTOMS",
|
|
"EXPORT_BULK_CUSTOMS",
|
|
"EXPORT_BULK_ETHIOPIAN_CUSTOMS",
|
|
"EXPORT_BULK_NO_CUSTOMS",
|
|
"INTERCITY_BULK",
|
|
"IMPORT_CONTAINER_CUSTOMS",
|
|
"IMPORT_CONTAINER_ETHIOPIAN_CUSTOMS",
|
|
"IMPORT_CONTAINER_NO_CUSTOMS",
|
|
"EXPORT_CONTAINER_CUSTOMS",
|
|
"EXPORT_CONTAINER_ETHIOPIAN_CUSTOMS",
|
|
"EXPORT_CONTAINER_NO_CUSTOMS",
|
|
"INTERCITY_CONTAINER",
|
|
] as const;
|
|
|
|
export type ContractTemplateCode = (typeof CONTRACT_TEMPLATE_CODES)[number];
|
|
|
|
/**
|
|
* One dynamic article on a contract template. `body` is plain multiline text:
|
|
* each non-empty line renders as a numbered clause; lines prefixed with "- "
|
|
* render as bullet points nested under the preceding clause. A single-line
|
|
* body renders as an unnumbered paragraph. Handlebars placeholders (e.g.
|
|
* {{client.companyName}}, {{contractDate}}, {{contractYear}}, {{reference}})
|
|
* are interpolated against the contract view model at render time.
|
|
*/
|
|
export interface ContractTemplateArticle {
|
|
id: string;
|
|
title: string;
|
|
body: string;
|
|
order: number;
|
|
}
|
|
|
|
/**
|
|
* Map a contract's stored direction/freight/customs triple onto a template
|
|
* code. `customsClearingEnabled` is treated as false when absent so an older
|
|
* contract row with a null flag still resolves to a real template rather than
|
|
* falling through to the generic layout.
|
|
*
|
|
* Intercity is domestic and has no customs leg, so it resolves to a single
|
|
* unsuffixed code regardless of the flag.
|
|
*/
|
|
export function contractTemplateCodeFor(
|
|
tradeDirection?: string | null,
|
|
freightType?: string | null,
|
|
customsClearingEnabled?: boolean | null,
|
|
ethiopianCustomsOnly?: boolean | null,
|
|
): ContractTemplateCode {
|
|
const direction =
|
|
tradeDirection === "IMPORT"
|
|
? "IMPORT"
|
|
: tradeDirection === "EXPORT"
|
|
? "EXPORT"
|
|
: "INTERCITY";
|
|
const freight =
|
|
(freightType ?? "").toUpperCase().includes("BULK") ? "BULK" : "CONTAINER";
|
|
if (direction === "INTERCITY") {
|
|
return `INTERCITY_${freight}` as ContractTemplateCode;
|
|
}
|
|
const customs = customsClearingEnabled
|
|
? ethiopianCustomsOnly
|
|
? "ETHIOPIAN_CUSTOMS"
|
|
: "CUSTOMS"
|
|
: "NO_CUSTOMS";
|
|
return `${direction}_${freight}_${customs}` as ContractTemplateCode;
|
|
}
|
|
|
|
/** The three directions a bulk template can be written for. */
|
|
export const BULK_TEMPLATE_DIRECTIONS = ["IMPORT", "EXPORT", "INTERCITY"] as const;
|
|
|
|
export type BulkTemplateDirection = (typeof BULK_TEMPLATE_DIRECTIONS)[number];
|
|
|
|
/**
|
|
* Contracts store DOMESTIC for intercity movements; templates use INTERCITY.
|
|
* Anything that is not an explicit IMPORT/EXPORT is domestic, matching
|
|
* `contractTemplateCodeFor`.
|
|
*/
|
|
export function bulkTemplateDirectionFor(
|
|
tradeDirection?: string | null,
|
|
): BulkTemplateDirection {
|
|
const value = (tradeDirection ?? "").toUpperCase();
|
|
return value === "IMPORT" || value === "EXPORT" ? value : "INTERCITY";
|
|
}
|
|
|
|
/**
|
|
* Generated code for a staff-created bulk template. Intercity gets no customs
|
|
* suffix — it crosses no border, so the variant does not exist.
|
|
*/
|
|
export function bulkTemplateCode(
|
|
cargoCode: string,
|
|
direction: BulkTemplateDirection,
|
|
withCustoms: boolean | null,
|
|
ethiopianCustomsOnly = false,
|
|
): string {
|
|
const suffix =
|
|
direction === "INTERCITY"
|
|
? ""
|
|
: withCustoms
|
|
? ethiopianCustomsOnly
|
|
? "_ETHIOPIAN_CUSTOMS"
|
|
: "_CUSTOMS"
|
|
: "_NO_CUSTOMS";
|
|
return `BULK_${direction}_${cargoCode}${suffix}`.toUpperCase();
|
|
}
|
|
|
|
@Entity({ schema: "freight", name: "contract_templates" })
|
|
// Uniqueness lives in partial DB indexes (live rows only): code, and
|
|
// (cargo_type_id, trade_direction, coalesce(with_customs,false)) for
|
|
// staff-created bulk templates.
|
|
@Index(["code"])
|
|
export class ContractTemplate extends BaseEntity {
|
|
@Column({ name: "code", type: "varchar", length: 80 })
|
|
code!: string;
|
|
|
|
@Column({ name: "name", type: "varchar", length: 200 })
|
|
name!: string;
|
|
|
|
@Column({ name: "description", type: "text", nullable: true })
|
|
description?: string | null;
|
|
|
|
/** Cover-page service line, e.g. "Steel Billet Transportation and Customs Clearance Services". */
|
|
@Column({ name: "document_title", type: "varchar", length: 300 })
|
|
documentTitle!: string;
|
|
|
|
/** WHEREAS recitals rendered between the parties block and the articles. */
|
|
@Column({ name: "whereas_clauses", type: "jsonb", default: () => "'[]'" })
|
|
whereasClauses!: string[];
|
|
|
|
@Column({ name: "articles", type: "jsonb", default: () => "'[]'" })
|
|
articles!: ContractTemplateArticle[];
|
|
|
|
@Column({ name: "is_active", type: "boolean", default: true })
|
|
isActive!: boolean;
|
|
|
|
/** Bulk templates only: the cargo type this template is written for. */
|
|
@Column({ name: "cargo_type_id", type: "uuid", nullable: true })
|
|
cargoTypeId?: string | null;
|
|
|
|
@ManyToOne(() => CargoType, { nullable: true })
|
|
@JoinColumn({ name: "cargo_type_id" })
|
|
cargoType?: CargoType | null;
|
|
|
|
/** Bulk templates only: IMPORT, EXPORT or INTERCITY. */
|
|
@Column({ name: "trade_direction", type: "varchar", length: 20, nullable: true })
|
|
tradeDirection?: BulkTemplateDirection | null;
|
|
|
|
/**
|
|
* Bulk templates only: whether this is the with-customs-clearing variant.
|
|
* Always null for INTERCITY templates — domestic movements have no customs
|
|
* leg, so neither variant applies.
|
|
*/
|
|
@Column({ name: "with_customs", type: "boolean", nullable: true })
|
|
withCustoms?: boolean | null;
|
|
|
|
/**
|
|
* Bulk templates only: the with-customs variant restricted to Ethiopian-side
|
|
* clearing (Djibouti stays with the Client). Only meaningful when
|
|
* `withCustoms` is true; null/false otherwise.
|
|
*/
|
|
@Column({ name: "ethiopian_customs_only", type: "boolean", nullable: true })
|
|
ethiopianCustomsOnly?: boolean | null;
|
|
|
|
/** The seeded container templates — cannot be deleted. */
|
|
@Column({ name: "is_system", type: "boolean", default: false })
|
|
isSystem!: boolean;
|
|
}
|