Files
edr-platform/apps/edr-freight-api/src/contracts/contract-dynamic-template.spec.ts
Marshal 5d68a3f7b7 add permanent purge functionality for wagons and routes
- Implemented a  method in  to permanently delete wagons without history.
- Added corresponding permissions for hard delete actions in .
- Updated the UI components to include purge actions, ensuring they are only available to users with the appropriate permissions.
- Created modals for confirming permanent deletions in  and .
- Enhanced API services to handle purge requests for locomotives, wagons, and routes.
- Added tests for the purge functionality in both  and  services to ensure proper behavior and error handling.
2026-08-04 14:07:16 +00:00

268 lines
9.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 { 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 sub-clauses by outline token and marks each level 1. → a. → i.', () => {
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'],
['a', 2, 'Rail transport'],
['i', 3, 'Wagon supply'],
['2', 1, 'Payment'],
]);
});
it('cycles markers back to arabic at depth 4 and counts each level on its own', () => {
const parsed = parseArticleBody(
'1. One\n1.1 Alpha\n1.2 Beta\n1.2.1 Roman one\n1.2.2 Roman two\n1.2.2.1 Deep',
);
expect(parsed.clauses.map((c) => [c.number, c.depth])).toEqual([
['1', 1],
['a', 2],
['b', 2],
['i', 3],
['ii', 3],
['1', 4],
]);
});
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,
contractStartDate: '1 January 2026',
contractEndDate: '31 December 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',
cargoTypeName: 'Steel billets',
containerType: '—',
cargoSummary: 'Steel billets × 2,800',
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('shows the contract validity window in the commercial schedule annex', () => {
const html = renderer.render(dynamicView());
expect(html).toContain('Valid from');
expect(html).toContain('Valid until');
expect(html).toContain('1 January 2026');
expect(html).toContain('31 December 2026');
});
it('interpolates the start/end date placeholders inside article text', () => {
const view = dynamicView();
expect(
interpolateTemplateText(
'In force {{contractStartDate}} to {{contractEndDate}}.',
view,
),
).toBe('In force 1 January 2026 to 31 December 2026.');
});
it('shows cargo type and container type in the commercial schedule annex', () => {
const html = renderer.render(dynamicView());
expect(html).toContain('Cargo type');
expect(html).toContain('Container type');
expect(html).toContain('Cargo scope');
expect(html).toContain('Steel billets × 2,800');
});
it('interpolates the cargo/container placeholders inside article text', () => {
const view = dynamicView();
const body =
'Cargo: {{schedule.cargoTypeName}} in {{schedule.containerType}} ' +
'({{schedule.freightType}}). Scope: {{schedule.cargoSummary}}.';
expect(interpolateTemplateText(body, view)).toBe(
'Cargo: Steel billets in — (BULK). Scope: Steel billets × 2,800.',
);
});
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');
});
});