Files
edr-platform/apps/edr-freight-api/src/contracts/contract-dynamic-template.spec.ts
Marshal 3a1a08b1e1 add lashing surcharge for cargo types with hasLashing flag
add lashing surcharge for cargo types with hasLashing flag
2026-07-17 23:25:53 +00:00

213 lines
7.8 KiB
TypeScript

import { parseArticleBody, interpolateTemplateText } from './contract-article.util';
import { ContractRendererService } from './contract-renderer.service';
import { getTemplateMeta } from './contract-template.registry';
import type { ContractViewModel } from './contract-view-model.builder';
describe('parseArticleBody', () => {
it('numbers each non-empty line as a clause', () => {
const parsed = parseArticleBody('First clause.\nSecond clause.\n\nThird clause.');
expect(parsed.paragraph).toBeUndefined();
expect(parsed.clauses.map((c) => c.text)).toEqual([
'First clause.',
'Second clause.',
'Third clause.',
]);
});
it('nests "- " lines as bullets under the previous clause', () => {
const parsed = parseArticleBody('Rates are:\n- USD 10 per ton\n- USD 20 per wagon\nPayment in advance.');
expect(parsed.clauses).toHaveLength(2);
expect(parsed.clauses[0].bullets).toEqual(['USD 10 per ton', 'USD 20 per wagon']);
expect(parsed.clauses[1].text).toBe('Payment in advance.');
});
it('renders a single bare line as a paragraph', () => {
const parsed = parseArticleBody('This Agreement becomes effective when signed.');
expect(parsed.paragraph).toBe('This Agreement becomes effective when signed.');
expect(parsed.clauses).toEqual([]);
});
it('nests numbered sub-clauses by their outline token and renumbers sequentially', () => {
const parsed = parseArticleBody(
'1. Scope\n5.1 Rail transport\n1.1.1 Wagon supply\n2. Payment',
);
expect(parsed.clauses.map((c) => [c.number, c.depth, c.text])).toEqual([
['1', 1, 'Scope'],
['1.1', 2, 'Rail transport'],
['1.1.1', 3, 'Wagon supply'],
['2', 1, 'Payment'],
]);
});
it('clamps a sub-clause with no open parent to the next available level', () => {
const parsed = parseArticleBody('1.1.1 Orphan sub-clause\nSecond clause.');
expect(parsed.clauses.map((c) => [c.number, c.depth])).toEqual([
['1', 1],
['2', 1],
]);
});
it('leaves prose that merely starts with a number un-tokenized', () => {
const parsed = parseArticleBody('10 tons is the minimum load.\nPayment in advance.');
expect(parsed.clauses.map((c) => c.text)).toEqual([
'10 tons is the minimum load.',
'Payment in advance.',
]);
});
it('keeps a single explicitly numbered line as a clause, not a paragraph', () => {
const parsed = parseArticleBody('1. Only clause.');
expect(parsed.paragraph).toBeUndefined();
expect(parsed.clauses.map((c) => [c.number, c.text])).toEqual([['1', 'Only clause.']]);
});
});
describe('interpolateTemplateText', () => {
it('fills placeholders from the view model', () => {
expect(
interpolateTemplateText('Valid until August 31, {{contractYear}}.', {
contractYear: 2026,
}),
).toBe('Valid until August 31, 2026.');
});
it('falls back to raw text on malformed placeholders', () => {
expect(interpolateTemplateText('Broken {{#if}} tag', {})).toBe('Broken {{#if}} tag');
});
});
describe('dynamic template rendering (edr-dynamic.hbs)', () => {
const renderer = new ContractRendererService();
renderer.onModuleInit();
function dynamicView(): ContractViewModel {
const meta = getTemplateMeta('IMP_BULK_USD_FORWARDING');
return {
bookingId: 'test-id',
reference: 'EDR/CT/2026/0042',
status: 'CONTRACT_READY',
templateKey: 'IMP_BULK_USD_FORWARDING',
template: { ...meta, title: 'Bulk Import Contract', templateFile: 'edr-dynamic.hbs' },
contractDate: '1 January 2026',
contractYear: 2026,
client: {
companyName: 'Abyssinia Trading PLC',
companyAddress: 'Bole Sub-city, Addis Ababa',
companyLocation: 'Ethiopia',
phone: '+251900000000',
email: 'test@example.com',
tinNumber: '1234567890',
vatNumber: 'VAT-001',
fanNumber: 'FAN-001',
businessLicense: 'BL-001',
},
provider: {
name: 'Ethio-Djibouti Standard Gauge Railway Share Company',
address: 'Nifas Silk Lafto Sub City, Addis Ababa, Ethiopia',
phone: '+251 11 872 0000',
email: 'info@edr.gov.et',
tinNumber: '—',
},
schedule: {
originLabel: 'Nagad',
destinationLabel: 'Galaan Multipurpose Port',
tradeDirection: 'IMPORT',
freightType: 'BULK',
serviceType: 'Rail + clearance',
scheduledDate: '—',
contractType: 'GENERAL',
cargoDescription: 'Steel billets',
totalWeightVgm: '—',
equipmentReturn: '—',
hazardousLabel: 'No',
firstMilePickupAddress: '—',
lastMileDeliveryAddress: '—',
},
pricing: {
displayMode: 'UNIT_RATES',
unitRates: [
{ label: 'Rail transport', unitPrice: 59.4, unit: 'ton', currency: 'USD' },
],
currency: 'USD',
equipmentReturn: '—',
originLabel: 'Nagad',
destinationLabel: 'Galaan Multipurpose Port',
} as unknown as ContractViewModel['pricing'],
rateSchedule: {
freightLanes: [
{ route: 'Nagad → Galaan Multipurpose Port', cargo: 'Wheat', currency: 'USD', amount: '100', unit: 'per wagon' },
],
additionalServices: [
{ route: 'First-mile pickup by truck', cargo: '—', currency: 'USD', amount: '50', unit: 'per wagon' },
],
surcharges: [],
isEmpty: false,
currencyLabel: 'USD',
},
signatures: [],
canSignCustomer: false,
canSignStaff: false,
hasContractDocument: false,
hasCustomerSignature: false,
hasStaffSignature: false,
dynamicTemplate: {
code: 'IMPORT_BULK',
name: 'Bulk Import Contract',
documentTitle: 'Bulk Cargo Transportation and Customs Clearance Services',
whereasClauses: ['The Client has agreed to engage the Service Provider.'],
articles: [
{
id: 'objective',
title: 'Objective of the Services',
body: 'Integrated logistics services including:\n- Rail transport to GMP\n- Customs clearance',
order: 1,
},
{
id: 'pricing',
title: 'Contract Price and Payment Terms',
body: 'Rates are set out in the Rate Schedule below.\nPayments 100% in advance.',
order: 2,
},
{
id: 'duration',
title: 'Duration',
body: 'Valid until August 31, {{contractYear}}.',
order: 3,
},
],
},
};
}
it('renders numbered dynamic articles with bullets and interpolation', () => {
const html = renderer.render(dynamicView());
expect(html).toContain('Bulk Cargo Transportation and Customs Clearance Services');
expect(html).toContain('Article 1');
expect(html).toContain('Objective of the Services');
expect(html).toContain('Rail transport to GMP');
expect(html).toContain('Valid until August 31, 2026.');
expect(html).toContain('Abyssinia Trading PLC');
expect(html).toContain('Annex A — Commercial Schedule');
// Greenish theme marker from styles.hbs
expect(html).toContain('#1b9e7a');
});
it('renders the live rate schedule lane under the pricing article', () => {
const html = renderer.render(dynamicView());
expect(html).toContain('Rate Schedule');
// Base freight lane pulled from the rate config
expect(html).toContain('Nagad → Galaan Multipurpose Port');
expect(html).toContain('USD 100 per wagon');
// Additional-service group
expect(html).toContain('First-mile pickup by truck');
});
it('keeps the generic layout when no dynamic template is attached', () => {
const view = dynamicView();
delete view.dynamicTemplate;
view.template = getTemplateMeta('IMP_BULK_USD_FORWARDING');
const html = renderer.render(view);
expect(html).toContain('Article 5: Contract Price');
});
});