Files
edr-platform/apps/edr-freight-api/src/modules/contract-templates/entities/contract-template.entity.ts

129 lines
4.8 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 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_<cargo code>_(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;
}