mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 15:30:56 +00:00
210 lines
7.0 KiB
TypeScript
210 lines
7.0 KiB
TypeScript
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<ContractSignatureView[]> {
|
|
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);
|
|
}
|
|
}
|