diff --git a/apps/edr-freight-api/src/contracts/contract-clause-packs.ts b/apps/edr-freight-api/src/contracts/contract-clause-packs.ts new file mode 100644 index 000000000..2d73cd50d --- /dev/null +++ b/apps/edr-freight-api/src/contracts/contract-clause-packs.ts @@ -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 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; +} diff --git a/apps/edr-freight-api/src/contracts/contract-renderer.service.spec.ts b/apps/edr-freight-api/src/contracts/contract-renderer.service.spec.ts new file mode 100644 index 000000000..421621808 --- /dev/null +++ b/apps/edr-freight-api/src/contracts/contract-renderer.service.spec.ts @@ -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'); + }); +}); diff --git a/apps/edr-freight-api/src/contracts/contract-template.registry.spec.ts b/apps/edr-freight-api/src/contracts/contract-template.registry.spec.ts new file mode 100644 index 000000000..2c921889a --- /dev/null +++ b/apps/edr-freight-api/src/contracts/contract-template.registry.spec.ts @@ -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); + }); +}); diff --git a/apps/edr-freight-api/src/contracts/contract-template.registry.ts b/apps/edr-freight-api/src/contracts/contract-template.registry.ts index aacf6a0a3..3e91e1b4f 100644 --- a/apps/edr-freight-api/src/contracts/contract-template.registry.ts +++ b/apps/edr-freight-api/src/contracts/contract-template.registry.ts @@ -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 = { +export type { ContractTemplateMeta } from './contract-template.types'; + +const DIRECTION_LABELS: Record = { IMP: 'Import', EXP: 'Export', DOM: 'Domestic', }; -const FREIGHT_LABELS: Record = { +const FREIGHT_LABELS: Record = { 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 = {}; @@ -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); } diff --git a/apps/edr-freight-api/src/contracts/contract-template.resolver.spec.ts b/apps/edr-freight-api/src/contracts/contract-template.resolver.spec.ts new file mode 100644 index 000000000..f4cb1c4ee --- /dev/null +++ b/apps/edr-freight-api/src/contracts/contract-template.resolver.spec.ts @@ -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 { + 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'); + }); +}); diff --git a/apps/edr-freight-api/src/contracts/contract-template.types.ts b/apps/edr-freight-api/src/contracts/contract-template.types.ts new file mode 100644 index 000000000..b056b4c69 --- /dev/null +++ b/apps/edr-freight-api/src/contracts/contract-template.types.ts @@ -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; +} diff --git a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts index 36c0328c8..2eab2a01b 100644 --- a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts +++ b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts @@ -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, diff --git a/apps/edr-freight-api/src/contracts/templates/IMP_CON_ETB_TRANSPORT_ONLY.hbs b/apps/edr-freight-api/src/contracts/templates/IMP_CON_ETB_TRANSPORT_ONLY.hbs deleted file mode 100644 index 90b6dba93..000000000 --- a/apps/edr-freight-api/src/contracts/templates/IMP_CON_ETB_TRANSPORT_ONLY.hbs +++ /dev/null @@ -1,68 +0,0 @@ - - - - - Import Container Transport — {{reference}} - {{> styles}} - - -
-

Contract Agreement

-

Import Container Transport Service by Railway

-

Contract Ref No: {{reference}}

-

Year: {{contractYear}}

-
- -

This Contract Agreement is made on {{contractDate}}.

-

Between Ethio-Djibouti Standard Gauge Railway Share Company (EDR), Addis Ababa (“Service Provider”), and {{client.companyName}} at {{client.companyAddress}}, {{client.companyLocation}} (“Client”). Phone {{client.phone}} / {{client.email}}. TIN {{client.tinNumber}}.

- -

Whereas

-

{{template.whereas}}

-

Now therefore, the parties agree as follows:

- -
-

Article 1: Objective and Scope of Services

-

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: (1) Railway transport service; (2) Cargo handling at Galaan Multipurpose port (GMP) where applicable.

-
- -
-

Article 2: Obligations of the Client (summary)

-
    -
  1. Provide shipment instructions to EDR for container movements on the agreed corridor.
  2. -
  3. Meet minimum container supply per terminal (Modjo, Dire Dawa, GMP) as per EDR operational rules.
  4. -
  5. Submit required documents to Djibouti Nagad station at least 24 hours before loading.
  6. -
  7. Pay 100% transportation fees in advance per train set in {{paymentArticle}}.
  8. -
  9. Notify EDR 48 hours in advance for hazardous or valuable cargo.
  10. -
-
- -
-

Article 3: Obligations of the Service Provider (summary)

-
    -
  1. Assign voyage per operational schedule and notify train schedule 48 hours in advance.
  2. -
  3. Provide safe transportation and deliver within agreed timelines when documents are complete.
  4. -
  5. Return empty containers from Dire Dawa, Modjo and GMP to SGTD within seven (7) calendar days of receipt.
  6. -
  7. Maintain cargo liability insurance per wagon.
  8. -
-
- - {{> article5_pricing}} - -
-

Article 4: Force Majeure

-

Neither party is liable for delays due to force majeure interpreted under the Ethiopian Civil Code.

-
- -
-

Article 6: Contract Documents

-
    -
  1. Amendments (if any)
  2. -
  3. This Contract Agreement
  4. -
  5. Final Minutes of Negotiation (if any)
  6. -
-
- - {{> signatures_block}} - - diff --git a/apps/edr-freight-api/src/contracts/templates/_partials/article1.hbs b/apps/edr-freight-api/src/contracts/templates/_partials/article1.hbs new file mode 100644 index 000000000..ebdfa744f --- /dev/null +++ b/apps/edr-freight-api/src/contracts/templates/_partials/article1.hbs @@ -0,0 +1,12 @@ +
+

Article 1: Objective and Scope of Services

+

Objective: {{template.article1.objective}}

+ {{#if template.article1.scope.length}} +

Scope:

+
    + {{#each template.article1.scope}} +
  1. {{this}}
  2. + {{/each}} +
+ {{/if}} +
diff --git a/apps/edr-freight-api/src/contracts/templates/_partials/articles_obligations.hbs b/apps/edr-freight-api/src/contracts/templates/_partials/articles_obligations.hbs new file mode 100644 index 000000000..e48e1e453 --- /dev/null +++ b/apps/edr-freight-api/src/contracts/templates/_partials/articles_obligations.hbs @@ -0,0 +1,17 @@ +
+

Article 2: Obligations of the Client (summary)

+
    + {{#each template.clientObligations}} +
  1. {{this}}
  2. + {{/each}} +
+
+ +
+

Article 3: Obligations of the Service Provider (summary)

+
    + {{#each template.providerObligations}} +
  1. {{this}}
  2. + {{/each}} +
+
diff --git a/apps/edr-freight-api/src/contracts/templates/_partials/contract_documents.hbs b/apps/edr-freight-api/src/contracts/templates/_partials/contract_documents.hbs new file mode 100644 index 000000000..ed901a808 --- /dev/null +++ b/apps/edr-freight-api/src/contracts/templates/_partials/contract_documents.hbs @@ -0,0 +1,8 @@ +
+

Article 6: Contract Documents

+
    + {{#each template.contractDocuments}} +
  1. {{this}}
  2. + {{/each}} +
+
diff --git a/apps/edr-freight-api/src/contracts/templates/_partials/force_majeure.hbs b/apps/edr-freight-api/src/contracts/templates/_partials/force_majeure.hbs new file mode 100644 index 000000000..6785d1be6 --- /dev/null +++ b/apps/edr-freight-api/src/contracts/templates/_partials/force_majeure.hbs @@ -0,0 +1,4 @@ +
+

Article 4: Force Majeure

+

Neither party is liable for delays due to force majeure interpreted under the Ethiopian Civil Code.

+
diff --git a/apps/edr-freight-api/src/contracts/templates/generic.hbs b/apps/edr-freight-api/src/contracts/templates/generic.hbs index 23a35cb88..7e6069e19 100644 --- a/apps/edr-freight-api/src/contracts/templates/generic.hbs +++ b/apps/edr-freight-api/src/contracts/templates/generic.hbs @@ -14,29 +14,17 @@

This Contract Agreement is made on {{contractDate}}.

-

Between Ethio-Djibouti Standard Gauge Railway Share Company (EDR) (“Service Provider”) and {{client.companyName}} (“Client”) at {{client.companyAddress}}, {{client.companyLocation}}. Phone: {{client.phone}}. Email: {{client.email}}. TIN: {{client.tinNumber}}.

+

Between Ethio-Djibouti Standard Gauge Railway Share Company (EDR), Addis Ababa (“Service Provider”), and {{client.companyName}} at {{client.companyAddress}}, {{client.companyLocation}} (“Client”). Phone {{client.phone}} / {{client.email}}. TIN {{client.tinNumber}}.

Whereas

{{template.whereas}}

Now therefore, the parties agree as follows:

-
-

Article 1: Objective and Scope

-

{{template.article1Objective}}

-
- + {{> article1}} + {{> articles_obligations}} {{> article5_pricing}} - -
-

Article 4: Force Majeure

-

Neither party shall be liable for delays caused by force majeure beyond reasonable control, interpreted per the Ethiopian Civil Code.

-
- -
-

Article 6: Contract Documents

-

This agreement, amendments (if any), and negotiated minutes constitute the contract.

-
- + {{> force_majeure}} + {{> contract_documents}} {{> signatures_block}} diff --git a/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts b/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts index 647e4fb5b..4df7ea4ce 100644 --- a/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts +++ b/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts @@ -4,16 +4,17 @@ export class AddFanNumberToCompanies1749300000000 implements MigrationInterface name = 'AddFanNumberToCompanies1749300000000'; public async up(queryRunner: QueryRunner): Promise { + // fan_number may already exist when CreateCompaniesModule ran with the full schema await queryRunner.query(` ALTER TABLE freight.companies - ADD COLUMN fan_number varchar(16) NULL; + ADD COLUMN IF NOT EXISTS fan_number varchar(16) NULL; `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE freight.companies - DROP COLUMN fan_number; + DROP COLUMN IF EXISTS fan_number; `); } } diff --git a/apps/edr-freight-api/src/migrations/1749500000000-AddCompanyIdToBookings.ts b/apps/edr-freight-api/src/migrations/1749500000000-AddCompanyIdToBookings.ts new file mode 100644 index 000000000..25fbe1806 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1749500000000-AddCompanyIdToBookings.ts @@ -0,0 +1,59 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCompanyIdToBookings1749500000000 implements MigrationInterface { + name = 'AddCompanyIdToBookings1749500000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.bookings + ALTER COLUMN customer_id DROP NOT NULL; + `); + + await queryRunner.query(` + ALTER TABLE freight.bookings + ADD COLUMN IF NOT EXISTS company_id UUID; + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS idx_bookings_company_id + ON freight.bookings(company_id); + `); + + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'FK_bookings_company_id' + ) THEN + ALTER TABLE freight.bookings + ADD CONSTRAINT "FK_bookings_company_id" + FOREIGN KEY (company_id) + REFERENCES freight.companies(id); + END IF; + END $$; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.bookings + DROP CONSTRAINT IF EXISTS "FK_bookings_company_id"; + `); + + await queryRunner.query(` + UPDATE freight.bookings SET customer_id = company_id WHERE customer_id IS NULL AND company_id IS NOT NULL; + `); + + await queryRunner.query(` + ALTER TABLE freight.bookings + ALTER COLUMN customer_id SET NOT NULL; + `); + await queryRunner.query(` + DROP INDEX IF EXISTS freight.idx_bookings_company_id; + `); + await queryRunner.query(` + ALTER TABLE freight.bookings + DROP COLUMN IF EXISTS company_id; + `); + } +} diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts index 1f96176ba..628ea90d3 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts @@ -1,7 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CustomersModule } from '../customers/customers.module'; +// import { CustomersModule } from '../customers/customers.module'; +import { CompaniesModule } from '../companies/companies.module'; import { FilesModule } from '../files/files.module'; import { MinioModule } from '../minio/minio.module'; import { RuleEngineModule } from '../rule-engine/rule-engine.module'; @@ -41,7 +42,8 @@ import { ContractViewModelBuilder } from '../../contracts/contract-view-model.bu ]), FilesModule, MinioModule, - CustomersModule, + CompaniesModule, + // CustomersModule, RuleEngineModule, ], controllers: [BookingsController, PaymentsWebhookController], diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts index 04c8a2fb7..eb7bbd10a 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts @@ -61,7 +61,8 @@ export class BookingsRepository extends BaseRepository { .createQueryBuilder('booking') .leftJoinAndSelect('booking.bookingContainers', 'bc') .leftJoinAndSelect('bc.containerType', 'ct') - .leftJoinAndSelect('booking.customer', 'customer') + .leftJoinAndSelect('booking.company', 'company') + // .leftJoinAndSelect('booking.customer', 'customer') .leftJoinAndSelect('booking.train', 'train') .leftJoinAndSelect('booking.serviceType', 'st') .leftJoinAndSelect('booking.cargoType', 'cargo') @@ -351,7 +352,8 @@ export class BookingsRepository extends BaseRepository { const qb = this.repository .createQueryBuilder('booking') - .leftJoinAndSelect('booking.customer', 'customer') + .leftJoinAndSelect('booking.company', 'company') + // .leftJoinAndSelect('booking.customer', 'customer') .leftJoinAndSelect('booking.cargoType', 'cargo') .leftJoinAndSelect('booking.serviceType', 'serviceType') .where('booking.status IN (:...statuses)', { statuses }); diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index f4351a1e6..89085f144 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -6,7 +6,8 @@ import { } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; -import { CustomersService } from '../customers/customers.service'; +// import { CustomersService } from '../customers/customers.service'; +import { CompaniesService } from '../companies/companies.service'; import { FilesService } from '../files/files.service'; import { MinioService } from '../minio/minio.service'; import { ContainerTypesService } from '../rule-engine/services/container-types.service'; @@ -30,7 +31,8 @@ export class BookingsService { private readonly bookingsRepository: BookingsRepository, private readonly filesService: FilesService, private readonly minioService: MinioService, - private readonly customersService: CustomersService, + // private readonly customersService: CustomersService, + private readonly companiesService: CompaniesService, private readonly ruleEngineService: RuleEngineService, private readonly containerTypesService: ContainerTypesService, private readonly consolidationService: ConsolidationService, @@ -154,15 +156,26 @@ export class BookingsService { ): Promise<{ booking: Booking; warnings: string[] }> { const warnings: string[] = []; - let customerId = dto.customerId; - if (!customerId) { + // let customerId = dto.customerId; + // if (!customerId) { + // if (!userId) { + // throw new BadRequestException( + // 'customerId is required or must be resolvable from auth token', + // ); + // } + // const customer = await this.customersService.findByUserId(userId); + // customerId = customer.id; + // } + + let companyId = dto.companyId; + if (!companyId) { if (!userId) { throw new BadRequestException( - 'customerId is required or must be resolvable from auth token', + 'companyId is required or must be resolvable from auth token', ); } - const customer = await this.customersService.findByUserId(userId); - customerId = customer.id; + const { company } = await this.companiesService.getCompanyInfoByUserId(userId); + companyId = company.id; } const reference = dto.reference || (await this.generateReference()); @@ -196,7 +209,7 @@ export class BookingsService { const booking = await this.bookingsRepository.create({ reference, - customerId, + companyId, trainId: dto.trainId, contractType: dto.contractType, previousContractId: dto.previousContractId, @@ -375,7 +388,8 @@ export class BookingsService { const where: Record = {}; if (filter.status) where.status = filter.status; - if (filter.customerId) where.customerId = filter.customerId; + // if (filter.customerId) where.customerId = filter.customerId; + if (filter.companyId) where.companyId = filter.companyId; if (filter.contractType) where.contractType = filter.contractType; if (filter.serviceTypeId) where.serviceTypeId = filter.serviceTypeId; if (filter.cargoTypeId) where.cargoTypeId = filter.cargoTypeId; @@ -399,7 +413,8 @@ export class BookingsService { skip: (page - 1) * pageSize, take: pageSize, order: { [sortField]: sortDir }, - relations: ['customer', 'originYard', 'destinationYard', 'serviceType'], + relations: ['company', 'originYard', 'destinationYard', 'serviceType'], + // relations: ['customer', 'originYard', 'destinationYard', 'serviceType'], }); return { items, total }; } diff --git a/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts b/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts index 30cfd9fab..8089d0307 100644 --- a/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts +++ b/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts @@ -61,10 +61,15 @@ export class CreateBookingDto { @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) reference?: string; - @ApiPropertyOptional({ format: 'uuid', description: 'Admin only: target customer' }) + // @ApiPropertyOptional({ format: 'uuid', description: 'Admin only: target customer (legacy)' }) + // @IsOptional() + // @IsUUID() + // customerId?: string; + + @ApiPropertyOptional({ format: 'uuid', description: 'Admin only: target company' }) @IsOptional() @IsUUID() - customerId?: string; + companyId?: string; @ApiPropertyOptional({ format: 'uuid' }) @IsOptional() diff --git a/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts b/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts index cdf4a6b18..50165ac00 100644 --- a/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts +++ b/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts @@ -14,10 +14,15 @@ export class FilterBookingDto { @IsIn([...BOOKING_STATUSES]) status?: string; + // @ApiPropertyOptional({ format: 'uuid' }) + // @IsOptional() + // @IsUUID() + // customerId?: string; + @ApiPropertyOptional({ format: 'uuid' }) @IsOptional() @IsUUID() - customerId?: string; + companyId?: string; @ApiPropertyOptional() @IsOptional() diff --git a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts index eaec84e82..fb3679a60 100644 --- a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts +++ b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts @@ -1,6 +1,7 @@ import { BaseEntity } from '@edr/api-common'; import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; -import { Customer } from '../../customers/entities/customer.entity'; +// import { Customer } from '../../customers/entities/customer.entity'; +import { Company } from '../../companies/entities/company.entity'; import { CargoType } from '../../rule-engine/entities/cargo-type.entity'; import { ServiceType } from '../../rule-engine/entities/service-type.entity'; import { ShippingLine } from '../../rule-engine/entities/shipping-line.entity'; @@ -60,12 +61,19 @@ export class Booking extends BaseEntity { @Column({ name: 'reference', type: 'varchar', length: 64, unique: true }) reference!: string; - @Column({ name: 'customer_id', type: 'uuid' }) - customerId!: string; + // Legacy — superseded by companyId (column kept in DB) + // @Column({ name: 'customer_id', type: 'uuid' }) + // customerId!: string; + // @ManyToOne(() => Customer) + // @JoinColumn({ name: 'customer_id' }) + // customer?: Customer; - @ManyToOne(() => Customer) - @JoinColumn({ name: 'customer_id' }) - customer?: Customer; + @Column({ name: 'company_id', type: 'uuid' }) + companyId!: string; + + @ManyToOne(() => Company) + @JoinColumn({ name: 'company_id' }) + company?: Company; @Column({ name: 'train_id', type: 'uuid', nullable: true }) trainId?: string | null; diff --git a/apps/edr-freight-web/backoffice/src/features/bookings/mapBookingListRow.ts b/apps/edr-freight-web/backoffice/src/features/bookings/mapBookingListRow.ts index eaf7421f4..6ad2a0cc7 100644 --- a/apps/edr-freight-web/backoffice/src/features/bookings/mapBookingListRow.ts +++ b/apps/edr-freight-web/backoffice/src/features/bookings/mapBookingListRow.ts @@ -18,7 +18,8 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow { return { id: booking.id, reference: booking.reference, - customerLabel: labelFromRef(booking.customer, booking.customerId), + customerLabel: labelFromRef(booking.company, booking.companyId), + // customerLabel: labelFromRef(booking.customer, booking.customerId), status: booking.status, scheduledDate: booking.scheduledDate, totalAmount: Number(booking.totalAmount), diff --git a/apps/edr-freight-web/backoffice/src/services/bookings.service.ts b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts index fffcdea44..4dd64823d 100644 --- a/apps/edr-freight-web/backoffice/src/services/bookings.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts @@ -7,7 +7,8 @@ const B = URL_CONSTANTS.BOOKINGS; export interface BookingListFilter { status?: string; - customerId?: string; + // customerId?: string; + companyId?: string; freightType?: string; tradeDirection?: string; paymentCurrency?: string; diff --git a/apps/edr-freight-web/backoffice/src/types/booking.ts b/apps/edr-freight-web/backoffice/src/types/booking.ts index caf98f689..9cdd4a42e 100644 --- a/apps/edr-freight-web/backoffice/src/types/booking.ts +++ b/apps/edr-freight-web/backoffice/src/types/booking.ts @@ -70,7 +70,8 @@ export interface BookingFile { export interface BookingDetail { id: string; reference: string; - customerId: string; + // customerId: string; + companyId: string; status: BookingStatus; scheduledDate: string; totalAmount: number; @@ -91,7 +92,8 @@ export interface BookingDetail { latestChangeRequestNote?: string | null; createdAt: string; updatedAt: string; - customer?: BookingNamedRef & { companyName?: string }; + // customer?: BookingNamedRef & { companyName?: string }; + company?: BookingNamedRef; originYard?: BookingNamedRef; destinationYard?: BookingNamedRef; serviceType?: BookingNamedRef & { code?: string }; diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index 4a1c53fef..d69258e0d 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -13,6 +13,7 @@ import { } from "lucide-react"; import { Button } from "@edr/ui-common"; import { api } from "@/services/api"; +import { Freight } from "@edr/types"; import type { CreateBookingPayload } from "@/services/bookings.service"; import { BookingFormInputValues, @@ -120,7 +121,6 @@ export default function NewBookingPage() { const group = cargoTree.find( (g) => g.code === "CONTAINER" || /container/i.test(g.name), ); - console.log(group, cargoTree); return group?.id ?? ""; }; @@ -132,14 +132,14 @@ export default function NewBookingPage() { return ""; }; - const cargoTypeId = cargoTree[0].id; - // data.cargoType === "container" - // ? findContainerCargoTypeId() - // : (findCargoTypeId( - // data.freightType === "bulk" - // ? data.bulkCommodity - // : data.breakBulkType, - // ) ?? ""); + const cargoTypeId = + data.cargoType === "container" + ? findContainerCargoTypeId() + : (findCargoTypeId( + data.freightType === "bulk" + ? data.bulkCommodity + : data.breakBulkType, + ) ?? ""); const cargoFreeText = data.cargoType === "container" @@ -168,7 +168,11 @@ export default function NewBookingPage() { : direction === "domestic" ? "DOMESTIC" : "IMPORT", - cargoTypeId, + freightType: + data.cargoType === "container" + ? Freight.FreightType.Container + : Freight.FreightType.Bulk, + cargoTypeId: data.cargoType === "container" ? undefined : cargoTypeId, cargoTotalWeightVgm: totalWeight, isHazardous: data.isHazardous, paymentCurrency: "USD", @@ -181,7 +185,7 @@ export default function NewBookingPage() { vgmPerUnitTons: Number(c.vgm || 0), })) : [], - ...(customer ? { customerId: customer.id } : {}), + ...(customer?.company?.id ? { companyId: customer.company.id } : {}), ...(data.previousContractRef ? { previousContractId: data.previousContractRef } : {}), diff --git a/packages/types/src/freight/index.ts b/packages/types/src/freight/index.ts index 85c2f1a3b..c6a627de2 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -288,6 +288,7 @@ export interface CreateBookingContainerDto { export interface CreateBookingDto { reference?: string; customerId?: string; + companyId?: string; trainId?: string; scheduledDate: string; contractType: "NEW" | "RENEWAL";