Files
edr-platform/apps/edr-freight-api/src/modules/contract-templates/entities/contract-template.entity.ts
2026-08-04 09:48:49 +00:00

104 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BaseEntity } from "@edr/api-common";
import { Column, Entity, Index } from "typeorm";
/**
* The ten canonical contract document templates. Import and export split by
* customs clearing (× freight type = 8); intercity does not, because it is a
* purely domestic Ethiopian movement that crosses no border and therefore has
* no customs leg at all (× freight type = 2).
*
* 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" })
@Index(["code"], { unique: true })
export class ContractTemplate extends BaseEntity {
@Column({ name: "code", type: "varchar", length: 40, unique: true })
code!: ContractTemplateCode;
@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;
}