import { Injectable, NotFoundException } from '@nestjs/common'; import { BookingsRepository } from '../modules/bookings/bookings.repository'; import { Booking } from '../modules/bookings/entities/booking.entity'; import { BookingContractSignature, ContractSignerRole, } from '../modules/bookings/entities/booking-contract-signature.entity'; import { ContractPricingScheduleBuilder, PricingSchedule } from './contract-pricing-schedule.builder'; import { ContractTemplateResolver } from './contract-template.resolver'; import { ContractTemplateMeta, getTemplateMeta } from './contract-template.registry'; export interface ContractSignatureView { role: ContractSignerRole; signerDisplayName: string; signedAt: string; signatureImageUrl?: string | null; } export interface ContractViewModel { bookingId: string; reference: string; status: string; templateKey: string; template: ContractTemplateMeta; contractDate: string; contractYear: number; client: { companyName: string; companyAddress: string; companyLocation: string; phone: string; email: string; tinNumber: string; vatNumber: string; fanNumber: string; businessLicense: string; }; provider: { name: string; address: string; phone: string; email: string; tinNumber: string; }; schedule: { originLabel: string; destinationLabel: string; tradeDirection: string; freightType: string; serviceType: string; scheduledDate: string; contractType: string; cargoDescription: string; totalWeightVgm: string; equipmentReturn: string; hazardousLabel: string; firstMilePickupAddress: string; lastMileDeliveryAddress: string; }; pricing: PricingSchedule; signatures: ContractSignatureView[]; canSignCustomer: boolean; canSignStaff: boolean; hasContractDocument: boolean; hasCustomerSignature: boolean; hasStaffSignature: boolean; } @Injectable() export class ContractViewModelBuilder { constructor( private readonly bookingsRepository: BookingsRepository, private readonly templateResolver: ContractTemplateResolver, private readonly pricingBuilder: ContractPricingScheduleBuilder, ) {} async build(bookingId: string): Promise<{ booking: Booking; view: ContractViewModel }> { const booking = await this.bookingsRepository.findByIdWithFiles(bookingId); if (!booking) { throw new NotFoundException(`Booking ${bookingId} not found`); } const templateKey = booking.contractTemplateKey ?? this.templateResolver.resolve(booking); const template = getTemplateMeta(templateKey); const pricing = await this.pricingBuilder.build(booking); const signatures = await this.loadSignatures(bookingId); const hasCustomer = signatures.some((s) => s.role === 'CUSTOMER'); const hasStaff = signatures.some((s) => s.role === 'STAFF'); const hasContractFile = Boolean( booking.files?.some((f) => f.code === 'contract'), ); const view: ContractViewModel = { bookingId: booking.id, reference: booking.reference, status: booking.status, templateKey, template, contractDate: new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric', }), contractYear: new Date().getFullYear(), client: { companyName: booking.company?.name ?? 'Client', companyAddress: this.valueOrDash(booking.company?.address), companyLocation: this.valueOrDash(booking.company?.country), phone: this.valueOrDash(booking.company?.phone), email: this.valueOrDash(booking.company?.email), tinNumber: this.valueOrDash(booking.company?.tin), vatNumber: this.valueOrDash(booking.company?.vatNumber), fanNumber: this.valueOrDash(booking.company?.fanNumber), businessLicense: this.valueOrDash( booking.company?.companyProfiles?.[0]?.businessLicense, ), }, provider: { name: 'Ethio-Djibouti Standard Gauge Railway Share Company', address: 'Addis Ababa, Ethiopia', phone: '+251 11 872 0000', email: 'info@edr.gov.et', tinNumber: '—', }, schedule: this.buildSchedule(booking), pricing, signatures, canSignCustomer: booking.status === 'CONTRACT_READY' && !hasCustomer, canSignStaff: booking.status === 'SIGNED_CUSTOMER' && hasCustomer && !hasStaff, hasContractDocument: hasContractFile, hasCustomerSignature: hasCustomer, hasStaffSignature: hasStaff, }; return { booking, view }; } private async loadSignatures(bookingId: string): Promise { const rows = await this.bookingsRepository.findContractSignatures(bookingId); return rows.map((s) => this.toSignatureView(s)); } toSignatureView(row: BookingContractSignature): ContractSignatureView { return { role: row.signerRole, signerDisplayName: row.signerDisplayName, signedAt: this.formatDate(row.signedAt), signatureImageUrl: row.signatureFile?.url ?? null, }; } private buildSchedule(booking: Booking): ContractViewModel['schedule'] { const cargoName = booking.freightType === 'BULK' ? booking.cargoFreeText || booking.cargoType?.cargoTypeName || 'Bulk commodity' : booking.cargoType?.cargoTypeName || 'Container cargo'; const totalWeight = Number(booking.cargoTotalWeightVgm || 0); return { originLabel: this.yardLabel(booking.originYard), destinationLabel: this.yardLabel(booking.destinationYard), tradeDirection: this.valueOrDash(booking.tradeDirection), freightType: this.valueOrDash(booking.freightType), serviceType: this.valueOrDash( booking.serviceType?.serviceName ?? booking.serviceType?.code, ), scheduledDate: this.formatDate(booking.scheduledDate), contractType: this.valueOrDash(booking.contractType), cargoDescription: this.valueOrDash(cargoName), totalWeightVgm: totalWeight > 0 ? `${totalWeight.toLocaleString()} tons` : '—', equipmentReturn: this.valueOrDash(booking.equipmentReturn), hazardousLabel: booking.isHazardous ? 'Yes' : 'No', firstMilePickupAddress: this.valueOrDash( booking.firstMilePickupAddress, ), lastMileDeliveryAddress: this.valueOrDash( booking.lastMileDeliveryAddress, ), }; } private yardLabel(yard?: { label?: string; code?: string } | null): string { return this.valueOrDash(yard?.label ?? yard?.code); } private formatDate(value?: Date | string | null): string { if (!value) return '—'; const date = value instanceof Date ? value : new Date(value); if (Number.isNaN(date.getTime())) return '—'; return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric', }); } private valueOrDash(value?: string | number | null): string { if (value === undefined || value === null || value === '') return '—'; return String(value); } }