mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
dynamic contract templates and companyId on bookings
This commit is contained in:
224
apps/edr-freight-api/src/contracts/contract-clause-packs.ts
Normal file
224
apps/edr-freight-api/src/contracts/contract-clause-packs.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import type {
|
||||
Article1Clause,
|
||||
ContractClausePack,
|
||||
ContractDirection,
|
||||
ContractFreight,
|
||||
ContractServiceScope,
|
||||
} from './contract-template.types';
|
||||
|
||||
const STANDARD_CONTRACT_DOCUMENTS = [
|
||||
'Amendments (if any)',
|
||||
'This Contract Agreement',
|
||||
'Final Minutes of Negotiation (if any)',
|
||||
];
|
||||
|
||||
const PAYMENT_OBLIGATION =
|
||||
'Pay 100% transportation fees in advance per train set in accordance with Article 5.';
|
||||
|
||||
const HAZARDOUS_OBLIGATION =
|
||||
'Notify EDR 48 hours in advance for hazardous or valuable cargo.';
|
||||
|
||||
function clonePack(pack: ContractClausePack): ContractClausePack {
|
||||
return {
|
||||
article1: {
|
||||
objective: pack.article1.objective,
|
||||
scope: [...pack.article1.scope],
|
||||
},
|
||||
clientObligations: [...pack.clientObligations],
|
||||
providerObligations: [...pack.providerObligations],
|
||||
contractDocuments: [...pack.contractDocuments],
|
||||
};
|
||||
}
|
||||
|
||||
function applyForwardingOverlay(
|
||||
pack: ContractClausePack,
|
||||
service: ContractServiceScope,
|
||||
): ContractClausePack {
|
||||
if (service !== 'FORWARDING') return pack;
|
||||
|
||||
const next = clonePack(pack);
|
||||
next.article1.scope.push(
|
||||
'First-mile and/or last-mile coordination, documentation, and handover with road or port partners where included in the agreed service scope.',
|
||||
);
|
||||
next.providerObligations.push(
|
||||
'Coordinate first-mile and last-mile logistics with designated partners and keep the Client informed of handover milestones.',
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildImportContainerPack(): ContractClausePack {
|
||||
return {
|
||||
article1: {
|
||||
objective:
|
||||
'To provide railway transportation for 40ft and/or 20ft full containers from SGTD to Dire Dawa, Modjo dry port and/or Galaan Multipurpose port (GMP), and empty container return from those terminals to SGTD.',
|
||||
scope: [
|
||||
'Railway transport service on the agreed import corridor.',
|
||||
'Cargo handling at Galaan Multipurpose port (GMP) where applicable.',
|
||||
],
|
||||
},
|
||||
clientObligations: [
|
||||
'Provide shipment instructions to EDR for container movements on the agreed corridor.',
|
||||
'Meet minimum container supply per terminal (Modjo, Dire Dawa, GMP) as per EDR operational rules.',
|
||||
'Submit required documents to Djibouti Nagad station at least 24 hours before loading.',
|
||||
PAYMENT_OBLIGATION,
|
||||
HAZARDOUS_OBLIGATION,
|
||||
],
|
||||
providerObligations: [
|
||||
'Assign voyage per operational schedule and notify train schedule 48 hours in advance.',
|
||||
'Provide safe transportation and deliver within agreed timelines when documents are complete.',
|
||||
'Return empty containers from Dire Dawa, Modjo and GMP to SGTD within seven (7) calendar days of receipt.',
|
||||
'Maintain cargo liability insurance per wagon.',
|
||||
],
|
||||
contractDocuments: [...STANDARD_CONTRACT_DOCUMENTS],
|
||||
};
|
||||
}
|
||||
|
||||
function buildImportBulkPack(): ContractClausePack {
|
||||
return {
|
||||
article1: {
|
||||
objective:
|
||||
'To provide railway transportation for bulk cargo from SGTD railway freight station at Djibouti to designated Ethiopian rail terminals on the import corridor.',
|
||||
scope: [
|
||||
'Railway bulk transport service on the agreed import corridor.',
|
||||
'Loading and unloading coordination at designated terminals per EDR operational rules.',
|
||||
],
|
||||
},
|
||||
clientObligations: [
|
||||
'Provide accurate commodity description, weight, and shipment instructions for each train movement.',
|
||||
'Ensure cargo is prepared and available at origin per the agreed loading window.',
|
||||
'Submit required customs and operational documents at least 24 hours before loading where applicable.',
|
||||
PAYMENT_OBLIGATION,
|
||||
HAZARDOUS_OBLIGATION,
|
||||
],
|
||||
providerObligations: [
|
||||
'Assign train capacity per operational schedule and notify departure timing 48 hours in advance where practicable.',
|
||||
'Provide safe bulk transportation and deliver within agreed timelines when documents are complete.',
|
||||
'Maintain cargo liability insurance per wagon or train consist as applicable.',
|
||||
],
|
||||
contractDocuments: [...STANDARD_CONTRACT_DOCUMENTS],
|
||||
};
|
||||
}
|
||||
|
||||
function buildExportContainerPack(): ContractClausePack {
|
||||
return {
|
||||
article1: {
|
||||
objective:
|
||||
'To provide railway transportation for 40ft and/or 20ft full containers from designated Ethiopian dry ports and terminals to SGTD and related export corridors.',
|
||||
scope: [
|
||||
'Railway export transport service on the agreed corridor.',
|
||||
'Terminal coordination at origin yards for export dispatch where applicable.',
|
||||
],
|
||||
},
|
||||
clientObligations: [
|
||||
'Provide export shipment instructions and container release details for each movement.',
|
||||
'Ensure containers are available at origin terminals per EDR operational windows.',
|
||||
'Submit required export, customs, and operational documents at origin at least 24 hours before loading.',
|
||||
PAYMENT_OBLIGATION,
|
||||
HAZARDOUS_OBLIGATION,
|
||||
],
|
||||
providerObligations: [
|
||||
'Assign voyage per operational schedule and notify train schedule 48 hours in advance.',
|
||||
'Provide safe transportation to SGTD and hand over for export processing when documents are complete.',
|
||||
'Maintain cargo liability insurance per wagon.',
|
||||
],
|
||||
contractDocuments: [...STANDARD_CONTRACT_DOCUMENTS],
|
||||
};
|
||||
}
|
||||
|
||||
function buildExportBulkPack(): ContractClausePack {
|
||||
return {
|
||||
article1: {
|
||||
objective:
|
||||
'To provide railway transportation for bulk export cargo from designated Ethiopian rail terminals to SGTD and related export corridors.',
|
||||
scope: [
|
||||
'Railway bulk export transport on the agreed corridor.',
|
||||
'Loading coordination at origin terminals per EDR operational rules.',
|
||||
],
|
||||
},
|
||||
clientObligations: [
|
||||
'Provide accurate commodity description, weight, and export shipment instructions.',
|
||||
'Ensure bulk cargo is prepared and available at origin per the agreed loading window.',
|
||||
'Submit required export and customs documents at least 24 hours before loading where applicable.',
|
||||
PAYMENT_OBLIGATION,
|
||||
HAZARDOUS_OBLIGATION,
|
||||
],
|
||||
providerObligations: [
|
||||
'Assign train capacity per operational schedule and notify departure timing 48 hours in advance where practicable.',
|
||||
'Provide safe bulk transportation to SGTD within agreed timelines when documents are complete.',
|
||||
'Maintain cargo liability insurance per wagon or train consist as applicable.',
|
||||
],
|
||||
contractDocuments: [...STANDARD_CONTRACT_DOCUMENTS],
|
||||
};
|
||||
}
|
||||
|
||||
function buildDomesticContainerPack(): ContractClausePack {
|
||||
return {
|
||||
article1: {
|
||||
objective:
|
||||
'To provide railway transportation for 40ft and/or 20ft containers between designated Ethiopian rail terminals on the domestic corridor.',
|
||||
scope: ['Domestic railway container transport between agreed origin and destination yards.'],
|
||||
},
|
||||
clientObligations: [
|
||||
'Provide shipment instructions for each domestic container movement.',
|
||||
'Ensure containers are available at origin per EDR operational rules.',
|
||||
PAYMENT_OBLIGATION,
|
||||
HAZARDOUS_OBLIGATION,
|
||||
],
|
||||
providerObligations: [
|
||||
'Assign voyage per operational schedule and notify train schedule 48 hours in advance where practicable.',
|
||||
'Provide safe transportation and deliver within agreed timelines when instructions are complete.',
|
||||
'Maintain cargo liability insurance per wagon.',
|
||||
],
|
||||
contractDocuments: [...STANDARD_CONTRACT_DOCUMENTS],
|
||||
};
|
||||
}
|
||||
|
||||
function buildDomesticBulkPack(): ContractClausePack {
|
||||
return {
|
||||
article1: {
|
||||
objective:
|
||||
'To provide railway transportation for bulk cargo between designated Ethiopian rail terminals on the domestic corridor.',
|
||||
scope: ['Domestic railway bulk transport between agreed origin and destination terminals.'],
|
||||
},
|
||||
clientObligations: [
|
||||
'Provide commodity description, weight, and shipment instructions for each movement.',
|
||||
'Ensure cargo is prepared at origin per the agreed loading window.',
|
||||
PAYMENT_OBLIGATION,
|
||||
HAZARDOUS_OBLIGATION,
|
||||
],
|
||||
providerObligations: [
|
||||
'Assign train capacity per operational schedule and notify departure timing 48 hours in advance where practicable.',
|
||||
'Provide safe bulk transportation within agreed timelines.',
|
||||
'Maintain cargo liability insurance per wagon or train consist as applicable.',
|
||||
],
|
||||
contractDocuments: [...STANDARD_CONTRACT_DOCUMENTS],
|
||||
};
|
||||
}
|
||||
|
||||
const BASE_PACKS: Record<ContractDirection, Record<ContractFreight, () => ContractClausePack>> = {
|
||||
IMP: {
|
||||
CON: buildImportContainerPack,
|
||||
BULK: buildImportBulkPack,
|
||||
},
|
||||
EXP: {
|
||||
CON: buildExportContainerPack,
|
||||
BULK: buildExportBulkPack,
|
||||
},
|
||||
DOM: {
|
||||
CON: buildDomesticContainerPack,
|
||||
BULK: buildDomesticBulkPack,
|
||||
},
|
||||
};
|
||||
|
||||
export function buildClausePack(
|
||||
direction: ContractDirection,
|
||||
freight: ContractFreight,
|
||||
service: ContractServiceScope,
|
||||
): ContractClausePack {
|
||||
const base = BASE_PACKS[direction][freight]();
|
||||
return applyForwardingOverlay(base, service);
|
||||
}
|
||||
|
||||
export function article1ObjectiveFromClause(article1: Article1Clause): string {
|
||||
return article1.objective;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ContractRendererService } from './contract-renderer.service';
|
||||
import { getTemplateMeta } from './contract-template.registry';
|
||||
import type { ContractViewModel } from './contract-view-model.builder';
|
||||
|
||||
describe('ContractRendererService', () => {
|
||||
const renderer = new ContractRendererService();
|
||||
renderer.onModuleInit();
|
||||
|
||||
function minimalView(templateKey: string): ContractViewModel {
|
||||
const template = getTemplateMeta(templateKey);
|
||||
return {
|
||||
bookingId: 'test-id',
|
||||
reference: 'BK-TEST-001',
|
||||
status: 'CONTRACT_READY',
|
||||
templateKey,
|
||||
template,
|
||||
contractDate: '1 January 2026',
|
||||
contractYear: 2026,
|
||||
client: {
|
||||
companyName: 'Test Co',
|
||||
companyAddress: 'Addis Ababa',
|
||||
companyLocation: 'Ethiopia',
|
||||
phone: '+251900000000',
|
||||
email: 'test@example.com',
|
||||
tinNumber: '1234567890',
|
||||
},
|
||||
pricing: {
|
||||
lineItems: [{ label: 'RAIL', description: 'Rail transport', amount: 1000, currency: 'ETB' }],
|
||||
surcharges: [],
|
||||
totalAmount: 1000,
|
||||
currency: 'ETB',
|
||||
originLabel: 'SGTD',
|
||||
destinationLabel: 'Modjo',
|
||||
containerLines: [{ label: '40ft', quantity: 2, vgmPerUnitTons: 12 }],
|
||||
},
|
||||
signatures: [],
|
||||
canSignCustomer: true,
|
||||
canSignStaff: false,
|
||||
hasContractDocument: false,
|
||||
hasCustomerSignature: false,
|
||||
hasStaffSignature: false,
|
||||
};
|
||||
}
|
||||
|
||||
it('renders import flagship with Nagad and Article 5', () => {
|
||||
const html = renderer.render(minimalView('IMP_CON_ETB_TRANSPORT_ONLY'));
|
||||
expect(html).toContain('Djibouti Nagad');
|
||||
expect(html).toContain('Article 5: Contract Price');
|
||||
expect(html).toContain('Article 2: Obligations of the Client');
|
||||
});
|
||||
|
||||
it('renders export variant without import empty-return clause', () => {
|
||||
const html = renderer.render(minimalView('EXP_CON_USD_TRANSPORT_ONLY'));
|
||||
expect(html).toContain('export corridors');
|
||||
expect(html).not.toContain('Return empty containers from Dire Dawa');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
CONTRACT_TEMPLATE_KEYS,
|
||||
CONTRACT_TEMPLATE_REGISTRY,
|
||||
getTemplateMeta,
|
||||
isValidTemplateKey,
|
||||
} from './contract-template.registry';
|
||||
|
||||
describe('ContractTemplateRegistry', () => {
|
||||
it('defines exactly 24 template keys', () => {
|
||||
expect(CONTRACT_TEMPLATE_KEYS).toHaveLength(24);
|
||||
expect(Object.keys(CONTRACT_TEMPLATE_REGISTRY)).toHaveLength(24);
|
||||
});
|
||||
|
||||
it('keys match the direction_freight_currency_service pattern', () => {
|
||||
for (const key of CONTRACT_TEMPLATE_KEYS) {
|
||||
expect(isValidTemplateKey(key)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('each meta has non-empty obligations and article1 scope', () => {
|
||||
for (const key of CONTRACT_TEMPLATE_KEYS) {
|
||||
const meta = CONTRACT_TEMPLATE_REGISTRY[key]!;
|
||||
expect(meta.clientObligations.length).toBeGreaterThan(0);
|
||||
expect(meta.providerObligations.length).toBeGreaterThan(0);
|
||||
expect(meta.article1.scope.length).toBeGreaterThan(0);
|
||||
expect(meta.article1.objective.length).toBeGreaterThan(0);
|
||||
expect(meta.contractDocuments.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('IMP_CON_ETB_TRANSPORT_ONLY retains import container flagship clauses', () => {
|
||||
const meta = getTemplateMeta('IMP_CON_ETB_TRANSPORT_ONLY');
|
||||
expect(meta.direction).toBe('IMP');
|
||||
expect(meta.freight).toBe('CON');
|
||||
expect(meta.article1.objective).toContain('SGTD');
|
||||
expect(meta.article1.objective).toContain('empty container return');
|
||||
const clientText = meta.clientObligations.join(' ');
|
||||
expect(clientText).toContain('Djibouti Nagad');
|
||||
const providerText = meta.providerObligations.join(' ');
|
||||
expect(providerText).toContain('seven (7) calendar days');
|
||||
});
|
||||
|
||||
it('EXP_CON_USD_TRANSPORT_ONLY uses export-oriented article1', () => {
|
||||
const meta = getTemplateMeta('EXP_CON_USD_TRANSPORT_ONLY');
|
||||
expect(meta.direction).toBe('EXP');
|
||||
expect(meta.article1.objective).toContain('SGTD');
|
||||
expect(meta.providerObligations.join(' ')).not.toContain(
|
||||
'Return empty containers from Dire Dawa',
|
||||
);
|
||||
});
|
||||
|
||||
it('FORWARDING adds scope and provider obligations', () => {
|
||||
const transport = getTemplateMeta('IMP_CON_ETB_TRANSPORT_ONLY');
|
||||
const forwarding = getTemplateMeta('IMP_CON_ETB_FORWARDING');
|
||||
expect(forwarding.article1.scope.length).toBeGreaterThan(
|
||||
transport.article1.scope.length,
|
||||
);
|
||||
expect(forwarding.providerObligations.length).toBeGreaterThan(
|
||||
transport.providerObligations.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('getTemplateMeta fallback includes clause arrays for unknown keys', () => {
|
||||
const meta = getTemplateMeta('UNKNOWN_KEY');
|
||||
expect(meta.clientObligations.length).toBeGreaterThan(0);
|
||||
expect(meta.article1.scope.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +1,44 @@
|
||||
export interface ContractTemplateMeta {
|
||||
key: string;
|
||||
title: string;
|
||||
directionLabel: string;
|
||||
freightLabel: string;
|
||||
currency: string;
|
||||
serviceScope: 'TRANSPORT_ONLY' | 'FORWARDING';
|
||||
/** Optional dedicated .hbs file; otherwise uses generic.hbs */
|
||||
templateFile?: string;
|
||||
whereas: string;
|
||||
article1Objective: string;
|
||||
}
|
||||
import {
|
||||
article1ObjectiveFromClause,
|
||||
buildClausePack,
|
||||
} from './contract-clause-packs';
|
||||
import type {
|
||||
ContractDirection,
|
||||
ContractFreight,
|
||||
ContractServiceScope,
|
||||
ContractTemplateMeta,
|
||||
} from './contract-template.types';
|
||||
|
||||
const DIRECTION_LABELS: Record<string, string> = {
|
||||
export type { ContractTemplateMeta } from './contract-template.types';
|
||||
|
||||
const DIRECTION_LABELS: Record<ContractDirection, string> = {
|
||||
IMP: 'Import',
|
||||
EXP: 'Export',
|
||||
DOM: 'Domestic',
|
||||
};
|
||||
|
||||
const FREIGHT_LABELS: Record<string, string> = {
|
||||
const FREIGHT_LABELS: Record<ContractFreight, string> = {
|
||||
CON: 'Container',
|
||||
BULK: 'Bulk',
|
||||
};
|
||||
|
||||
const DIRECTIONS: ContractDirection[] = ['IMP', 'EXP', 'DOM'];
|
||||
const FREIGHTS: ContractFreight[] = ['CON', 'BULK'];
|
||||
const CURRENCIES = ['ETB', 'USD'] as const;
|
||||
const SERVICES: ContractServiceScope[] = ['TRANSPORT_ONLY', 'FORWARDING'];
|
||||
|
||||
const KEY_PATTERN =
|
||||
/^(IMP|EXP|DOM)_(CON|BULK)_(ETB|USD)_(TRANSPORT_ONLY|FORWARDING)$/;
|
||||
|
||||
function buildMeta(
|
||||
dir: string,
|
||||
freight: string,
|
||||
dir: ContractDirection,
|
||||
freight: ContractFreight,
|
||||
currency: string,
|
||||
service: 'TRANSPORT_ONLY' | 'FORWARDING',
|
||||
templateFile?: string,
|
||||
service: ContractServiceScope,
|
||||
): ContractTemplateMeta {
|
||||
const key = `${dir}_${freight}_${currency}_${service}`;
|
||||
const dirLabel = DIRECTION_LABELS[dir] ?? dir;
|
||||
const freightLabel = FREIGHT_LABELS[freight] ?? freight;
|
||||
const dirLabel = DIRECTION_LABELS[dir];
|
||||
const freightLabel = FREIGHT_LABELS[freight];
|
||||
const serviceLabel =
|
||||
service === 'FORWARDING' ? 'Rail and Forwarding' : 'Transport Only';
|
||||
|
||||
@@ -42,24 +49,26 @@ function buildMeta(
|
||||
? 'from Ethiopian dry ports to SGTD and related export corridors'
|
||||
: 'between designated Ethiopian rail terminals';
|
||||
|
||||
const clauses = buildClausePack(dir, freight, service);
|
||||
|
||||
return {
|
||||
key,
|
||||
direction: dir,
|
||||
freight,
|
||||
currency,
|
||||
serviceScope: service,
|
||||
title: `${dirLabel} ${freightLabel} Transport Service by Railway (${serviceLabel})`,
|
||||
directionLabel: dirLabel,
|
||||
freightLabel,
|
||||
currency,
|
||||
serviceScope: service,
|
||||
templateFile,
|
||||
whereas: `The Client has requested transportation of ${freightLabel.toLowerCase()} cargo ${corridor} using the Addis Ababa–Djibouti Railway line. The Service Provider has agreed to provide services per this contract.`,
|
||||
article1Objective: `To provide railway transportation services for ${freightLabel.toLowerCase()} cargo on the agreed corridor (${serviceLabel}).`,
|
||||
article1Objective: article1ObjectiveFromClause(clauses.article1),
|
||||
article1: clauses.article1,
|
||||
clientObligations: clauses.clientObligations,
|
||||
providerObligations: clauses.providerObligations,
|
||||
contractDocuments: clauses.contractDocuments,
|
||||
};
|
||||
}
|
||||
|
||||
const DIRECTIONS = ['IMP', 'EXP', 'DOM'] as const;
|
||||
const FREIGHTS = ['CON', 'BULK'] as const;
|
||||
const CURRENCIES = ['ETB', 'USD'] as const;
|
||||
const SERVICES = ['TRANSPORT_ONLY', 'FORWARDING'] as const;
|
||||
|
||||
/** Full template matrix (24 keys). */
|
||||
export const CONTRACT_TEMPLATE_REGISTRY: Record<string, ContractTemplateMeta> =
|
||||
{};
|
||||
@@ -68,31 +77,43 @@ for (const dir of DIRECTIONS) {
|
||||
for (const freight of FREIGHTS) {
|
||||
for (const currency of CURRENCIES) {
|
||||
for (const service of SERVICES) {
|
||||
const dedicated =
|
||||
dir === 'IMP' &&
|
||||
freight === 'CON' &&
|
||||
currency === 'ETB' &&
|
||||
service === 'TRANSPORT_ONLY'
|
||||
? 'IMP_CON_ETB_TRANSPORT_ONLY.hbs'
|
||||
: undefined;
|
||||
const meta = buildMeta(dir, freight, currency, service, dedicated);
|
||||
const meta = buildMeta(dir, freight, currency, service);
|
||||
CONTRACT_TEMPLATE_REGISTRY[meta.key] = meta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTemplateMeta(key: string): ContractTemplateMeta {
|
||||
return (
|
||||
CONTRACT_TEMPLATE_REGISTRY[key] ?? {
|
||||
key,
|
||||
title: 'Freight Contract Agreement',
|
||||
directionLabel: 'Freight',
|
||||
freightLabel: 'Cargo',
|
||||
currency: 'USD',
|
||||
serviceScope: 'TRANSPORT_ONLY',
|
||||
whereas: 'The parties agree to railway freight services as described in the schedule below.',
|
||||
article1Objective: 'To provide railway transportation services per the agreed schedule.',
|
||||
}
|
||||
);
|
||||
export const CONTRACT_TEMPLATE_KEYS = Object.keys(CONTRACT_TEMPLATE_REGISTRY);
|
||||
|
||||
export function listTemplateKeys(): string[] {
|
||||
return CONTRACT_TEMPLATE_KEYS;
|
||||
}
|
||||
|
||||
export function getTemplateMeta(key: string): ContractTemplateMeta {
|
||||
const found = CONTRACT_TEMPLATE_REGISTRY[key];
|
||||
if (found) return found;
|
||||
|
||||
const fallbackClauses = buildClausePack('IMP', 'CON', 'TRANSPORT_ONLY');
|
||||
return {
|
||||
key,
|
||||
direction: 'IMP',
|
||||
freight: 'CON',
|
||||
currency: 'USD',
|
||||
serviceScope: 'TRANSPORT_ONLY',
|
||||
title: 'Freight Contract Agreement',
|
||||
directionLabel: 'Freight',
|
||||
freightLabel: 'Cargo',
|
||||
whereas:
|
||||
'The parties agree to railway freight services as described in the schedule below.',
|
||||
article1Objective: article1ObjectiveFromClause(fallbackClauses.article1),
|
||||
article1: fallbackClauses.article1,
|
||||
clientObligations: fallbackClauses.clientObligations,
|
||||
providerObligations: fallbackClauses.providerObligations,
|
||||
contractDocuments: fallbackClauses.contractDocuments,
|
||||
};
|
||||
}
|
||||
|
||||
export function isValidTemplateKey(key: string): boolean {
|
||||
return KEY_PATTERN.test(key);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ContractTemplateResolver } from './contract-template.resolver';
|
||||
import { Booking } from '../modules/bookings/entities/booking.entity';
|
||||
import { ServiceType } from '../modules/rule-engine/entities/service-type.entity';
|
||||
import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity';
|
||||
|
||||
describe('ContractTemplateResolver', () => {
|
||||
const resolver = new ContractTemplateResolver();
|
||||
|
||||
function booking(partial: Partial<Booking>): Booking {
|
||||
return partial as Booking;
|
||||
}
|
||||
|
||||
it('resolves import container ETB transport-only', () => {
|
||||
const key = resolver.resolve(
|
||||
booking({
|
||||
tradeDirection: 'IMPORT',
|
||||
freightType: 'CONTAINER',
|
||||
paymentCurrency: 'ETB',
|
||||
serviceType: { code: 'RAIL_ONLY', includesFirstMile: false, includesLastMile: false } as ServiceType,
|
||||
}),
|
||||
);
|
||||
expect(key).toBe('IMP_CON_ETB_TRANSPORT_ONLY');
|
||||
});
|
||||
|
||||
it('resolves export bulk USD forwarding', () => {
|
||||
const key = resolver.resolve(
|
||||
booking({
|
||||
tradeDirection: 'EXPORT',
|
||||
freightType: 'BULK',
|
||||
paymentCurrency: 'USD',
|
||||
serviceType: {
|
||||
code: 'RAIL_FORWARDING',
|
||||
includesFirstMile: true,
|
||||
includesLastMile: false,
|
||||
} as ServiceType,
|
||||
}),
|
||||
);
|
||||
expect(key).toBe('EXP_BULK_USD_FORWARDING');
|
||||
});
|
||||
|
||||
it('maps BREAK_BULK cargo to BULK freight', () => {
|
||||
const key = resolver.resolve(
|
||||
booking({
|
||||
tradeDirection: 'IMPORT',
|
||||
freightType: 'CONTAINER',
|
||||
paymentCurrency: 'ETB',
|
||||
cargoType: { code: 'BREAK_BULK_GENERAL' } as CargoType,
|
||||
serviceType: undefined,
|
||||
}),
|
||||
);
|
||||
expect(key).toBe('IMP_BULK_ETB_TRANSPORT_ONLY');
|
||||
});
|
||||
|
||||
it('resolves domestic container', () => {
|
||||
const key = resolver.resolve(
|
||||
booking({
|
||||
tradeDirection: 'DOMESTIC',
|
||||
freightType: 'CONTAINER',
|
||||
paymentCurrency: 'USD',
|
||||
serviceType: undefined,
|
||||
}),
|
||||
);
|
||||
expect(key).toBe('DOM_CON_USD_TRANSPORT_ONLY');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
export type ContractDirection = 'IMP' | 'EXP' | 'DOM';
|
||||
export type ContractFreight = 'CON' | 'BULK';
|
||||
export type ContractServiceScope = 'TRANSPORT_ONLY' | 'FORWARDING';
|
||||
|
||||
export interface Article1Clause {
|
||||
objective: string;
|
||||
scope: string[];
|
||||
}
|
||||
|
||||
export interface ContractClausePack {
|
||||
article1: Article1Clause;
|
||||
clientObligations: string[];
|
||||
providerObligations: string[];
|
||||
contractDocuments: string[];
|
||||
}
|
||||
|
||||
export interface ContractTemplateMeta {
|
||||
key: string;
|
||||
direction: ContractDirection;
|
||||
freight: ContractFreight;
|
||||
currency: string;
|
||||
serviceScope: ContractServiceScope;
|
||||
title: string;
|
||||
directionLabel: string;
|
||||
freightLabel: string;
|
||||
whereas: string;
|
||||
/** Summary line for APIs; mirrors article1.objective */
|
||||
article1Objective: string;
|
||||
article1: Article1Clause;
|
||||
clientObligations: string[];
|
||||
providerObligations: string[];
|
||||
contractDocuments: string[];
|
||||
/** Optional dedicated .hbs file; otherwise uses generic.hbs */
|
||||
templateFile?: string;
|
||||
}
|
||||
@@ -81,12 +81,18 @@ export class ContractViewModelBuilder {
|
||||
}),
|
||||
contractYear: new Date().getFullYear(),
|
||||
client: {
|
||||
companyName: booking.customer?.companyName ?? 'Client',
|
||||
companyAddress: booking.customer?.companyAddress ?? '—',
|
||||
companyLocation: booking.customer?.companyLocation ?? '—',
|
||||
phone: booking.customer?.companyPhone ?? booking.customer?.phone ?? '—',
|
||||
email: booking.customer?.companyEmail ?? booking.customer?.email ?? '—',
|
||||
tinNumber: booking.customer?.tinNumber ?? '—',
|
||||
// companyName: booking.customer?.companyName ?? 'Client',
|
||||
// companyAddress: booking.customer?.companyAddress ?? '—',
|
||||
// companyLocation: booking.customer?.companyLocation ?? '—',
|
||||
// phone: booking.customer?.companyPhone ?? booking.customer?.phone ?? '—',
|
||||
// email: booking.customer?.companyEmail ?? booking.customer?.email ?? '—',
|
||||
// tinNumber: booking.customer?.tinNumber ?? '—',
|
||||
companyName: booking.company?.name ?? 'Client',
|
||||
companyAddress: booking.company?.address ?? '—',
|
||||
companyLocation: booking.company?.country ?? '—',
|
||||
phone: booking.company?.phone ?? '—',
|
||||
email: booking.company?.email ?? '—',
|
||||
tinNumber: booking.company?.tin ?? '—',
|
||||
},
|
||||
pricing,
|
||||
signatures,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Import Container Transport — {{reference}}</title>
|
||||
{{> styles}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="cover">
|
||||
<h1>Contract Agreement</h1>
|
||||
<h1>Import Container Transport Service by Railway</h1>
|
||||
<p class="meta"><strong>Contract Ref No:</strong> {{reference}}</p>
|
||||
<p class="meta"><strong>Year:</strong> {{contractYear}}</p>
|
||||
</div>
|
||||
|
||||
<p>This Contract Agreement is made on <strong>{{contractDate}}</strong>.</p>
|
||||
<p><strong>Between</strong> Ethio-Djibouti Standard Gauge Railway Share Company (EDR), Addis Ababa (“Service Provider”), and <strong>{{client.companyName}}</strong> at {{client.companyAddress}}, {{client.companyLocation}} (“Client”). Phone {{client.phone}} / {{client.email}}. TIN {{client.tinNumber}}.</p>
|
||||
|
||||
<h2>Whereas</h2>
|
||||
<p>{{template.whereas}}</p>
|
||||
<p>Now therefore, the parties agree as follows:</p>
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 1: Objective and Scope of Services</h2>
|
||||
<p><strong>Objective:</strong> To provide railway transportation for 40ft and/or 20ft full containers from SGTD to Dire Dawa, Modjo dry port and/or Galaan Multipurpose port (GMP), and empty container return from those terminals to SGTD.</p>
|
||||
<p><strong>Scope:</strong> (1) Railway transport service; (2) Cargo handling at Galaan Multipurpose port (GMP) where applicable.</p>
|
||||
</div>
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 2: Obligations of the Client (summary)</h2>
|
||||
<ol>
|
||||
<li>Provide shipment instructions to EDR for container movements on the agreed corridor.</li>
|
||||
<li>Meet minimum container supply per terminal (Modjo, Dire Dawa, GMP) as per EDR operational rules.</li>
|
||||
<li>Submit required documents to Djibouti Nagad station at least 24 hours before loading.</li>
|
||||
<li>Pay 100% transportation fees in advance per train set in {{paymentArticle}}.</li>
|
||||
<li>Notify EDR 48 hours in advance for hazardous or valuable cargo.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 3: Obligations of the Service Provider (summary)</h2>
|
||||
<ol>
|
||||
<li>Assign voyage per operational schedule and notify train schedule 48 hours in advance.</li>
|
||||
<li>Provide safe transportation and deliver within agreed timelines when documents are complete.</li>
|
||||
<li>Return empty containers from Dire Dawa, Modjo and GMP to SGTD within seven (7) calendar days of receipt.</li>
|
||||
<li>Maintain cargo liability insurance per wagon.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{> article5_pricing}}
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 4: Force Majeure</h2>
|
||||
<p>Neither party is liable for delays due to force majeure interpreted under the Ethiopian Civil Code.</p>
|
||||
</div>
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 6: Contract Documents</h2>
|
||||
<ol>
|
||||
<li>Amendments (if any)</li>
|
||||
<li>This Contract Agreement</li>
|
||||
<li>Final Minutes of Negotiation (if any)</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{{> signatures_block}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="article">
|
||||
<h2>Article 1: Objective and Scope of Services</h2>
|
||||
<p><strong>Objective:</strong> {{template.article1.objective}}</p>
|
||||
{{#if template.article1.scope.length}}
|
||||
<p><strong>Scope:</strong></p>
|
||||
<ol>
|
||||
{{#each template.article1.scope}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="article">
|
||||
<h2>Article 2: Obligations of the Client (summary)</h2>
|
||||
<ol>
|
||||
{{#each template.clientObligations}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 3: Obligations of the Service Provider (summary)</h2>
|
||||
<ol>
|
||||
{{#each template.providerObligations}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="article">
|
||||
<h2>Article 6: Contract Documents</h2>
|
||||
<ol>
|
||||
{{#each template.contractDocuments}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="article">
|
||||
<h2>Article 4: Force Majeure</h2>
|
||||
<p>Neither party is liable for delays due to force majeure interpreted under the Ethiopian Civil Code.</p>
|
||||
</div>
|
||||
@@ -14,29 +14,17 @@
|
||||
</div>
|
||||
|
||||
<p>This Contract Agreement is made on <strong>{{contractDate}}</strong>.</p>
|
||||
<p><strong>Between</strong> Ethio-Djibouti Standard Gauge Railway Share Company (EDR) (“Service Provider”) and <strong>{{client.companyName}}</strong> (“Client”) at {{client.companyAddress}}, {{client.companyLocation}}. Phone: {{client.phone}}. Email: {{client.email}}. TIN: {{client.tinNumber}}.</p>
|
||||
<p><strong>Between</strong> Ethio-Djibouti Standard Gauge Railway Share Company (EDR), Addis Ababa (“Service Provider”), and <strong>{{client.companyName}}</strong> at {{client.companyAddress}}, {{client.companyLocation}} (“Client”). Phone {{client.phone}} / {{client.email}}. TIN {{client.tinNumber}}.</p>
|
||||
|
||||
<h2>Whereas</h2>
|
||||
<p>{{template.whereas}}</p>
|
||||
<p>Now therefore, the parties agree as follows:</p>
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 1: Objective and Scope</h2>
|
||||
<p>{{template.article1Objective}}</p>
|
||||
</div>
|
||||
|
||||
{{> article1}}
|
||||
{{> articles_obligations}}
|
||||
{{> article5_pricing}}
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 4: Force Majeure</h2>
|
||||
<p>Neither party shall be liable for delays caused by force majeure beyond reasonable control, interpreted per the Ethiopian Civil Code.</p>
|
||||
</div>
|
||||
|
||||
<div class="article">
|
||||
<h2>Article 6: Contract Documents</h2>
|
||||
<p>This agreement, amendments (if any), and negotiated minutes constitute the contract.</p>
|
||||
</div>
|
||||
|
||||
{{> force_majeure}}
|
||||
{{> contract_documents}}
|
||||
{{> signatures_block}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user