diff --git a/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts index 1cef9528c..3809dc086 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts @@ -157,13 +157,13 @@ export class BookingLifecycleNotifierService { }); } - /** Clearance finalized → customer can proceed to request operation. */ + /** Document approval finalized → customer can proceed to request operation. */ clearanceReady(b: Booking): void { const msg = - `Clearance for booking ${b.reference} is complete. ` + + `Document approval for booking ${b.reference} is finalized. ` + `You can now proceed to request operation from the portal.`; - void this.notifyContact(b, msg, 'CLEARANCE READY'); - this.inApp(b, 'Clearance complete', msg, { + void this.notifyContact(b, msg, 'DOCUMENT APPROVAL FINALIZED'); + this.inApp(b, 'Document approval finalized', msg, { type: NotificationType.CLEARANCE_DECISION, }); } diff --git a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts index d752d071a..029952f45 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts @@ -456,7 +456,7 @@ export class ContractClearanceService { const allowed = ['CLEARANCE_UNDER_REVIEW', 'AWAITING_CLEARANCE_DOCUMENTS']; if (!allowed.includes(contract.status)) { throw new ConflictException( - `Cannot finalize clearance on status "${contract.status}".`, + `Cannot finalize document approval on status "${contract.status}".`, ); } } diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts index 224745cc4..c98e24b19 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts @@ -374,23 +374,18 @@ export class ContractsService { if (companyProfileId) { // Business-license files are FileRecords (resource "company_profiles"); // carry the live ones by reference. Staged/pending uploads are excluded by - // code. Codes are slugged from each document name so they group under - // "Profile documents" on the contract detail page. + // code. The `business_license` prefix is preserved so the portal groups + // them under "Business license" instead of the clearance catch-all — the + // index suffix keeps multiple licences distinct. const records = await this.filesService.findByResource( companyProfileId, 'company_profiles', ); - const slug = (name: string) => - name - .toLowerCase() - .replace(/\.[a-z0-9]+$/, '') - .replace(/[^a-z0-9]+/g, '_') - .replace(/^_+|_+$/g, '') || 'profile_document'; records .filter((r) => r.code === 'business_license') .forEach((r, i) => { - const code = `${slug(r.name)}_${i + 1}`; + const code = `business_license_${i + 1}`; if (existingCodes.has(code)) return; docs.push({ code, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index 4a836060e..415b5df97 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -3444,19 +3444,22 @@ export class BookingBatchService implements OnModuleInit { /** * Physical wagons marshalled in the schedule's built train, or null when the - * schedule has no built train (or the consist is still empty) and the legacy - * locomotive-derived capacity must apply. This count is what caps a built - * train's bookings: 50 wagons coupled → 50 wagon slots, no more. + * schedule has NO built train and the legacy locomotive-derived capacity must + * apply. This count is what caps a built train's bookings: 50 wagons coupled + * → 50 wagon slots, no more. + * + * A built train with an EMPTY consist returns 0, NOT null: zero coupled + * wagons means zero capacity. Folding that case into null used to hand an + * un-consisted train the abstract locomotive budget, so an empty train + * advertised its full maxWagons as free space and accepted bookings the + * allocator could never place. */ private async builtTrainWagonCount( schedule: TrainSchedule, ): Promise { const trainId = schedule.trainSet?.train?.id; if (!trainId) return null; - const count = await this.dataSource - .getRepository(Wagon) - .count({ where: { trainId } }); - return count > 0 ? count : null; + return this.dataSource.getRepository(Wagon).count({ where: { trainId } }); } /** diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts index 9d426077e..2f7ca0776 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts @@ -7131,9 +7131,19 @@ export class TrainSchedulingService { await this.dataSource.transaction(async (manager) => { await manager.getRepository(TrainSetWagon).delete(trainSetWagonId); + // Recount from the slot rows rather than decrementing the cached counter. + // A blind `wagonCount - 1` desyncs the moment two removals race or the + // in-memory schedule graph is stale, and the counter is what the schedule + // capacity math reads. + const remaining = await manager.getRepository(TrainSetWagon).find({ + where: { trainSetId: schedule.trainSetId }, + select: { id: true, lengthMeters: true }, + }); await manager.getRepository(TrainSet).update(schedule.trainSetId, { - wagonCount: Math.max(0, (schedule.trainSet?.wagonCount ?? 0) - 1), - totalLengthMeters: Math.max(0, (schedule.trainSet?.totalLengthMeters ?? 0) - (wagon.lengthMeters ?? 0)), + wagonCount: remaining.length, + totalLengthMeters: roundTons( + remaining.reduce((sum, w) => sum + (Number(w.lengthMeters) || 0), 0), + ), }); }); diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx index 3dcf1ea9e..501e074e2 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/ClearanceReviewSection.tsx @@ -135,12 +135,14 @@ export function ClearanceReviewSection({ const finalizeMutation = useMutation({ mutationFn: () => bookingsService.finalizeClearance(bookingId), onSuccess: () => { - toast.success("Clearance finalized"); + toast.success("Document approval finalized"); refresh(); }, onError: (e) => toast.error( - e instanceof Error ? e.message : "Could not finalize clearance", + e instanceof Error + ? e.message + : "Could not finalize document approval", ), }); @@ -366,7 +368,7 @@ export function ClearanceReviewSection({ }> {finalizeMutation.error instanceof Error ? finalizeMutation.error.message - : "Could not finalize clearance."} + : "Could not finalize document approval."} )} @@ -445,7 +447,7 @@ export function ClearanceReviewSection({ loading={finalizeMutation.isPending} onClick={() => finalizeMutation.mutate()} > - Finalize clearance + Finalize document approval diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx index c85dc0b07..c7d668ade 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx @@ -68,7 +68,7 @@ export interface ContractClearanceReviewSectionProps { queriesLocked?: boolean; /** * ONE_TIME customs contracts use the phased milestone workflow. Hides the - * legacy "Finalize clearance" shortcut; booking readiness follows delivery + * legacy "Finalize document approval" shortcut; booking readiness follows delivery * order (import) or export release. */ phasedCustoms?: boolean; @@ -393,7 +393,7 @@ export function ContractClearanceReviewSection({ }> {finalizeClearance.error instanceof Error ? finalizeClearance.error.message - : "Could not finalize clearance."} + : "Could not finalize document approval."} )} @@ -482,7 +482,7 @@ export function ContractClearanceReviewSection({ }) } > - Finalize clearance + Finalize document approval diff --git a/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx index 05b4bd546..925ff528d 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainBuilder/BuildTrainModal.tsx @@ -160,7 +160,8 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain onChange={(value) => setImportTrainNumber(value ?? "")} searchable clearable - nothingFoundMessage="No free run numbers — add more in Dropdown Settings" + nothingFoundMessage={importNumbers.emptyMessage} + error={importNumbers.settingMissing ? importNumbers.emptyMessage : undefined} />