mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
add GL operations for customs risk assignment, duty advising, and incident reporting
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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<string, string> = {
|
||||
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<Booking> {
|
||||
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<Booking> {
|
||||
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<ClearanceIncident> {
|
||||
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<ClearanceIncident[]> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user