import { BaseEntity } from "@edr/api-common"; import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; import { CargoType } from "../../rule-engine/entities/cargo-type.entity"; /** * The five seeded container templates (import/export split by customs * clearing; 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`) and customs option (`withCustoms`), one template per * combination. Their codes are generated as BULK__(NO_)CUSTOMS. * 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); `_NO_CUSTOMS` is the transport-only paper, where the Client handles * its own declarations. */ export const CONTRACT_TEMPLATE_CODES = [ "IMPORT_BULK_CUSTOMS", "IMPORT_BULK_NO_CUSTOMS", "EXPORT_BULK_CUSTOMS", "EXPORT_BULK_NO_CUSTOMS", "INTERCITY_BULK", "IMPORT_CONTAINER_CUSTOMS", "IMPORT_CONTAINER_NO_CUSTOMS", "EXPORT_CONTAINER_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, ): 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 ? "CUSTOMS" : "NO_CUSTOMS"; return `${direction}_${freight}_${customs}` as ContractTemplateCode; } @Entity({ schema: "freight", name: "contract_templates" }) // Uniqueness lives in partial DB indexes (live rows only): code, and // (cargo_type_id, with_customs) 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: whether this is the with-customs-clearing variant. */ @Column({ name: "with_customs", type: "boolean", nullable: true }) withCustoms?: boolean | null; /** The five seeded container templates — cannot be deleted. */ @Column({ name: "is_system", type: "boolean", default: false }) isSystem!: boolean; }