import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { FilesService } from '../files/files.service'; import { Booking } from '../bookings/entities/booking.entity'; import { ClearanceIncident, IncidentType, } from './entities/clearance-incident.entity'; import { ClearanceMilestoneService } from './clearance-milestone.service'; /** * Maps a GL post-booking document `code` to the milestone it auto-completes when * uploaded (doc §11.3/§12.2 — doc-triggered milestones). Uploading the document * marks the milestone done so the timeline advances without a separate click. */ const DOC_CODE_TO_MILESTONE: Record = { release_order: 'RELEASE_ORDER_SECURED', // export — GL DJ delivery_order: 'DO_COLLECTED', // import — GL DJ t1_transport_document: 'T1_CLOSED', // import — GL ET import_release: 'IMPORT_RELEASE_GRANTED', // import — GL ET full_in_interchange: 'OFFLOADED', // export — GL DJ final_declaration: 'IMPORT_PROCESS_COMPLETED', // import — GL ET }; /** * Operational Global Logistics actions that hang off a shipment booking after GL * creates it: station routing, damage/incident reporting, and the phased GL * document uploads (Release Order, Delivery Order, T1, etc.) that advance * doc-triggered milestones. See docs/new-doc.md §11–§13, gap matrix #14/#16/#18. */ @Injectable() export class GlOperationsService { constructor( private readonly dataSource: DataSource, private readonly filesService: FilesService, private readonly milestoneService: ClearanceMilestoneService, ) {} private get bookings() { return this.dataSource.getRepository(Booking); } private get incidents() { return this.dataSource.getRepository(ClearanceIncident); } private async getBooking(bookingId: string): Promise { const booking = await this.bookings.findOne({ where: { id: bookingId } }); if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`); return booking; } /** * Route a shipment to an origin station and (optionally) bind a GL staff user * to it (GL US-02). Setting both moves the shipment to that station's queue. */ async assignStation( bookingId: string, input: { stationYardId: string; staffId?: string }, ): Promise { const booking = await this.getBooking(bookingId); booking.glStationYardId = input.stationYardId; if (input.staffId) { booking.glAssignedStaffId = input.staffId; booking.glAssignedAt = new Date(); } return this.bookings.save(booking); } /** Log a cargo exception (seal broken, container damaged, etc.) with photos. */ async reportIncident( bookingId: string, input: { incidentType: IncidentType; description: string; files: Express.Multer.File[]; }, userId?: string, ): Promise { await this.getBooking(bookingId); if (!input.description?.trim()) { throw new BadRequestException('A description is required for an incident report.'); } const photoFileIds: string[] = []; for (const file of input.files ?? []) { const record = await this.filesService.upload({ resourceId: bookingId, resource: 'bookings', code: 'incident_photo', file, }); photoFileIds.push(record.id); } const incident = this.incidents.create({ bookingId, incidentType: input.incidentType, description: input.description.trim(), photoFileIds, reportedByUserId: userId ?? null, reportedAt: new Date(), }); return this.incidents.save(incident); } async listIncidents(bookingId: string): Promise { return this.incidents.find({ where: { bookingId }, order: { reportedAt: 'DESC' }, }); } /** * Customer uploads the duty/tax payment slip after GL advised the amount. The * slip attaches to the booking and doc-triggers DUTY_TAX_PAID (§11.3 #7). */ async uploadDutySlip( bookingId: string, file: Express.Multer.File, ): Promise<{ milestoneCompleted: boolean }> { await this.getBooking(bookingId); if (!file) throw new BadRequestException('No payment slip uploaded'); await this.filesService.upsertByCode({ resourceId: bookingId, resource: 'bookings', code: 'duty_tax_receipt', file, }); await this.milestoneService.completeByDocTrigger({ bookingId }, 'DUTY_TAX_PAID'); return { milestoneCompleted: true }; } /** * GL uploads a post-booking operational document (DO, RO, T1, import release, * interchange…). The file attaches to the booking; if the code maps to a * doc-triggered milestone, that milestone auto-completes. */ async uploadDocuments( bookingId: string, files: Express.Multer.File[], ): Promise<{ uploaded: number; completedMilestones: string[] }> { await this.getBooking(bookingId); if (!files?.length) throw new BadRequestException('No documents uploaded'); const completedMilestones: string[] = []; for (const file of files) { await this.filesService.upsertByCode({ resourceId: bookingId, resource: 'bookings', code: file.fieldname, file, }); const milestoneCode = DOC_CODE_TO_MILESTONE[file.fieldname]; if (milestoneCode) { await this.milestoneService.completeByDocTrigger({ bookingId }, milestoneCode); completedMilestones.push(milestoneCode); } } return { uploaded: files.length, completedMilestones }; } }