mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
213 lines
7.8 KiB
TypeScript
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');
|
|
});
|
|
});
|