diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index a81a539ba..f40e4f40f 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -59,7 +59,6 @@ import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-k import { DemoFreightDataSeeder } from "./seed/demo-freight-data.seeder"; import { GovCompaniesSeeder } from "./seed/gov-companies.seeder"; import { ApprovedFirstLastMileDemoBookingsSeeder } from "./seed/approved-first-lastmile-demo-bookings.seeder"; -import { PaidIndodeDemoBookingsSeeder } from "./seed/paid-indode-demo-bookings.seeder"; //New Trains, Wagons, Container and Cargo management modules import { TrainsModule } from "./modules/trains/trains.module"; import { WagonsModule } from './modules/wagons/wagons.module'; @@ -161,7 +160,6 @@ import { ImportOperationsModule } from './modules/import-operations/import-opera ExportDjiboutiInterchangeDemoSeeder, MarshallingDemoTrainsSeeder, ApprovedFirstLastMileDemoBookingsSeeder, - PaidIndodeDemoBookingsSeeder, ], }) export class AppModule implements OnApplicationBootstrap { @@ -180,7 +178,6 @@ export class AppModule implements OnApplicationBootstrap { private readonly warehouseDemoSeeder: WarehouseDemoSeeder, private readonly exportDjiboutiInterchangeDemoSeeder: ExportDjiboutiInterchangeDemoSeeder, private readonly marshallingDemoTrainsSeeder: MarshallingDemoTrainsSeeder, - private readonly paidIndodeDemoBookingsSeeder: PaidIndodeDemoBookingsSeeder, private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder, private readonly demoFreightDataSeeder: DemoFreightDataSeeder, private readonly govCompaniesSeeder: GovCompaniesSeeder, @@ -202,7 +199,6 @@ export class AppModule implements OnApplicationBootstrap { await this.warehouseDemoSeeder.run(); await this.exportDjiboutiInterchangeDemoSeeder.run(); await this.marshallingDemoTrainsSeeder.run(); - await this.paidIndodeDemoBookingsSeeder.run(); // Idempotent demo data: ≥100 wagons/type, approval chains, 4 staff users. // Each block self-guards on an empty-table check, so this is safe every boot. // Demo data seeds (DemoBookingsSeeder, PricingDataSeeder, diff --git a/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts index 21fef08ea..ddf09dea6 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts @@ -138,7 +138,7 @@ export class BookingInvoiceService { ); return; } - if (booking.paymentStatus === "PAID") return; + // if (booking.paymentStatus === "PAID") return; await this.dataSource.transaction(async (mg) => { await mg.update( diff --git a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts index 910d3f103..4b033bab5 100644 --- a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts +++ b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts @@ -291,7 +291,7 @@ export class Booking extends BaseEntity { @Column({ name: 'origin_yard_id', type: 'uuid' }) originYardId!: string; - @ManyToOne(() => Yard) + @ManyToOne(() => Yard) @JoinColumn({ name: 'origin_yard_id' }) originYard?: Yard; diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts index 7ea818e34..2b0126003 100644 --- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts +++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.spec.ts @@ -65,6 +65,16 @@ function makeService(overrides?: { children: [{ value: '2' }], }), }; + const glOperationsService = { + t1State: jest.fn().mockResolvedValue({ + bookingId: 'b-general', + wagonAllocated: false, + trainDepartedAt: null, + trainArrivedAt: null, + closed: false, + closedAt: null, + }), + }; const service = new BookingClearanceService( bookingsRepository as never, @@ -74,6 +84,7 @@ function makeService(overrides?: { workflowService as never, milestoneService as never, dropdownSettingsService as never, + glOperationsService as never, ); return { diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts index 7bb835df2..4e9a7b69d 100644 --- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { ContractDocPhase } from '@edr/types'; +import { ContractDocPhase, type ClearanceT1State } from '@edr/types'; import { DropdownSettingsService } from '../dropdown-settings/dropdown-settings.service'; import { FileUploadSettingsService } from '../file-upload-settings/file-upload-settings.service'; @@ -11,6 +11,7 @@ import { Booking } from '../bookings/entities/booking.entity'; import { clearanceCodesForBooking } from '../bookings/clearance.util'; import { ClearanceWorkflowService } from './clearance-workflow.service'; import { ClearanceMilestoneService } from './clearance-milestone.service'; +import { GlOperationsService } from './gl-operations.service'; import { AdviseContractDutyDto } from './dto/phased-clearance.dto'; import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue, DJ_BOOKING_QUEUE_STATUSES, persistDeclarationUploads, persistTransitPermitUploads, PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES } from './phased-clearance.util'; @@ -63,6 +64,8 @@ export interface BookingClearanceView { noticeFile?: { id: string; name: string; url: string } | null; } | null; workflowFiles?: ReturnType; + /** Import post-allocation T1 transit document state (null until wagon allocation). */ + t1?: ClearanceT1State | null; } @Injectable() @@ -75,6 +78,7 @@ export class BookingClearanceService { private readonly workflowService: ClearanceWorkflowService, private readonly milestoneService: ClearanceMilestoneService, private readonly dropdownSettingsService: DropdownSettingsService, + private readonly glOperationsService: GlOperationsService, ) {} private async assertPhasedGeneralCustoms(booking: Booking): Promise { @@ -162,6 +166,15 @@ export class BookingClearanceService { booking.tradeDirection ?? 'IMPORT', ); + let t1: ClearanceT1State | null = null; + if ((booking.tradeDirection ?? 'IMPORT') === 'IMPORT') { + try { + t1 = await this.glOperationsService.t1State(bookingId); + } catch { + t1 = null; + } + } + return { bookingId, status: booking.status, @@ -192,6 +205,7 @@ export class BookingClearanceService { preClearanceFinalized: Boolean(booking.preClearanceFinalizedAt), dutyAdvice, workflowFiles, + t1, }; } @@ -421,6 +435,13 @@ export class BookingClearanceService { clearanceCurrentPhase: ContractDocPhase.GlDjCollection, } as never); + // GL Djibouti may have uploaded the DO early (un-gated) — count it now. + const files = await this.filesService.findByResource(bookingId, 'bookings'); + if (files.some((f) => f.code === 'delivery_order')) { + await this.workflowService.completeMilestoneForBooking(bookingId, 'DO_COLLECTED'); + await this.workflowService.markReadyForOperation(bookingId); + } + return this.bookingsService.findById(bookingId); } @@ -434,15 +455,11 @@ export class BookingClearanceService { throw new BadRequestException('Delivery Order applies only to import bookings.'); } - if (!booking.preClearanceFinalizedAt) { - throw new BadRequestException( - 'GL Ethiopia must finalize pre-clearance before the Delivery Order can be uploaded.', - ); - } - - await this.workflowService.assertPriorCompleteForBooking(bookingId, 'IMPORT', 'DO_COLLECTED'); if (!file) throw new BadRequestException('No Delivery Order uploaded'); + // DO upload is deliberately un-gated: GL Djibouti may attach it at any point, + // any file type. The DO_COLLECTED milestone (and operation readiness) still + // waits for GL Ethiopia to finalize pre-clearance so the workflow order holds. await this.filesService.upsertByCode({ resourceId: bookingId, resource: 'bookings', @@ -450,8 +467,10 @@ export class BookingClearanceService { file, }); - await this.workflowService.completeMilestoneForBooking(bookingId, 'DO_COLLECTED', userId); - await this.workflowService.markReadyForOperation(bookingId); + if (booking.preClearanceFinalizedAt) { + await this.workflowService.completeMilestoneForBooking(bookingId, 'DO_COLLECTED', userId); + await this.workflowService.markReadyForOperation(bookingId); + } return this.bookingsService.findById(bookingId); } 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 a09de407c..34e3d3d94 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 @@ -1,5 +1,5 @@ import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; -import { ContractDocPhase } from '@edr/types'; +import { ContractDocPhase, type ClearanceT1State } from '@edr/types'; import { DropdownSettingsService } from '../dropdown-settings/dropdown-settings.service'; import { FileUploadSettingsService } from '../file-upload-settings/file-upload-settings.service'; @@ -10,6 +10,7 @@ import { BookingsService } from '../bookings/bookings.service'; import { contractClearanceCodes } from './contract-clearance.util'; import { ClearanceWorkflowService } from './clearance-workflow.service'; import { ClearanceMilestoneService } from './clearance-milestone.service'; +import { GlOperationsService } from './gl-operations.service'; import { ClearanceMilestone } from './entities/clearance-milestone.entity'; import { Contract } from './entities/contract.entity'; import { ContractDocReviewStatus } from './entities/contract-document-review.entity'; @@ -77,6 +78,8 @@ export interface ContractClearanceView { noticeFile?: { id: string; name: string; url: string } | null; } | null; workflowFiles?: ReturnType; + /** Import post-allocation T1 transit document state (null until a booking is linked). */ + t1?: ClearanceT1State | null; } @Injectable() @@ -90,6 +93,7 @@ export class ContractClearanceService { private readonly workflowService: ClearanceWorkflowService, private readonly milestoneService: ClearanceMilestoneService, private readonly dropdownSettingsService: DropdownSettingsService, + private readonly glOperationsService: GlOperationsService, ) {} private isPhasedCustoms(contract: Contract): boolean { @@ -224,6 +228,15 @@ export class ContractClearanceService { workflowFiles = [...byCode.values()]; } + let t1: ClearanceT1State | null = null; + if (cycle?.bookingId && contract.tradeDirection === 'IMPORT') { + try { + t1 = await this.glOperationsService.t1State(cycle.bookingId); + } catch { + t1 = null; // linked booking missing — view stays usable + } + } + let nextAction = this.workflowService.computeNextAction(contract, cycle, milestones); if (cycle?.bookingId && contract.tradeDirection === 'EXPORT') { const bookingMilestones = await this.workflowService.listMilestonesForBooking( @@ -272,6 +285,7 @@ export class ContractClearanceService { linkedBookingId: cycle?.bookingId ?? null, dutyAdvice, workflowFiles, + t1, }; } @@ -1011,6 +1025,13 @@ export class ContractClearanceService { currentPhase: ContractDocPhase.GlDjCollection, }); + // GL Djibouti may have uploaded the DO early (un-gated) — count it now. + const files = await this.filesService.findByResource(contractId, 'contracts'); + if (files.some((f) => f.code === 'delivery_order')) { + await this.workflowService.completeMilestone(contractId, 'DO_COLLECTED'); + await this.workflowService.markReadyForBooking(contractId); + } + return this.contractsService.findById(contractId); } @@ -1025,17 +1046,11 @@ export class ContractClearanceService { throw new BadRequestException('Delivery Order applies only to import contracts.'); } - const cycle = await this.contractsRepository.currentCycle(contractId); - if (!cycle?.preClearanceFinalizedAt) { - throw new BadRequestException( - 'GL Ethiopia must finalize pre-clearance before the Delivery Order can be uploaded.', - ); - } - - await this.workflowService.assertPriorComplete(contractId, 'IMPORT', 'DO_COLLECTED'); - if (!file) throw new BadRequestException('No Delivery Order uploaded'); + // DO upload is deliberately un-gated: GL Djibouti may attach it at any point, + // any file type. The DO_COLLECTED milestone (and booking readiness) still waits + // for GL Ethiopia to finalize pre-clearance so the workflow order holds. await this.filesService.upsertByCode({ resourceId: contractId, resource: 'contracts', @@ -1043,8 +1058,11 @@ export class ContractClearanceService { file, }); - await this.workflowService.completeMilestone(contractId, 'DO_COLLECTED', userId); - await this.workflowService.markReadyForBooking(contractId); + const cycle = await this.contractsRepository.currentCycle(contractId); + if (cycle?.preClearanceFinalizedAt) { + await this.workflowService.completeMilestone(contractId, 'DO_COLLECTED', userId); + await this.workflowService.markReadyForBooking(contractId); + } return this.contractsService.findById(contractId); } diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts index 5e712a177..872d13a0c 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts @@ -873,6 +873,33 @@ export class ContractsController { return this.glOperationsService.uploadTransportDocument(bookingId, files ?? []); } + @Post('bookings/:bookingId/t1-documents') + @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions) + @UseInterceptors(AnyFilesInterceptor()) + @ApiConsumes('multipart/form-data') + @ApiOperation({ + summary: + 'GL Djibouti uploads T1 transit documents (multi-file) after wagon allocation; locked once the train departs', + }) + uploadT1Documents( + @Param('bookingId', ParseUUIDPipe) bookingId: string, + @UploadedFiles() files: Express.Multer.File[], + ) { + return this.glOperationsService.uploadT1Documents(bookingId, files ?? []); + } + + @Post('bookings/:bookingId/t1-close') + @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions) + @ApiOperation({ + summary: 'GL Ethiopia closes (accepts) the T1 document set after the train arrives', + }) + closeT1( + @Param('bookingId', ParseUUIDPipe) bookingId: string, + @CurrentUser() user: AuthUserPayload, + ) { + return this.glOperationsService.closeT1(bookingId, resolveAuthUserId(user)); + } + @Post('bookings/:bookingId/documents') @BookingStaff(FREIGHT_PERMS.bookings.uploadClearanceOutput) @UseInterceptors(AnyFilesInterceptor()) diff --git a/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts b/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts index 73ca3a65d..f900c564f 100644 --- a/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/gl-operations.service.ts @@ -1,14 +1,19 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import { isT1TransportFileCode, type Freight } from '@edr/types'; import { FilesService } from '../files/files.service'; import { Booking } from '../bookings/entities/booking.entity'; +import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity'; import { ClearanceIncident, IncidentType, } from './entities/clearance-incident.entity'; import { ClearanceMilestoneService } from './clearance-milestone.service'; -import { persistExportTransportUploads } from './phased-clearance.util'; +import { + persistExportTransportUploads, + persistT1TransportUploads, +} from './phased-clearance.util'; /** * Maps a GL post-booking document `code` to the milestone it auto-completes when @@ -18,7 +23,8 @@ import { persistExportTransportUploads } from './phased-clearance.util'; 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 + // t1_transport_document intentionally NOT doc-triggered: T1_CLOSED completes only + // when GL Ethiopia accepts the T1 set after the train arrives (closeT1). import_release: 'IMPORT_RELEASE_GRANTED', // import — GL ET full_in_interchange: 'OFFLOADED', // export — GL DJ final_declaration: 'IMPORT_PROCESS_COMPLETED', // import — GL ET @@ -161,6 +167,114 @@ export class GlOperationsService { return { uploaded: files.length, completedMilestones }; } + /** + * T1 transit-document lifecycle state for an import shipment booking. Wagon + * allocation opens the upload window; train departure locks it; train arrival + * lets GL Ethiopia close (accept) the T1 set. + */ + async t1State(bookingId: string): Promise { + const booking = await this.getBooking(bookingId); + const milestones = await this.milestoneService.listForBooking(bookingId); + + const wagonMilestone = milestones.find((m) => m.milestoneCode === 'WAGON_ALLOCATED'); + const wagonAllocated = + wagonMilestone?.status === 'COMPLETED' || + booking.schedulingStatus === 'SCHEDULED' || + booking.schedulingStatus === 'DISPATCHED' || + Boolean(booking.trainScheduleId); + + let schedule: TrainSchedule | null = null; + if (booking.trainScheduleId) { + schedule = await this.dataSource + .getRepository(TrainSchedule) + .findOne({ where: { id: booking.trainScheduleId } }); + } + + const closedMilestone = milestones.find( + (m) => m.milestoneCode === 'T1_CLOSED' && m.status === 'COMPLETED', + ); + + return { + bookingId, + wagonAllocated, + trainDepartedAt: schedule?.actualDepartureAt + ? new Date(schedule.actualDepartureAt).toISOString() + : null, + trainArrivedAt: schedule?.actualArrivalAt + ? new Date(schedule.actualArrivalAt).toISOString() + : null, + closed: Boolean(closedMilestone), + closedAt: closedMilestone?.triggeredAt + ? new Date(closedMilestone.triggeredAt).toISOString() + : null, + }; + } + + /** + * GL Djibouti uploads T1 transport documents (multi-file) after wagon allocation. + * Replaces the previous batch; locked once the train departs or T1 is closed. + */ + async uploadT1Documents( + bookingId: string, + files: Express.Multer.File[], + ): Promise<{ uploaded: number }> { + const booking = await this.getBooking(bookingId); + if (booking.tradeDirection !== 'IMPORT') { + throw new BadRequestException('T1 transport documents apply to import shipments only.'); + } + + const state = await this.t1State(bookingId); + if (!state.wagonAllocated) { + throw new BadRequestException( + 'Wagons must be allocated before T1 transport documents can be uploaded.', + ); + } + if (state.closed) { + throw new BadRequestException('T1 has been closed by GL Ethiopia — documents are final.'); + } + if (state.trainDepartedAt) { + throw new BadRequestException( + 'The train has departed — T1 transport documents can no longer be changed.', + ); + } + + await persistT1TransportUploads(this.filesService, bookingId, files); + return { uploaded: files.length }; + } + + /** + * GL Ethiopia closes (accepts) the T1 document set once the train has arrived. + * Completes the T1_CLOSED milestone; the document set becomes final. + */ + async closeT1( + bookingId: string, + userId?: string, + ): Promise { + const booking = await this.getBooking(bookingId); + if (booking.tradeDirection !== 'IMPORT') { + throw new BadRequestException('T1 closure applies to import shipments only.'); + } + + const state = await this.t1State(bookingId); + if (state.closed) return state; + if (!state.trainArrivedAt) { + throw new BadRequestException( + 'The train has not arrived yet — T1 can be closed only after arrival.', + ); + } + + const files = await this.filesService.findByResource(bookingId, 'bookings'); + const hasT1 = files.some((f) => isT1TransportFileCode(f.code)); + if (!hasT1) { + throw new BadRequestException( + 'No T1 transport documents on file — GL Djibouti must upload them first.', + ); + } + + await this.milestoneService.completeForBooking(bookingId, 'T1_CLOSED', userId); + return this.t1State(bookingId); + } + /** * GL ET uploads export transport document after wagon allocation (export ONE_TIME). */ diff --git a/apps/edr-freight-api/src/modules/contracts/phased-clearance.util.ts b/apps/edr-freight-api/src/modules/contracts/phased-clearance.util.ts index 89fbf797e..2e8682951 100644 --- a/apps/edr-freight-api/src/modules/contracts/phased-clearance.util.ts +++ b/apps/edr-freight-api/src/modules/contracts/phased-clearance.util.ts @@ -5,7 +5,9 @@ import { isDeclarationFileCode, isImportTransitPermitFileCode, isExportTransportFileCode, + isT1TransportFileCode, exportTransportFileLabel, + t1TransportFileLabel, transitPermitFileLabel, type ClearanceWorkflowFile, } from '@edr/types'; @@ -160,6 +162,50 @@ export async function persistExportTransportUploads( ); } +/** Require at least one T1 transport document in the upload batch. */ +export function assertT1TransportFiles(files: Express.Multer.File[]): void { + if (files.length === 0) { + throw new BadRequestException('No T1 transport documents uploaded'); + } +} + +export function normalizeT1TransportFieldNames( + files: Express.Multer.File[], +): Express.Multer.File[] { + return files.map((file, index) => ({ + ...file, + fieldname: `t1_transport_document_${index}`, + })); +} + +/** Replace all T1 transport documents on a booking with a new multi-file batch. */ +export async function persistT1TransportUploads( + store: DeclarationFileStore, + bookingId: string, + files: Express.Multer.File[], +): Promise { + const normalized = normalizeT1TransportFieldNames(files); + assertT1TransportFiles(normalized); + + const existing = await store.findByResource(bookingId, 'bookings'); + await Promise.all( + existing + .filter((f) => f.code && isT1TransportFileCode(f.code)) + .map((f) => store.deleteByCode(bookingId, 'bookings', f.code!)), + ); + + await Promise.all( + normalized.map((file, index) => + store.upload({ + resourceId: bookingId, + resource: 'bookings', + code: `t1_transport_document_${index}`, + file, + }), + ), + ); +} + export function parseDutyRequiredForm(value: string | boolean | undefined): boolean { if (typeof value === 'boolean') return value; if (value === undefined || value === '') return false; @@ -194,9 +240,9 @@ export function belongsOnDjClearanceQueue( ); if (hasDjActivity) return true; - const preFinalized = - cycle?.preClearanceFinalizedAt ?? extras?.preClearanceFinalizedAt ?? null; - if (tradeDirection === 'IMPORT' && preFinalized) return true; + // Import DO upload is un-gated — Djibouti GL must see import customs items from + // the start, not only after Ethiopia finalizes pre-clearance. + if (tradeDirection === 'IMPORT') return true; return false; } @@ -295,6 +341,22 @@ export function buildWorkflowFiles( file: { id: file.id, name: file.name, url: file.url }, }); }); + + const extraT1 = files + .filter((f) => f.code && isT1TransportFileCode(f.code) && !included.has(f.code)) + .sort((a, b) => (a.code ?? '').localeCompare(b.code ?? '')); + + extraT1.forEach((file, index) => { + if (!file.code) return; + included.add(file.code); + out.push({ + code: file.code, + label: t1TransportFileLabel(file.code, index), + uploadedBy: 'gl_dj', + category: 'djibouti', + file: { id: file.id, name: file.name, url: file.url }, + }); + }); } if (tradeDirection === 'EXPORT') { diff --git a/apps/edr-freight-api/src/seed/paid-indode-demo-bookings.seeder.ts b/apps/edr-freight-api/src/seed/paid-indode-demo-bookings.seeder.ts deleted file mode 100644 index e33baa7fd..000000000 --- a/apps/edr-freight-api/src/seed/paid-indode-demo-bookings.seeder.ts +++ /dev/null @@ -1,1131 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { CargoUnitOfMeasure, TrainScheduleStatus, WagonStatus } from '@edr/types'; -import { randomUUID } from 'crypto'; -import { DataSource, EntityManager, In } from 'typeorm'; - -import { BookingContainer } from '../modules/bookings/entities/booking-container.entity'; -import { Booking } from '../modules/bookings/entities/booking.entity'; -import { - Company, - CompanyKind, - CompanyNationality, - CompanyStatus, - CompanyType, -} from '../modules/companies/entities/company.entity'; -import { - CompanyProfile, - ProfileStatus, - ProfileType, -} from '../modules/companies/entities/company-profile.entity'; -import { FirstMile } from '../modules/first-mile/entities/first-mile.entity'; -import { LastMile } from '../modules/last-mile/entities/last-mile.entity'; -import { Locomotive } from '../modules/locomotives/entities/locomotive.entity'; -import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity'; -import { ContainerType } from '../modules/rule-engine/entities/container-type.entity'; -import { ServiceType } from '../modules/rule-engine/entities/service-type.entity'; -import { Yard } from '../modules/rule-engine/entities/yard.entity'; -import { WagonAllocationContainerItem } from '../modules/train-schedules/entities/wagon-allocation-container-item.entity'; -import { WagonBookingAllocation } from '../modules/train-schedules/entities/wagon-booking-allocation.entity'; -import { TrainSchedule } from '../modules/train-schedules/entities/train-schedule.entity'; -import { TrainScheduleBooking } from '../modules/train-schedules/entities/train-schedule-booking.entity'; -import { TrainSetWagon } from '../modules/train-sets/entities/train-set-wagon.entity'; -import { TrainSet } from '../modules/train-sets/entities/train-set.entity'; -import { ImportDjiboutiOperation } from '../modules/train-scheduling/entities/import-djibouti-operation.entity'; -import { WagonType } from '../modules/wagon-types/entities/wagon-type.entity'; -import { Wagon } from '../modules/wagons/entities/wagon.entity'; -import { WarehouseInventory } from '../modules/warehouses/entities/warehouse-inventory.entity'; -import { WarehouseActivityLog } from '../modules/warehouses/entities/warehouse-activity-log.entity'; -import { Warehouse } from '../modules/warehouses/entities/warehouse.entity'; -import { WarehouseYard } from '../modules/warehouses/entities/warehouse-yard.entity'; -import { WarehouseZone } from '../modules/warehouses/entities/warehouse-zone.entity'; -import { Driver, DriverStatus } from '../modules/drivers/entities/driver.entity'; -import { FuelType, Vehicle, VehicleStatus, VehicleType } from '../modules/vehicles/entities/vehicle.entity'; - -const CUSTOMER_TIN = 'US12DEMO01'; - -const DEMO_TRAINS = [ - { - trainNumber: 'US12-DJI-IND-01', - direction: 'IMPORT', - originCode: 'NAGAD', - destinationCode: 'INDODE', - departureHoursAgo: 30, - arrivalHoursAgo: 14, - }, - { - trainNumber: 'US12-IND-DJI-01', - direction: 'EXPORT', - originCode: 'INDODE', - destinationCode: 'NAGAD', - departureHoursAgo: 28, - arrivalHoursAgo: 12, - }, - { - trainNumber: 'US12-DJI-IND-LM-02', - direction: 'IMPORT', - originCode: 'NAGAD', - destinationCode: 'INDODE', - departureHoursAgo: 24, - arrivalHoursAgo: 8, - }, - { - trainNumber: 'US12-IND-DJI-LM-02', - direction: 'EXPORT', - originCode: 'INDODE', - destinationCode: 'NAGAD', - departureHoursAgo: 22, - arrivalHoursAgo: 6, - }, -] as const; - -const TRAIN_DEMO_BOOKINGS = [ - { - reference: 'US12-IMP-FM-001', - trainNumber: 'US12-DJI-IND-01', - tradeDirection: 'IMPORT', - freightType: 'CONTAINER', - withFirstMile: true, - withLastMile: true, - containerCode: '40FT', - cargoCode: 'GENERAL_CARGO', - weightTons: 27, - totalAmount: 18450, - pickupAddress: 'Doraleh Container Terminal, Djibouti', - pickupLat: 11.5881, - pickupLng: 43.1372, - deliveryAddress: 'Indode bonded warehouse gate, Ethiopia', - deliveryLat: 8.7566, - deliveryLng: 38.9846, - }, - { - reference: 'US12-IMP-NOFM-001', - trainNumber: 'US12-DJI-IND-01', - tradeDirection: 'IMPORT', - freightType: 'BULK', - withFirstMile: false, - withLastMile: false, - containerCode: null, - cargoCode: 'BULK', - weightTons: 42, - totalAmount: 22100, - pickupAddress: null, - pickupLat: null, - pickupLng: null, - deliveryAddress: null, - deliveryLat: null, - deliveryLng: null, - }, - { - reference: 'US12-EXP-FM-001', - trainNumber: 'US12-IND-DJI-01', - tradeDirection: 'EXPORT', - freightType: 'CONTAINER', - withFirstMile: true, - withLastMile: true, - containerCode: '20FT', - cargoCode: 'GENERAL_CARGO', - weightTons: 19, - totalAmount: 15680, - pickupAddress: 'Indode export truck gate, Ethiopia', - pickupLat: 8.7566, - pickupLng: 38.9846, - deliveryAddress: 'Nagad Terminal customer handover yard, Djibouti', - deliveryLat: 11.5536, - deliveryLng: 43.1103, - }, - { - reference: 'US12-EXP-NOFM-001', - trainNumber: 'US12-IND-DJI-01', - tradeDirection: 'EXPORT', - freightType: 'BULK', - withFirstMile: false, - withLastMile: false, - containerCode: null, - cargoCode: 'BULK', - weightTons: 55, - totalAmount: 29800, - pickupAddress: null, - pickupLat: null, - pickupLng: null, - deliveryAddress: null, - deliveryLat: null, - deliveryLng: null, - }, - { - reference: 'US12-IMP-LM-TRAIN-001', - trainNumber: 'US12-DJI-IND-LM-02', - tradeDirection: 'IMPORT', - freightType: 'CONTAINER', - withFirstMile: false, - withLastMile: true, - containerCode: '40FT', - cargoCode: 'GENERAL_CARGO', - weightTons: 31, - totalAmount: 20300, - pickupAddress: null, - pickupLat: null, - pickupLng: null, - deliveryAddress: 'Indode last-mile customer delivery bay, Ethiopia', - deliveryLat: 8.7581, - deliveryLng: 38.9834, - }, - { - reference: 'US12-EXP-LM-TRAIN-001', - trainNumber: 'US12-IND-DJI-LM-02', - tradeDirection: 'EXPORT', - freightType: 'CONTAINER', - withFirstMile: false, - withLastMile: true, - containerCode: '20FT', - cargoCode: 'GENERAL_CARGO', - weightTons: 21, - totalAmount: 17600, - pickupAddress: null, - pickupLat: null, - pickupLng: null, - deliveryAddress: 'Nagad last-mile consignee handover yard, Djibouti', - deliveryLat: 11.5549, - deliveryLng: 43.1121, - }, -] as const; - -const CUSTOMER_TRUCK_DEMO_BOOKINGS = [ - { - reference: 'US12-EXP-FM-TRUCK-001', - trainNumber: null, - originCode: 'INDODE', - destinationCode: 'NAGAD', - tradeDirection: 'EXPORT', - freightType: 'CONTAINER', - withFirstMile: true, - withLastMile: false, - containerCode: '40FT', - cargoCode: 'GENERAL_CARGO', - weightTons: 24, - totalAmount: 14800, - pickupAddress: 'Customer factory gate, Addis Ababa', - pickupLat: 8.9806, - pickupLng: 38.8736, - deliveryAddress: null, - deliveryLat: null, - deliveryLng: null, - }, - { - reference: 'US12-EXP-NOFM-TRUCK-001', - trainNumber: null, - originCode: 'INDODE', - destinationCode: 'NAGAD', - tradeDirection: 'EXPORT', - freightType: 'CONTAINER', - withFirstMile: false, - withLastMile: false, - containerCode: '20FT', - cargoCode: 'GENERAL_CARGO', - weightTons: 18, - totalAmount: 11200, - pickupAddress: null, - pickupLat: null, - pickupLng: null, - deliveryAddress: null, - deliveryLat: null, - deliveryLng: null, - customerTruckPlateNumber: 'ET-CUS-2046', - customerTruckDriverName: 'Dawit Customer Carrier', - customerTruckType: 'Container Chassis', - customerTruckContainerNumber: 'USDU1234567', - }, -] as const; - -const DEMO_BOOKINGS = [...TRAIN_DEMO_BOOKINGS, ...CUSTOMER_TRUCK_DEMO_BOOKINGS] as const; - -@Injectable() -export class PaidIndodeDemoBookingsSeeder { - private readonly logger = new Logger(PaidIndodeDemoBookingsSeeder.name); - - constructor(private readonly dataSource: DataSource) {} - - async run(): Promise { - try { - await this.dataSource.transaction(async (manager) => { - const refs = await this.ensureReferenceData(manager); - const schedules = await this.ensureArrivedTrains(manager, refs); - const bookings = await this.ensureBookings(manager, refs, schedules); - await this.ensureTrainLinks(manager, refs, schedules, bookings); - await this.ensureGatepasses(manager, schedules); - await this.ensureImportWarehouseInventory(manager, bookings); - }); - - this.logger.log( - `US12 paid Indode demo bookings ready: ${DEMO_BOOKINGS.length} booking(s), ${DEMO_TRAINS.length} arrived train(s)`, - ); - } catch (error) { - this.logger.error( - `PaidIndodeDemoBookingsSeeder failed: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async ensureReferenceData(manager: EntityManager) { - await manager.getRepository(Yard).upsert( - [ - { - code: 'INDODE', - label: 'Indode Terminal', - country: 'Ethiopia', - isActive: true, - displayOrder: 1, - }, - { - code: 'NAGAD', - label: 'Nagad Terminal, Djibouti', - country: 'Djibouti', - isActive: true, - displayOrder: 2, - }, - ], - { conflictPaths: { code: true } }, - ); - - await manager.getRepository(ServiceType).upsert( - [ - { - code: 'RAIL_CONTAINER_FIRST_LAST', - serviceName: 'Rail Freight with First and Last Mile', - description: 'Rail movement with first-mile pickup and last-mile delivery', - canBeBookedAlone: true, - includesFirstMile: true, - includesLastMile: true, - includesCustoms: false, - priorityBonusPoints: 15, - isActive: true, - displayOrder: 3, - }, - { - code: 'RAIL_CONTAINER_LAST_MILE', - serviceName: 'Rail Freight with Last Mile', - description: 'Rail movement with last-mile delivery from terminal', - canBeBookedAlone: true, - includesFirstMile: false, - includesLastMile: true, - includesCustoms: false, - priorityBonusPoints: 8, - isActive: true, - displayOrder: 4, - }, - { - code: 'RAIL_CONTAINER', - serviceName: 'Rail Freight', - description: 'Rail movement without first-mile pickup', - canBeBookedAlone: true, - includesFirstMile: false, - includesLastMile: false, - includesCustoms: false, - priorityBonusPoints: 0, - isActive: true, - displayOrder: 1, - }, - { - code: 'RAIL_CONTAINER_FIRST_MILE', - serviceName: 'Rail Freight with First Mile', - description: 'Rail movement with first-mile pickup to terminal', - canBeBookedAlone: true, - includesFirstMile: true, - includesLastMile: false, - includesCustoms: false, - priorityBonusPoints: 10, - isActive: true, - displayOrder: 2, - }, - ], - { conflictPaths: { code: true } }, - ); - - await manager.getRepository(ContainerType).upsert( - [ - { - code: '20FT', - label: '20FT Standard', - sizeFt: 20, - wagonsPerUnit: 1, - isReefer: false, - isOpenTop: false, - isActive: true, - displayOrder: 1, - }, - { - code: '40FT', - label: '40FT Standard', - sizeFt: 40, - wagonsPerUnit: 1, - isReefer: false, - isOpenTop: false, - isActive: true, - displayOrder: 2, - }, - ], - { conflictPaths: { code: true } }, - ); - - await manager.getRepository(CargoType).upsert( - [ - { - code: 'GENERAL_CARGO', - cargoTypeName: 'General Cargo', - showFreeTextBox: true, - unitOfMeasure: null, - requiresDirectorApproval: false, - isActive: true, - displayOrder: 1, - }, - { - code: 'BULK', - cargoTypeName: 'Bulk Cargo', - showFreeTextBox: true, - unitOfMeasure: CargoUnitOfMeasure.PerTon, - requiresDirectorApproval: false, - isActive: true, - displayOrder: 2, - }, - ], - { conflictPaths: { code: true } }, - ); - - await manager.getRepository(WagonType).upsert( - { - code: 'US12-DEMO', - name: 'US12 Demo Flat/Bulk Wagon', - capacityTons: 70, - lengthMeters: 14, - maxWagonsPerTrain: 53, - supportedLoadTypes: ['CONTAINER', 'BULK'], - isActive: true, - equatedLengthM: 14, - tareWeightTons: 14, - supportsContainer: true, - maxContainerGrossT: 40, - }, - { conflictPaths: { code: true } }, - ); - - await manager.getRepository(Company).upsert( - { - name: 'US12 Indode Demo Customer PLC', - type: CompanyType.Customer, - kind: CompanyKind.Commercial, - status: CompanyStatus.Active, - tin: CUSTOMER_TIN, - vatNumber: 'VAT-US12-001', - fanNumber: 'US12000000000001', - country: 'Ethiopia', - nationality: CompanyNationality.Ethiopian, - address: 'Bole Road, Addis Ababa, Ethiopia', - phone: '251911120012', - email: 'us12.indode.demo@edr.local', - website: 'https://edr.local/us12-demo', - contactPersonName: 'Aster Bekele', - contactPersonPhone: '251911120013', - generalManagerName: 'Mekonnen Desta', - generalManagerEmail: 'manager.us12.demo@edr.local', - generalManagerPhone: '251911120014', - licenceNumber: 'LIC-US12-2026', - region: 'Addis Ababa', - zone: 'Bole', - woreda: '03', - kebele: '12', - houseNo: 'US12-01', - attributes: { - seededBy: 'PaidIndodeDemoBookingsSeeder', - note: 'Paid customer with import/export demo bookings for US12.', - } as any, - }, - { conflictPaths: { tin: true } }, - ); - - const company = await manager.getRepository(Company).findOneByOrFail({ tin: CUSTOMER_TIN }); - await manager.getRepository(CompanyProfile).upsert( - [ - { - companyId: company.id, - type: ProfileType.importer, - reference: 'US12-IMP', - status: ProfileStatus.Active, - businessLicense: 'BL-US12-IMP-2026', - attributes: { seededBy: 'PaidIndodeDemoBookingsSeeder' } as any, - }, - { - companyId: company.id, - type: ProfileType.exporter, - reference: 'US12-EXP', - status: ProfileStatus.Active, - businessLicense: 'BL-US12-EXP-2026', - attributes: { seededBy: 'PaidIndodeDemoBookingsSeeder' } as any, - }, - ], - { conflictPaths: { reference: true } }, - ); - - const [yards, serviceTypes, containerTypes, cargoTypes, wagonType, importerProfile, exporterProfile] = - await Promise.all([ - manager.getRepository(Yard).find({ where: { code: In(['INDODE', 'NAGAD']) } }), - manager - .getRepository(ServiceType) - .find({ - where: { - code: In([ - 'RAIL_CONTAINER', - 'RAIL_CONTAINER_FIRST_MILE', - 'RAIL_CONTAINER_LAST_MILE', - 'RAIL_CONTAINER_FIRST_LAST', - ]), - }, - }), - manager.getRepository(ContainerType).find({ where: { code: In(['20FT', '40FT']) } }), - manager.getRepository(CargoType).find({ where: { code: In(['GENERAL_CARGO', 'BULK']) } }), - manager.getRepository(WagonType).findOneByOrFail({ code: 'US12-DEMO' }), - manager.getRepository(CompanyProfile).findOneByOrFail({ reference: 'US12-IMP' }), - manager.getRepository(CompanyProfile).findOneByOrFail({ reference: 'US12-EXP' }), - ]); - - return { - company, - importerProfile, - exporterProfile, - yards: new Map(yards.map((yard) => [yard.code, yard])), - serviceTypes: new Map(serviceTypes.map((serviceType) => [serviceType.code, serviceType])), - containerTypes: new Map(containerTypes.map((containerType) => [containerType.code, containerType])), - cargoTypes: new Map(cargoTypes.map((cargoType) => [cargoType.code, cargoType])), - wagonType, - }; - } - - private async ensureArrivedTrains( - manager: EntityManager, - refs: Awaited>, - ): Promise> { - const schedules = new Map(); - const now = new Date(); - - for (const demo of DEMO_TRAINS) { - const origin = refs.yards.get(demo.originCode); - const destination = refs.yards.get(demo.destinationCode); - if (!origin || !destination) { - throw new Error(`US12 demo train missing yard: ${demo.trainNumber}`); - } - - const departure = this.addHours(now, -demo.departureHoursAgo); - const arrival = this.addHours(now, -demo.arrivalHoursAgo); - const locomotive = await this.ensureLocomotive(manager, origin.id); - const trainSet = await this.ensureTrainSet(manager, demo.trainNumber, locomotive.id); - const schedule = await this.ensureTrainSchedule(manager, { - trainNumber: demo.trainNumber, - trainSetId: trainSet.id, - originStationId: origin.id, - destinationStationId: destination.id, - scheduledDepartureDate: departure, - scheduledArrivalDate: arrival, - actualDepartureAt: departure, - actualArrivalAt: arrival, - direction: demo.direction, - }); - - await manager.getRepository(TrainSet).update(trainSet.id, { - totalWeightTons: TRAIN_DEMO_BOOKINGS.filter((booking) => booking.trainNumber === demo.trainNumber) - .reduce((sum, booking) => sum + booking.weightTons, 0), - totalLengthMeters: 28, - wagonCount: 2, - status: 'COMPLETED', - }); - schedules.set(demo.trainNumber, schedule); - } - - return schedules; - } - - private async ensureBookings( - manager: EntityManager, - refs: Awaited>, - schedules: Map, - ): Promise> { - const bookingRepo = manager.getRepository(Booking); - const bookingContainerRepo = manager.getRepository(BookingContainer); - const firstMileRepo = manager.getRepository(FirstMile); - const lastMileRepo = manager.getRepository(LastMile); - const now = new Date(); - const references = DEMO_BOOKINGS.map((booking) => booking.reference); - const existingBookings = await bookingRepo.find({ where: { reference: In(references) } }); - const existingBookingIds = existingBookings.map((booking) => booking.id); - const firstMileVehicle = await this.ensureFirstMileVehicle(manager); - - if (existingBookingIds.length) { - const existingInventory = await manager.getRepository(WarehouseInventory).find({ - where: { bookingId: In(existingBookingIds) }, - select: { id: true }, - }); - const existingInventoryIds = existingInventory.map((item) => item.id); - if (existingInventoryIds.length) { - await manager.getRepository(WarehouseActivityLog).delete({ - inventoryId: In(existingInventoryIds), - }); - await manager.getRepository(WarehouseInventory).delete({ - id: In(existingInventoryIds), - }); - } - await this.deleteBookingTrainChildren(manager, existingBookingIds); - await bookingContainerRepo.delete({ bookingId: In(existingBookingIds) }); - await firstMileRepo.delete({ bookingId: In(existingBookingIds) }); - await lastMileRepo.delete({ bookingId: In(existingBookingIds) }); - } - - for (const demo of DEMO_BOOKINGS) { - const schedule = demo.trainNumber ? schedules.get(demo.trainNumber) : null; - if (demo.trainNumber && !schedule) { - throw new Error(`US12 demo booking missing train: ${demo.reference}`); - } - - const train = demo.trainNumber ? DEMO_TRAINS.find((item) => item.trainNumber === demo.trainNumber) : null; - const originCode = train?.originCode ?? ('originCode' in demo ? demo.originCode : undefined); - const destinationCode = train?.destinationCode ?? ('destinationCode' in demo ? demo.destinationCode : undefined); - const origin = originCode ? refs.yards.get(originCode) : null; - const destination = destinationCode ? refs.yards.get(destinationCode) : null; - const serviceType = refs.serviceTypes.get( - demo.withFirstMile && demo.withLastMile - ? 'RAIL_CONTAINER_FIRST_LAST' - : demo.withFirstMile - ? 'RAIL_CONTAINER_FIRST_MILE' - : demo.withLastMile - ? 'RAIL_CONTAINER_LAST_MILE' - : 'RAIL_CONTAINER', - ); - const cargoType = refs.cargoTypes.get(demo.cargoCode); - const profile = demo.tradeDirection === 'IMPORT' ? refs.importerProfile : refs.exporterProfile; - - if (!origin || !destination || !serviceType || !cargoType) { - throw new Error(`US12 demo booking missing reference data: ${demo.reference}`); - } - - await bookingRepo.upsert( - { - reference: demo.reference, - companyId: refs.company.id, - companyProfileId: profile.id, - isGovernment: false, - status: - 'customerTruckPlateNumber' in demo && demo.customerTruckPlateNumber - ? 'TRUCK_ASSIGNED' - : demo.trainNumber - ? 'IN_TRANSIT' - : 'PAID', - scheduledDate: schedule?.scheduledDepartureDate ?? now, - estimatedShipmentDate: schedule?.scheduledDepartureDate ?? now, - totalAmount: demo.totalAmount, - paymentStatus: 'PAID', - contractType: 'NEW', - serviceTypeId: serviceType.id, - firstMilePickupAddress: demo.pickupAddress, - firstMilePickupLat: demo.pickupLat, - firstMilePickupLng: demo.pickupLng, - lastMileDeliveryAddress: demo.deliveryAddress, - lastMileDeliveryLat: demo.deliveryLat, - lastMileDeliveryLng: demo.deliveryLng, - customerTruckPlateNumber: - 'customerTruckPlateNumber' in demo ? demo.customerTruckPlateNumber : null, - customerTruckDriverName: - 'customerTruckDriverName' in demo ? demo.customerTruckDriverName : null, - customerTruckType: - 'customerTruckType' in demo ? demo.customerTruckType : null, - customerTruckContainerNumber: - 'customerTruckContainerNumber' in demo ? demo.customerTruckContainerNumber : null, - customerTruckAssignedAt: - 'customerTruckPlateNumber' in demo && demo.customerTruckPlateNumber - ? this.addHours(now, -2) - : null, - customerTruckArrivedAt: null, - customsClearingEnabled: false, - equipmentReturn: 'WITHOUT_RETURN', - originYardId: origin.id, - destinationYardId: destination.id, - tradeDirection: demo.tradeDirection, - freightType: demo.freightType, - cargoTypeId: cargoType.id, - cargoFreeText: demo.freightType === 'BULK' ? 'Seeded paid bulk cargo' : 'Seeded paid container cargo', - shippingLineId: null, - cargoTotalWeightVgm: demo.weightTons, - isHazardous: false, - isReefer: false, - paymentCurrency: 'ETB', - pnrCode: `PNR-${demo.reference}`, - versionNumber: 1, - approvedByStaffAt: now, - customerSignedAt: now, - fullyExecutedAt: now, - pricingBreakdown: { - paid: true, - source: 'PaidIndodeDemoBookingsSeeder', - firstMileIncluded: demo.withFirstMile, - lastMileIncluded: demo.withLastMile, - }, - priorityScore: demo.withFirstMile ? 30 : demo.withLastMile ? 25 : 20, - wagonsRequired: 1, - schedulingStatus: demo.trainNumber ? 'DISPATCHED' : 'NOT_SCHEDULED', - scheduledAt: demo.trainNumber ? now : null, - trainScheduleId: schedule?.id ?? null, - paymentDeadline: null, - selectedForBatchAt: demo.trainNumber ? now : null, - }, - { conflictPaths: { reference: true } }, - ); - - const booking = await bookingRepo.findOneByOrFail({ reference: demo.reference }); - - if (demo.freightType === 'CONTAINER' && demo.containerCode) { - const containerType = refs.containerTypes.get(demo.containerCode); - if (!containerType) { - throw new Error(`US12 demo booking missing container type: ${demo.reference}`); - } - await bookingContainerRepo.insert({ - id: randomUUID(), - bookingId: booking.id, - containerTypeId: containerType.id, - containerNumber: this.containerNumber(demo.reference), - containerSize: demo.containerCode.startsWith('40') ? '40ft' : '20ft', - quantity: 1, - hazardousQuantity: 0, - reeferQuantity: 0, - vgmPerUnitTons: demo.weightTons, - totalVgmTons: demo.weightTons, - wagonsRequired: 1, - weightLimitRuleId: null, - isOverweight: false, - overweightExcessTons: null, - }); - } - - if (demo.withFirstMile) { - await firstMileRepo.insert({ - id: randomUUID(), - bookingId: booking.id, - status: 'RECEIVED_TO_PORT', - advancedPayment: demo.totalAmount, - remainingPayment: 0, - estimatedKm: demo.tradeDirection === 'IMPORT' ? 12 : 35, - exactKm: demo.tradeDirection === 'IMPORT' ? 11.8 : 34.6, - vehicleId: firstMileVehicle.id, - }); - } - - if (demo.withLastMile) { - await lastMileRepo.insert({ - id: randomUUID(), - bookingId: booking.id, - status: 'DELIVERED', - advancedPayment: demo.totalAmount, - remainingPayment: 0, - estimatedKm: demo.tradeDirection === 'IMPORT' ? 18 : 14, - exactKm: demo.tradeDirection === 'IMPORT' ? 17.5 : 13.8, - vehicleId: null, - }); - } - } - - const savedBookings = await bookingRepo.find({ where: { reference: In(references) } }); - return new Map(savedBookings.map((booking) => [booking.reference, booking])); - } - - private async ensureTrainLinks( - manager: EntityManager, - refs: Awaited>, - schedules: Map, - bookings: Map, - ): Promise { - const scheduleBookingRepo = manager.getRepository(TrainScheduleBooking); - const trainSetWagonRepo = manager.getRepository(TrainSetWagon); - const allocationRepo = manager.getRepository(WagonBookingAllocation); - const containerItemRepo = manager.getRepository(WagonAllocationContainerItem); - const wagonCapacity = Number(refs.wagonType.capacityTons) || 70; - const wagonLength = Number(refs.wagonType.lengthMeters) || 14; - const tareWeight = Number(refs.wagonType.tareWeightTons) || 14; - - for (const demo of TRAIN_DEMO_BOOKINGS) { - const schedule = schedules.get(demo.trainNumber); - const booking = bookings.get(demo.reference); - if (!schedule || !booking) continue; - - const trainBookings = TRAIN_DEMO_BOOKINGS.filter((item) => item.trainNumber === demo.trainNumber); - const sequence = trainBookings.findIndex((item) => item.reference === demo.reference) + 1; - const wagon = await this.ensureWagon(manager, { - wagonNumber: `${demo.trainNumber}-W${String(sequence).padStart(2, '0')}`, - wagonTypeId: refs.wagonType.id, - yardId: schedule.destinationStationId, - trainScheduleId: schedule.id, - trainSetWagonId: null, - tareWeight, - capacityTons: wagonCapacity, - }); - - let trainSetWagon = await trainSetWagonRepo.findOne({ - where: { trainSetId: schedule.trainSetId, sequenceNo: sequence }, - }); - trainSetWagon = await trainSetWagonRepo.save( - trainSetWagonRepo.create({ - ...(trainSetWagon ? { id: trainSetWagon.id } : {}), - trainSetId: schedule.trainSetId, - wagonTypeId: refs.wagonType.id, - physicalWagonId: wagon.id, - sequenceNo: sequence, - capacityTons: wagonCapacity, - lengthMeters: wagonLength, - assignedWeightTons: demo.weightTons, - status: 'DEPARTED', - }), - ); - - await manager.getRepository(Wagon).update(wagon.id, { - trainSetWagonId: trainSetWagon.id, - currentTrainScheduleId: schedule.id, - currentYardId: schedule.destinationStationId, - status: WagonStatus.Assigned, - }); - - const allocation = await allocationRepo.save( - allocationRepo.create({ - trainSetWagonId: trainSetWagon.id, - bookingId: booking.id, - allocatedWeightTons: demo.weightTons, - loadType: demo.freightType, - status: 'DEPARTED', - confirmedAt: schedule.actualDepartureAt ?? new Date(), - }), - ); - - if (demo.freightType === 'CONTAINER') { - const bookingContainer = await manager.getRepository(BookingContainer).findOne({ - where: { bookingId: booking.id }, - }); - const containerType = demo.containerCode ? refs.containerTypes.get(demo.containerCode) : null; - await containerItemRepo.insert({ - id: randomUUID(), - wagonBookingAllocationId: allocation.id, - bookingContainerId: bookingContainer?.id ?? null, - containerNumber: this.containerNumber(demo.reference), - containerTypeId: containerType?.id ?? null, - positionOnWagon: 1, - sealNumber: `SEAL-${demo.reference}`, - chassisNumber: `CHS-${demo.reference}`, - grossWeightTons: demo.weightTons, - }); - } - - await scheduleBookingRepo.insert({ - id: randomUUID(), - trainScheduleId: schedule.id, - bookingId: booking.id, - }); - } - } - - private async ensureGatepasses( - manager: EntityManager, - schedules: Map, - ): Promise { - const repo = manager.getRepository(ImportDjiboutiOperation); - const securedAt = this.addHours(new Date(), -20); - - for (const schedule of schedules.values()) { - const existing = await repo.findOne({ where: { trainScheduleId: schedule.id } }); - await repo.save( - repo.create({ - ...(existing ? { id: existing.id } : {}), - trainScheduleId: schedule.id, - documents: { - ...(existing?.documents ?? {}), - GATE_PASS: { - reference: `GP-${schedule.trainNumber}`, - uploadedAt: securedAt.toISOString(), - uploadedBy: 'PaidIndodeDemoBookingsSeeder', - notes: 'Seeded secured gate pass for import/export Djibouti port entry testing.', - }, - }, - gatepassGrantedAt: securedAt, - performedBy: 'PaidIndodeDemoBookingsSeeder', - notes: 'Seeded SECURED gate pass for US12 warehouse workflow testing.', - }), - ); - } - } - - private async ensureImportWarehouseInventory( - manager: EntityManager, - bookings: Map, - ): Promise { - const warehouse = await manager.getRepository(Warehouse).findOne({ where: { code: 'INDODE_OPEN' } }); - if (!warehouse) { - this.logger.warn('INDODE_OPEN warehouse missing; skipping US12 import warehouse inventory seed'); - return; - } - - for (const demo of TRAIN_DEMO_BOOKINGS.filter((booking) => booking.tradeDirection === 'IMPORT')) { - const booking = bookings.get(demo.reference); - if (!booking) continue; - - const yard = await this.findWarehouseYard(manager, warehouse.id, demo.freightType); - if (!yard) { - this.logger.warn(`No warehouse yard found for ${warehouse.code}; skipping ${demo.reference}`); - continue; - } - const zone = await manager.getRepository(WarehouseZone).findOne({ where: { yardId: yard.id } }); - if (!zone) { - this.logger.warn(`No warehouse zone found for ${yard.code}; skipping ${demo.reference}`); - continue; - } - - const arrivedAt = this.addHours(new Date(), -Number(demo.trainNumber.includes('LM') ? 7 : 13)); - const grnNumber = `GRN-IMP-${demo.reference.replace(/[^A-Z0-9]/g, '')}`; - const saved = await manager.getRepository(WarehouseInventory).save( - manager.getRepository(WarehouseInventory).create({ - warehouseId: warehouse.id, - yardId: yard.id, - zoneId: zone.id, - bookingId: booking.id, - quantity: demo.freightType === 'CONTAINER' ? 1 : 1, - weight: demo.weightTons, - volume: null, - grnNumber, - status: 'UNLOADED', - inspectionStatus: null, - arrivedAt, - unloadedAt: arrivedAt, - notes: [ - `GRN Number: ${grnNumber}`, - 'Direction: IMPORT', - `Train: ${demo.trainNumber}`, - `Seeded For: ${demo.withLastMile ? 'Import with last mile' : 'Import terminal pickup / no last mile'}`, - 'Seeded by PaidIndodeDemoBookingsSeeder for Receive at Warehouse testing.', - ].join('\n'), - }), - ); - - await manager.getRepository(WarehouseActivityLog).save( - manager.getRepository(WarehouseActivityLog).create({ - inventoryId: saved.id, - warehouseId: warehouse.id, - activityType: 'INVENTORY_UNLOADED', - description: `Seeded import train arrival ${demo.trainNumber} into warehouse queue`, - performedBy: 'PaidIndodeDemoBookingsSeeder', - }), - ); - } - } - - private async findWarehouseYard( - manager: EntityManager, - warehouseId: string, - freightType: string, - ): Promise { - const preferredType = freightType === 'CONTAINER' ? 'CONTAINER_YARD' : 'BULK_YARD'; - return ( - (await manager.getRepository(WarehouseYard).findOne({ - where: { warehouseId, type: preferredType as any }, - })) ?? - (await manager.getRepository(WarehouseYard).findOne({ - where: { warehouseId }, - })) - ); - } - - private async deleteBookingTrainChildren(manager: EntityManager, bookingIds: string[]): Promise { - const allocationRepo = manager.getRepository(WagonBookingAllocation); - const allocations = await allocationRepo.find({ - where: { bookingId: In(bookingIds) }, - select: { id: true }, - }); - const allocationIds = allocations.map((allocation) => allocation.id); - if (allocationIds.length) { - await manager.getRepository(WagonAllocationContainerItem).delete({ - wagonBookingAllocationId: In(allocationIds), - }); - } - await allocationRepo.delete({ bookingId: In(bookingIds) }); - await manager.getRepository(TrainScheduleBooking).delete({ bookingId: In(bookingIds) }); - } - - private async ensureLocomotive( - manager: EntityManager, - currentYardId: string, - ): Promise { - const repo = manager.getRepository(Locomotive); - const existing = await repo.findOne({ where: { code: 'US12-DEMO-LOCO' } }); - if (existing) { - await repo.update(existing.id, { currentYardId, status: 'AVAILABLE' }); - return { ...existing, currentYardId, status: 'AVAILABLE' }; - } - - return repo.save( - repo.create({ - code: 'US12-DEMO-LOCO', - name: 'US12 Demo Locomotive', - locomotiveType: 'DIESEL', - maxPullWeightTons: 4200, - maxTrainLengthMeters: 760, - status: 'AVAILABLE', - currentYardId, - }), - ); - } - - private async ensureTrainSet( - manager: EntityManager, - trainNumber: string, - locomotiveId: string, - ): Promise { - const schedule = await manager.getRepository(TrainSchedule).findOne({ - where: { trainNumber }, - }); - if (schedule) { - const existing = await manager.getRepository(TrainSet).findOneByOrFail({ - id: schedule.trainSetId, - }); - await manager.getRepository(TrainSet).update(existing.id, { - locomotiveId, - status: 'COMPLETED', - }); - return { ...existing, locomotiveId, status: 'COMPLETED' }; - } - - return manager.getRepository(TrainSet).save( - manager.getRepository(TrainSet).create({ - locomotiveId, - totalWeightTons: 0, - totalLengthMeters: 0, - wagonCount: 0, - status: 'COMPLETED', - }), - ); - } - - private async ensureTrainSchedule( - manager: EntityManager, - input: { - trainNumber: string; - trainSetId: string; - originStationId: string; - destinationStationId: string; - scheduledDepartureDate: Date; - scheduledArrivalDate: Date; - actualDepartureAt: Date; - actualArrivalAt: Date; - direction: 'IMPORT' | 'EXPORT'; - }, - ): Promise { - const repo = manager.getRepository(TrainSchedule); - const existing = await repo.findOne({ where: { trainNumber: input.trainNumber } }); - const nextSchedule = repo.create({ - ...(existing ? { id: existing.id } : {}), - trainSetId: input.trainSetId, - originStationId: input.originStationId, - destinationStationId: input.destinationStationId, - scheduledDepartureDate: input.scheduledDepartureDate, - scheduledArrivalDate: input.scheduledArrivalDate, - actualDepartureAt: input.actualDepartureAt, - actualArrivalAt: input.actualArrivalAt, - status: TrainScheduleStatus.Arrived, - trainNumber: input.trainNumber, - direction: input.direction, - maxWagons: 53, - bookingWindowStatus: 'CLOSED', - }); - return repo.save(nextSchedule); - } - - private async ensureWagon( - manager: EntityManager, - input: { - wagonNumber: string; - wagonTypeId: string; - yardId: string; - trainScheduleId: string; - trainSetWagonId: string | null; - tareWeight: number; - capacityTons: number; - }, - ): Promise { - const repo = manager.getRepository(Wagon); - const existing = await repo.findOne({ where: { wagonNumber: input.wagonNumber } }); - return repo.save( - repo.create({ - ...(existing ? { id: existing.id } : {}), - wagonNumber: input.wagonNumber, - wagonTypeId: input.wagonTypeId, - currentYardId: input.yardId, - currentTrainScheduleId: input.trainScheduleId, - trainSetWagonId: input.trainSetWagonId, - tareWeight: input.tareWeight, - maxPayloadWeight: input.capacityTons, - status: WagonStatus.Assigned, - notes: 'US12 paid Indode demo seed wagon', - }), - ); - } - - private async ensureFirstMileVehicle(manager: EntityManager): Promise { - const driverRepo = manager.getRepository(Driver); - const vehicleRepo = manager.getRepository(Vehicle); - const licenseNumber = 'US12-FM-LIC-001'; - const plateNumber = 'ET-FM-1201'; - - await driverRepo.upsert( - { - licenseNumber, - firstName: 'Tesfaye', - lastName: 'Firstmile', - email: 'tesfaye.firstmile@edr.local', - phoneNumber: '251911120120', - licenseExpiryDate: this.addHours(new Date(), 24 * 365), - status: DriverStatus.ACTIVE, - vehicleTypesAuthorized: [VehicleType.TRUCK, VehicleType.FLATBED], - notes: 'Seeded first-mile driver for US12 receive-to-warehouse testing', - }, - { conflictPaths: { licenseNumber: true } }, - ); - const driver = await driverRepo.findOneByOrFail({ licenseNumber }); - - await vehicleRepo.upsert( - { - plateNumber, - registrationNumber: 'US12-FM-REG-001', - vehicleType: VehicleType.TRUCK, - manufacturer: 'Sinotruk', - model: 'HOWO Container Carrier', - year: 2024, - fuelType: FuelType.DIESEL, - capacity: 40, - status: VehicleStatus.ACTIVE, - assignedDriverId: driver.id, - assignedDriverName: `${driver.firstName} ${driver.lastName}`, - description: 'Seeded first-mile truck for US12 receive-to-warehouse testing', - estimatedDistanceKm: 35, - actualDistanceKm: 34.6, - }, - { conflictPaths: { plateNumber: true } }, - ); - const vehicle = await vehicleRepo.findOneByOrFail({ plateNumber }); - await manager.query( - `UPDATE freight.vehicles - SET trailer_plate_no = $2, - assigned_driver_id = $3, - assigned_driver_name = $4, - updated_at = NOW() - WHERE id = $1`, - [vehicle.id, 'ET-TRL-1201', driver.id, `${driver.firstName} ${driver.lastName}`], - ); - return vehicleRepo.findOneByOrFail({ plateNumber }); - } - - private containerNumber(reference: string): string { - const suffix = reference.replace(/[^A-Z0-9]/g, '').slice(-7); - return `US12${suffix}`; - } - - private addHours(date: Date, hours: number): Date { - return new Date(date.getTime() + hours * 60 * 60 * 1000); - } -} diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx index b33729bf1..ba54e3b8b 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/GlClearanceUploadModal.tsx @@ -121,7 +121,8 @@ export function GlClearanceUploadModal({ & { operationReady?: boolean }; type MilestoneRow = NonNullable[number]; @@ -79,7 +80,10 @@ function isBookingMilestoneDone( return m?.status === "COMPLETED" || m?.status === "SKIPPED"; } -function computeImportActiveStep(clearance: ClearanceViewLike): number { +function computeImportActiveStep( + clearance: ClearanceViewLike, + bookingCreated: boolean, +): number { if (!isMilestoneDone(clearance.milestones, "DOCUMENTS_APPROVED")) return 0; if (!isMilestoneDone(clearance.milestones, "DECLARED")) return 1; if ( @@ -98,7 +102,23 @@ function computeImportActiveStep(clearance: ClearanceViewLike): number { if (!isMilestoneDone(clearance.milestones, "TRANSIT_PERMIT_UPLOADED")) return 4; if (!clearance.preClearanceFinalized) return 5; if (!isMilestoneDone(clearance.milestones, "DO_COLLECTED")) return 6; - return 7; + if (!bookingCreated) return 7; + if (!clearance.t1?.closed) return 8; + return 9; +} + +function t1FilesFromWorkflow( + workflowFiles: Freight.ClearanceWorkflowFile[], +): Array<{ code: string; label: string; file: { id: string; name: string } }> { + return workflowFiles + .filter( + (f) => f.code.toLowerCase().startsWith("t1_transport_document") && f.file, + ) + .map((f) => ({ + code: f.code, + label: f.label, + file: f.file!, + })); } function declarationFilesFromWorkflow( @@ -174,9 +194,12 @@ export function PhasedClearanceActionPanel({ const showEt = roleMode === "ET" || roleMode === "ALL"; const showDj = roleMode === "DJ" || roleMode === "ALL"; const isImport = tradeDirection === "IMPORT"; + // The server only builds the t1 block once a booking is linked — use it as the + // booking-created signal on pages that don't pass bookingCreated (GL DJ detail). + const effectiveBookingCreated = bookingCreated || Boolean(clearance.t1); const activeStep = useMemo( - () => (isImport ? computeImportActiveStep(clearance) : 0), - [clearance, isImport], + () => (isImport ? computeImportActiveStep(clearance, effectiveBookingCreated) : 0), + [clearance, isImport, effectiveBookingCreated], ); if (isImport) { @@ -401,11 +424,7 @@ export function PhasedClearanceActionPanel({ description="GL Djibouti uploads DO" icon={} > - {showDj && - canDj && - !useUploadModals && - (activeStep >= 6 || - isMilestoneDone(clearance.milestones, "DO_COLLECTED")) ? ( + {showDj && canDj && !useUploadModals ? ( {useUploadModals && showDj && canDj && onUploadDoRequest ? ( @@ -439,7 +454,6 @@ export function PhasedClearanceActionPanel({ color="edr-green" leftSection={} onClick={onUploadDoRequest} - disabled={!clearance.preClearanceFinalized && !findWorkflowFile(workflowFiles, "delivery_order")} > {findWorkflowFile(workflowFiles, "delivery_order") ? "Replace DO" @@ -472,17 +486,37 @@ export function PhasedClearanceActionPanel({ ) : ( )} + + : + } + > + + @@ -578,6 +612,170 @@ export function PhasedClearanceActionPanel({ ); } +function ImportT1Section({ + t1, + workflowFiles = [], + canDjAct, + canEtAct, + onChanged, + onViewFile, + onDownloadFile, +}: { + t1: Freight.ClearanceT1State | null; + workflowFiles?: Freight.ClearanceWorkflowFile[]; + canDjAct: boolean; + canEtAct: boolean; + onChanged?: () => void; + onViewFile?: (file: { name: string; url: string }) => void; + onDownloadFile?: (file: { id: string; name: string }) => void; +}) { + const [files, setFiles] = useState([]); + const [uploading, setUploading] = useState(false); + const [closing, setClosing] = useState(false); + + const uploaded = t1FilesFromWorkflow(workflowFiles); + const replaceMode = uploaded.length > 0; + + if (!t1) { + return ( + + ); + } + + const departed = Boolean(t1.trainDepartedAt); + const arrived = Boolean(t1.trainArrivedAt); + const canUpload = canDjAct && t1.wagonAllocated && !departed && !t1.closed; + + return ( + + {uploaded.length > 0 ? ( + + + T1 document{uploaded.length > 1 ? "s" : ""} + + {uploaded.map((row) => ( + + ))} + + ) : null} + + {t1.closed ? ( + + ) : !t1.wagonAllocated ? ( + + ) : departed ? ( + }> + The train has departed — T1 documents are locked and can no longer be changed. + + ) : uploaded.length === 0 && !canUpload ? ( + + ) : null} + + {canUpload ? ( + <> + + + + + + ) : null} + + {canEtAct && !t1.closed ? ( + arrived ? ( + + + The train has arrived — review the T1 documents and close (accept) them. + + + + ) : departed ? ( + }> + Train en route — T1 can be closed once it arrives in Ethiopia. + + ) : null + ) : null} + + ); +} + function StepStatus({ done, pendingLabel, diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index dff9167f5..cab56f7ac 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -211,6 +211,10 @@ export const URL_CONSTANTS = { `/contracts/bookings/${bookingId}/documents`, BOOKING_TRANSPORT_DOCUMENT: (bookingId: string) => `/contracts/bookings/${bookingId}/transport-document`, + BOOKING_T1_DOCUMENTS: (bookingId: string) => + `/contracts/bookings/${bookingId}/t1-documents`, + BOOKING_T1_CLOSE: (bookingId: string) => + `/contracts/bookings/${bookingId}/t1-close`, BOOKING_INCIDENTS: (bookingId: string) => `/contracts/bookings/${bookingId}/incidents`, }, diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/GlClearanceDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/GlClearanceDetailPage.tsx index ecf95fe89..6756ecc3e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/GlClearanceDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/GlClearanceDetailPage.tsx @@ -112,7 +112,8 @@ export default function GlClearanceDetailPage() { const isImport = data.tradeDirection === "IMPORT"; const hasDo = Boolean(findWorkflowFile(workflowFiles, "delivery_order")); const hasRo = Boolean(findWorkflowFile(workflowFiles, "release_order")); - const canUploadDo = isImport && Boolean(data.clearance.preClearanceFinalized || hasDo); + // DO upload is un-gated — Djibouti GL may attach it at any point, any file type. + const canUploadDo = isImport; const vesselDepartureDate = "vesselDepartureDate" in data.clearance ? (data.clearance.vesselDepartureDate ?? null) diff --git a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts index 465cc302d..fe923d016 100644 --- a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts @@ -333,6 +333,27 @@ export const contractsService = { return unwrap(response.data); }, + /** GL Djibouti uploads T1 transit documents (multi-file, post wagon allocation). */ + uploadT1Documents: async ( + bookingId: string, + files: Record, + ) => { + const form = new FormData(); + for (const [key, file] of Object.entries(files)) { + if (file) form.append(key, file); + } + const response = await client.post(C.BOOKING_T1_DOCUMENTS(bookingId), form, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return unwrap(response.data); + }, + + /** GL Ethiopia closes (accepts) the T1 document set after the train arrives. */ + closeT1: async (bookingId: string): Promise => { + const response = await client.post(C.BOOKING_T1_CLOSE(bookingId)); + return unwrap(response.data) as Freight.ClearanceT1State; + }, + // ── Path A self-clearance (Operations review) ── getOpsClearanceQueue: async (): Promise => { const response = await client.get( diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index c445d67e9..a9bcf4ecc 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -20,18 +20,17 @@ "@mantine/hooks": "^9.3.0", "@tanstack/react-query": "^5.59.0", "@tria-plc/iamui": "file:../../../local-packages/tria-plc-iamui-0.1.1.tgz", + "@vis.gl/react-google-maps": "^1.8.3", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.6.0", - "leaflet": "^1.9.4", "lucide-react": "^1.14.0", "radix-ui": "^1.4.3", "react": "19.2.6", "react-dom": "19.2.6", "react-hook-form": "^7.76.0", "react-hot-toast": "^2.6.0", - "react-leaflet": "^5.0.0", "react-phone-number-input": "^3.4.17", "react-router-dom": "^6.27.0", "recharts": "^3.8.1", @@ -44,7 +43,7 @@ "@edr/tsconfig": "workspace:*", "@hookform/devtools": "^4.4.0", "@tailwindcss/vite": "^4.3.0", - "@types/leaflet": "^1.9.21", + "@types/google.maps": "^3.65.2", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.2", diff --git a/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx index 888a09c1a..a06c4c775 100644 --- a/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx @@ -130,11 +130,6 @@ export default function InvoiceDetailPage() { const amountDue = Number(invoice.balanceAmount ?? invoice.totalAmount); const handlePay = () => { -<<<<<<< HEAD - const returnUrl = `${window.location.origin}/payment/success`; - const failureUrl = `${window.location.origin}/payment/failure`; - payMutation.mutate({ id, payload: { method: paymentMethod, returnUrl, failureUrl } }); -======= setPayModalOpen(true); }; @@ -184,7 +179,6 @@ export default function InvoiceDetailPage() { } toast.error("This invoice's source isn't linked to a booking."); } ->>>>>>> 03740ee719f22f9617379a652b26f13b6870f671 }; return ( @@ -214,20 +208,6 @@ export default function InvoiceDetailPage() { -<<<<<<< HEAD - {payable && ( - -