mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
feat(bookings): two-level clearance charges (port + misc) billed to customer with invoices
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
buildClearanceDocHistory,
|
||||
type ClearanceDocEvent,
|
||||
} from './clearance-doc-history.util';
|
||||
import { ClearanceEventService } from './clearance-event.service';
|
||||
import { computeNextStep, type BookingNextStep } from './booking-next-step.util';
|
||||
import { SubmitBookingResponseDto } from './dto/submit-booking-response.dto';
|
||||
import { PriceLineItemDto } from './dto/generate-price-response.dto';
|
||||
@@ -72,6 +73,7 @@ export class BookingTransitionService {
|
||||
private readonly invoiceService: BookingInvoiceService,
|
||||
private readonly containerValidationService: ContainerValidationService,
|
||||
private readonly notifier: BookingLifecycleNotifierService,
|
||||
private readonly clearanceEvents: ClearanceEventService,
|
||||
private readonly events: EventEmitter2,
|
||||
@Optional() private readonly milestoneService?: ClearanceMilestoneService,
|
||||
// Optional + last so the hand-constructed service in *.spec.ts files keeps
|
||||
@@ -795,6 +797,7 @@ export class BookingTransitionService {
|
||||
async submitClearanceDocuments(
|
||||
bookingId: string,
|
||||
files: Express.Multer.File[],
|
||||
userId?: string,
|
||||
): Promise<Booking> {
|
||||
const booking = await this.bookingsService.findById(bookingId);
|
||||
assertBookingStatus(booking, [
|
||||
@@ -853,6 +856,16 @@ export class BookingTransitionService {
|
||||
} as never);
|
||||
}
|
||||
|
||||
const fileKeys = files.map((f) => f.fieldname);
|
||||
await this.clearanceEvents.record({
|
||||
bookingId,
|
||||
action: 'DOCS_SUBMITTED',
|
||||
label: `Customer submitted ${files.length} clearance document(s): ${fileKeys.join(', ')}`,
|
||||
actorType: 'CUSTOMER',
|
||||
actorId: userId ?? null,
|
||||
metadata: { fileKeys },
|
||||
});
|
||||
|
||||
const fresh = await this.bookingsService.findById(bookingId);
|
||||
this.notifier.clearanceDocsUploadedToStaff(fresh);
|
||||
return fresh;
|
||||
@@ -940,6 +953,16 @@ export class BookingTransitionService {
|
||||
staffId,
|
||||
note,
|
||||
);
|
||||
await this.clearanceEvents.record({
|
||||
bookingId,
|
||||
action: status === 'APPROVED' ? 'DOC_APPROVED' : 'DOC_QUERIED',
|
||||
label:
|
||||
status === 'APPROVED'
|
||||
? `Approved document "${fileKey.replace(/_/g, ' ')}"`
|
||||
: `Opened query on document "${fileKey.replace(/_/g, ' ')}"`,
|
||||
actorId: staffId,
|
||||
metadata: { fileKey, note: note ?? null },
|
||||
});
|
||||
if (status === "QUERIED") {
|
||||
await this.bookingsRepository.createReviewNote(
|
||||
bookingId,
|
||||
@@ -980,6 +1003,7 @@ export class BookingTransitionService {
|
||||
async uploadClearanceOutputDocuments(
|
||||
bookingId: string,
|
||||
files: Express.Multer.File[],
|
||||
userId?: string,
|
||||
): Promise<Booking> {
|
||||
const booking = await this.bookingsService.findById(bookingId);
|
||||
assertBookingStatus(booking, ["DOCUMENTS_UNDER_REVIEW"]);
|
||||
@@ -1000,6 +1024,15 @@ export class BookingTransitionService {
|
||||
file,
|
||||
});
|
||||
}
|
||||
await this.clearanceEvents.record({
|
||||
bookingId,
|
||||
action: 'OUTPUT_DOCS_UPLOADED',
|
||||
label: `Uploaded customs output document(s): ${files
|
||||
.map((f) => f.fieldname.replace(/_/g, ' '))
|
||||
.join(', ')}`,
|
||||
actorId: userId ?? null,
|
||||
metadata: { fileKeys: files.map((f) => f.fieldname) },
|
||||
});
|
||||
return this.bookingsService.findById(bookingId);
|
||||
}
|
||||
|
||||
@@ -1007,7 +1040,7 @@ export class BookingTransitionService {
|
||||
* GL confirms clearance: requires every customer document APPROVED (100% gate)
|
||||
* and, for customs, the required output documents present → CLEARANCE_READY.
|
||||
*/
|
||||
async finalizeClearance(bookingId: string): Promise<Booking> {
|
||||
async finalizeClearance(bookingId: string, userId?: string): Promise<Booking> {
|
||||
const booking = await this.bookingsService.findById(bookingId);
|
||||
if (this.isPhasedCustoms(booking)) {
|
||||
throw new BadRequestException(
|
||||
@@ -1063,6 +1096,12 @@ export class BookingTransitionService {
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
status: "CLEARANCE_READY",
|
||||
} as never);
|
||||
await this.clearanceEvents.record({
|
||||
bookingId,
|
||||
action: 'CLEARANCE_FINALIZED',
|
||||
label: 'Finalized document approval — clearance ready',
|
||||
actorId: userId ?? null,
|
||||
});
|
||||
const fresh = await this.bookingsService.findById(bookingId);
|
||||
this.notifier.clearanceReady(fresh);
|
||||
return fresh;
|
||||
@@ -1089,6 +1128,8 @@ export class BookingTransitionService {
|
||||
* the customer pools, so the gate here would wrongly reject them).
|
||||
*/
|
||||
bypassDayPool?: boolean;
|
||||
/** Acting user, recorded in the clearance history. */
|
||||
userId?: string;
|
||||
},
|
||||
): Promise<Booking> {
|
||||
const booking = await this.bookingsService.findById(bookingId);
|
||||
@@ -1203,6 +1244,14 @@ export class BookingTransitionService {
|
||||
scheduledDate: date,
|
||||
requestedTrainScheduleId: requestedId,
|
||||
} as never);
|
||||
await this.clearanceEvents.record({
|
||||
bookingId,
|
||||
action: 'OPERATION_REQUESTED',
|
||||
label: `Requested operation for shipment day ${scheduledDate}`,
|
||||
actorType: 'CUSTOMER',
|
||||
actorId: opts?.userId ?? null,
|
||||
metadata: { scheduledDate },
|
||||
});
|
||||
const fresh = await this.bookingsService.findById(bookingId);
|
||||
this.notifier.operationRequestedToStaff(fresh);
|
||||
return fresh;
|
||||
|
||||
Reference in New Issue
Block a user