From 9fcbf2783aab2db8194ff61508010b4e1ff89555 Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 08:33:51 +0000 Subject: [PATCH 01/10] booking id --- .../src/modules/first-mile/first-mile.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index a4ead8c2a..904ebf92f 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -37,7 +37,7 @@ export class FirstMileService { * unknown or the booking has not reached PAID status. */ async acceptBooking(bookingReference: string): Promise { - const booking = await this.bookingsRepository.findByReference(bookingReference); + const booking = await this.bookingsRepository.findById(bookingReference); if (!booking) { return null; From b1e0a403a6f01f795404f3f7b0b3895fd258d671 Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 09:02:09 +0000 Subject: [PATCH 02/10] fix --- .../src/pages/operations/FirstMilePage.tsx | 19 +++++++++++++------ .../src/services/first-mile.service.ts | 8 ++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 33f7300f4..c7b7f5f8a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -91,8 +91,9 @@ const cargoDesc = (r: FirstMileRecord) => { }; const priceAmount = (r: FirstMileRecord) => r.booking?.totalAmount ?? r.advancedPayment; +// First-mile destination is the origin yard (pickup → origin yard) const destinationYardName = (r: FirstMileRecord) => - r.booking?.destinationYard?.name ?? "—"; + r.booking?.originYard?.label ?? "—"; const contactPersonName = (r: FirstMileRecord) => r.booking?.company?.contactPersonName ?? "—"; const contactPhone = (r: FirstMileRecord) => @@ -102,7 +103,7 @@ const requestedDate = (r: FirstMileRecord) => { return d ? new Date(d).toISOString().slice(0, 10) : "—"; }; const serviceTypeName = (r: FirstMileRecord) => - r.booking?.serviceType?.name ?? "—"; + r.booking?.serviceType?.label ?? "—"; const InfoRow = ({ label, value }: { label: string; value: string }) => ( @@ -135,7 +136,7 @@ const BookingInfo = ({ record }: { record: FirstMileRecord }) => ( - + @@ -587,6 +588,12 @@ const FirstMilePage = () => { meta: { headerClassName, cellClassName }, cell: ({ row }) => pickupLocation(row.original), }, + { + id: "destination", + header: "Destination", + meta: { headerClassName, cellClassName }, + cell: ({ row }) => destinationYardName(row.original), + }, { id: "cargo", header: "Cargo", @@ -903,7 +910,7 @@ const FirstMilePage = () => { - {b.originYard?.name ?? "—"} → {b.destinationYard?.name ?? "—"} + {b.originYard?.label ?? "—"} → {b.destinationYard?.label ?? "—"} {formatPrice(b.totalAmount)} @@ -935,8 +942,8 @@ const FirstMilePage = () => { - - + + diff --git a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts index f761719ae..b16287124 100644 --- a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts @@ -18,10 +18,10 @@ export interface FirstMileBooking { totalAmount: number; scheduledDate?: string | null; company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null; - serviceType?: { id: string; name?: string } | null; - originYard?: { id: string; name?: string } | null; - destinationYard?: { id: string; name?: string } | null; - cargoType?: { id: string; name?: string } | null; + serviceType?: { id: string; label?: string } | null; + originYard?: { id: string; label?: string } | null; + destinationYard?: { id: string; label?: string } | null; + cargoType?: { id: string; label?: string } | null; } export interface FirstMileVehicle { From 49ae46934afda7867d2f716aebe13061d683bfd0 Mon Sep 17 00:00:00 2001 From: marshal Date: Wed, 24 Jun 2026 12:16:03 +0300 Subject: [PATCH 03/10] fix(freight-api): call acceptBooking after payment completion --- apps/edr-freight-api/src/modules/payment/payment.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index 1f95c6253..582e7467e 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -35,6 +35,7 @@ import { } from "./payments.dto"; import { BookingBatchService } from "../train-scheduling/booking-batch.service"; import { DropdownSettingsService } from "../dropdown-settings/dropdown-settings.service"; +import { FirstMileService } from "../first-mile/first-mile.service"; /** Setting code holding the global ordering window (months) for general contracts. */ const CONTRACT_PERIOD_SETTING_CODE = "general_contract_period"; @@ -60,6 +61,7 @@ export class PaymentService { @Inject(forwardRef(() => BookingBatchService)) private readonly bookingBatchService: BookingBatchService, private readonly dropdownSettings: DropdownSettingsService, + private readonly firstMileService: FirstMileService, ) { } /** Configured general-contract ordering window in months (defaults to 3). */ @@ -342,6 +344,8 @@ export class PaymentService { ? { paymentStatus: "PAID", status: "CONTRACT_ACTIVE", expiresAt: contractExpiresAt } : { paymentStatus: "PAID", status: "PAID" }, ); + await this.firstMileService.acceptBooking(input.bookingId); + }); if (isGeneralContract) { From 79860c807c82fd7617256a7f9e9d812a96cb9fb9 Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 09:30:11 +0000 Subject: [PATCH 04/10] fix --- .../1810000000002-AddVehicleCodeAndPlates.ts | 35 +++++++++++++++++++ .../vehicles/dto/create-vehicle.dto.ts | 12 +++++++ .../vehicles/entities/vehicle.entity.ts | 9 +++++ .../src/pages/fleet/config/vehicles.ts | 9 +++++ .../src/pages/operations/FirstMilePage.tsx | 17 ++++++--- .../src/pages/operations/LastMilePage.tsx | 17 ++++++--- .../src/services/first-mile.service.ts | 3 ++ .../src/services/last-mile.service.ts | 3 ++ .../src/services/vehicles.service.ts | 3 ++ 9 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1810000000002-AddVehicleCodeAndPlates.ts diff --git a/apps/edr-freight-api/src/migrations/1810000000002-AddVehicleCodeAndPlates.ts b/apps/edr-freight-api/src/migrations/1810000000002-AddVehicleCodeAndPlates.ts new file mode 100644 index 000000000..a2a1aae17 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1810000000002-AddVehicleCodeAndPlates.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddVehicleCodeAndPlates1810000000002 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const hasCode = await queryRunner.hasColumn('freight.vehicles', 'code'); + if (!hasCode) { + await queryRunner.addColumn( + 'freight.vehicles', + new TableColumn({ name: 'code', type: 'varchar', isNullable: true }), + ); + } + + const hasPower = await queryRunner.hasColumn('freight.vehicles', 'power_plate_no'); + if (!hasPower) { + await queryRunner.addColumn( + 'freight.vehicles', + new TableColumn({ name: 'power_plate_no', type: 'varchar', isNullable: true }), + ); + } + + const hasTrailer = await queryRunner.hasColumn('freight.vehicles', 'trailer_plate_no'); + if (!hasTrailer) { + await queryRunner.addColumn( + 'freight.vehicles', + new TableColumn({ name: 'trailer_plate_no', type: 'varchar', isNullable: true }), + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('freight.vehicles', 'trailer_plate_no'); + await queryRunner.dropColumn('freight.vehicles', 'power_plate_no'); + await queryRunner.dropColumn('freight.vehicles', 'code'); + } +} diff --git a/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts b/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts index 8a2ad8519..55e12047b 100644 --- a/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts +++ b/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts @@ -37,4 +37,16 @@ export class CreateVehicleDto { @IsOptional() @IsString() assignedDriverName?: string; + + @IsOptional() + @IsString() + code?: string; + + @IsOptional() + @IsString() + powerPlateNo?: string; + + @IsOptional() + @IsString() + trailerPlateNo?: string; } diff --git a/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts index 005118b0a..713edfc64 100644 --- a/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts +++ b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts @@ -62,4 +62,13 @@ export class Vehicle extends BaseEntity { @Column({ name: 'assigned_driver_name', nullable: true }) assignedDriverName?: string; + + @Column({ name: 'code', nullable: true }) + code?: string; + + @Column({ name: 'power_plate_no', nullable: true }) + powerPlateNo?: string; + + @Column({ name: 'trailer_plate_no', nullable: true }) + trailerPlateNo?: string; } diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts index 689d278a2..3e220040f 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/vehicles.ts @@ -47,7 +47,10 @@ export const vehiclesConfig: FleetResourceConfig = { ], searchKeys: ["plateNumber", "registrationNumber", "manufacturer", "model", "vehicleType", "status"], columns: [ + { id: "code", header: "Code", accessorKey: "code", format: "code", size: 110 }, { id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", format: "code", size: 130 }, + { id: "powerPlateNo", header: "Power Plate No", accessorKey: "powerPlateNo", format: "code", size: 140 }, + { id: "trailerPlateNo", header: "Trailer Plate No", accessorKey: "trailerPlateNo", format: "code", size: 140 }, { id: "registrationNumber", header: "Registration", accessorKey: "registrationNumber", format: "code", size: 140 }, { id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", format: "code", size: 140 }, { id: "model", header: "Model", accessorKey: "model", format: "code", size: 120 }, @@ -59,7 +62,10 @@ export const vehiclesConfig: FleetResourceConfig = { { id: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 }, ], formFields: [ + { name: "code", label: "Code", type: "text" }, { name: "plateNumber", label: "Plate Number", type: "text", required: true }, + { name: "powerPlateNo", label: "Power Plate No", type: "text" }, + { name: "trailerPlateNo", label: "Trailer Plate No", type: "text" }, { name: "vehicleType", label: "Vehicle Type", type: "select", required: true, options: VEHICLE_TYPE_OPTIONS }, { name: "manufacturer", label: "Manufacturer", type: "text", required: true }, { name: "model", label: "Model", type: "text", required: true }, @@ -70,7 +76,10 @@ export const vehiclesConfig: FleetResourceConfig = { { name: "description", label: "Description", type: "textarea" }, ], emptyValues: { + code: "", plateNumber: "", + powerPlateNo: "", + trailerPlateNo: "", vehicleType: "TRUCK", manufacturer: "", model: "", diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index c7b7f5f8a..ccd94137e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -75,7 +75,11 @@ const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [ const vehicleLabel = (record: FirstMileRecord) => { if (!record.vehicle) return null; const v = record.vehicle; - return `${v.manufacturer} ${v.model} (${v.plateNumber})`; + const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber]; + if (v.code) parts.unshift(v.code); + const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / "); + if (plates) parts.push(plates); + return parts.join(" · "); }; const isAssigned = (record: FirstMileRecord) => Boolean(record.vehicleId); @@ -346,10 +350,13 @@ const FirstMilePage = () => { const vehicleOptions = useMemo( () => - (Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => ({ - value: v.id, - label: `${v.manufacturer} ${v.model} (${v.plateNumber})`, - })), + (Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => { + const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber]; + if (v.code) parts.unshift(v.code); + const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / "); + if (plates) parts.push(plates); + return { value: v.id, label: parts.join(" · ") }; + }), [vehiclesData], ); diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index a5ad12c5b..7a68e447b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -71,7 +71,11 @@ const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [ const vehicleLabel = (record: LastMileRecord) => { if (!record.vehicle) return null; const v = record.vehicle; - return `${v.manufacturer} ${v.model} (${v.plateNumber})`; + const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber]; + if (v.code) parts.unshift(v.code); + const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / "); + if (plates) parts.push(plates); + return parts.join(" · "); }; const isAssigned = (record: LastMileRecord) => Boolean(record.vehicleId); @@ -313,10 +317,13 @@ const LastMilePage = () => { const vehicleOptions = useMemo( () => - (Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => ({ - value: v.id, - label: `${v.manufacturer} ${v.model} (${v.plateNumber})`, - })), + (Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => { + const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber]; + if (v.code) parts.unshift(v.code); + const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / "); + if (plates) parts.push(plates); + return { value: v.id, label: parts.join(" · ") }; + }), [vehiclesData], ); diff --git a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts index b16287124..23f5261c8 100644 --- a/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/first-mile.service.ts @@ -29,6 +29,9 @@ export interface FirstMileVehicle { plateNumber: string; manufacturer: string; model: string; + code?: string | null; + powerPlateNo?: string | null; + trailerPlateNo?: string | null; } export interface FirstMileRecord { diff --git a/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts b/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts index 5d8b60df2..6872d300e 100644 --- a/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts @@ -29,6 +29,9 @@ export interface LastMileVehicle { plateNumber: string; manufacturer: string; model: string; + code?: string | null; + powerPlateNo?: string | null; + trailerPlateNo?: string | null; } export interface LastMileRecord { diff --git a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts index d5f0308f9..b9d5129e3 100644 --- a/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/vehicles.service.ts @@ -26,6 +26,9 @@ export interface Vehicle { capacity: number; status: VehicleStatus; description?: string | null; + code?: string | null; + powerPlateNo?: string | null; + trailerPlateNo?: string | null; createdAt: string; updatedAt: string; } From 833e55385dc1a246517f3184770bd5aae88ae24c Mon Sep 17 00:00:00 2001 From: yaschalew Date: Wed, 24 Jun 2026 12:30:43 +0300 Subject: [PATCH 05/10] fix --- apps/edr-freight-web/backoffice/src/constants/apiConfig.ts | 4 ++-- apps/edr-freight-web/portal/src/constants/apiConfig.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts b/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts index 030b051a1..a7c8670cf 100644 --- a/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts +++ b/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts @@ -1,3 +1,3 @@ -export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; +// export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; -// export const API_BASE_URL = 'http://localhost:3001'; +export const API_BASE_URL = 'http://localhost:3001'; diff --git a/apps/edr-freight-web/portal/src/constants/apiConfig.ts b/apps/edr-freight-web/portal/src/constants/apiConfig.ts index dce8aad63..fc9f57a58 100644 --- a/apps/edr-freight-web/portal/src/constants/apiConfig.ts +++ b/apps/edr-freight-web/portal/src/constants/apiConfig.ts @@ -1,3 +1,3 @@ -export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; -// export const API_BASE_URL = 'http://localhost:3001'; +// export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; +export const API_BASE_URL = 'http://localhost:3001'; From 8c3827c70bcbd71d46e56044fcf79b6d42800b0e Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 09:33:37 +0000 Subject: [PATCH 06/10] fix --- apps/edr-freight-api/src/modules/payment/payment.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/edr-freight-api/src/modules/payment/payment.module.ts b/apps/edr-freight-api/src/modules/payment/payment.module.ts index e496bb867..e21309246 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.module.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.module.ts @@ -19,6 +19,7 @@ import { InternalPaymentController } from "./internal-payment.controller"; import { ServiceAuthGuard } from "../../common/guards/service-auth.guard"; import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.module"; import { DropdownSettingsModule } from "../dropdown-settings/dropdown-settings.module"; +import { FirstMileModule } from "../first-mile/first-mile.module"; import { PaymentWebhookEventEntity } from "./entities/payment-webhook-event.entity"; import { PaymentRefundEntity } from "./entities/payment-refund.entity"; @@ -29,6 +30,7 @@ const FREIGHT_QUEUE = PAYMENT_QUEUES[PaymentServiceEnum.FREIGHT]; HttpModule.register({ timeout: 10_000 }), ConfigModule, DropdownSettingsModule, + FirstMileModule, forwardRef(() => TrainSchedulingModule), TypeOrmModule.forFeature([PaymentWebhookEventEntity, PaymentRefundEntity]), RabbitMQModule.forRootAsync({ From 9c7300dcaceb4aa27a8780c3c43047c2d86e7a50 Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 09:35:54 +0000 Subject: [PATCH 07/10] fix --- .../src/modules/first-mile/first-mile.module.ts | 4 ++-- apps/edr-freight-api/src/modules/payment/payment.module.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts index 713efa52d..382499da8 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BookingsModule } from '../bookings/bookings.module'; @@ -8,7 +8,7 @@ import { FirstMileRepository } from './first-mile.repository'; import { FirstMileService } from './first-mile.service'; @Module({ - imports: [TypeOrmModule.forFeature([FirstMile]), BookingsModule], + imports: [TypeOrmModule.forFeature([FirstMile]), forwardRef(() => BookingsModule)], controllers: [FirstMileController], providers: [FirstMileRepository, FirstMileService], exports: [FirstMileRepository, FirstMileService], diff --git a/apps/edr-freight-api/src/modules/payment/payment.module.ts b/apps/edr-freight-api/src/modules/payment/payment.module.ts index e21309246..d0eb2eecb 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.module.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.module.ts @@ -30,7 +30,7 @@ const FREIGHT_QUEUE = PAYMENT_QUEUES[PaymentServiceEnum.FREIGHT]; HttpModule.register({ timeout: 10_000 }), ConfigModule, DropdownSettingsModule, - FirstMileModule, + forwardRef(() => FirstMileModule), forwardRef(() => TrainSchedulingModule), TypeOrmModule.forFeature([PaymentWebhookEventEntity, PaymentRefundEntity]), RabbitMQModule.forRootAsync({ From 0e351fa0b4286b0ac30d2794a511274d50ebf587 Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 10:02:16 +0000 Subject: [PATCH 08/10] sms --- .../modules/first-mile/first-mile.module.ts | 11 +++++- .../modules/first-mile/first-mile.service.ts | 36 ++++++++++++++++++- .../notifications/notifications.service.ts | 24 +++++++++++-- .../strategies/notification.sms.strategy.ts | 17 +++++---- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts index 382499da8..bf6815af7 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.module.ts @@ -2,13 +2,22 @@ import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BookingsModule } from '../bookings/bookings.module'; +import { DriversModule } from '../drivers/drivers.module'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { VehiclesModule } from '../vehicles/vehicles.module'; import { FirstMile } from './entities/first-mile.entity'; import { FirstMileController } from './first-mile.controller'; import { FirstMileRepository } from './first-mile.repository'; import { FirstMileService } from './first-mile.service'; @Module({ - imports: [TypeOrmModule.forFeature([FirstMile]), forwardRef(() => BookingsModule)], + imports: [ + TypeOrmModule.forFeature([FirstMile]), + forwardRef(() => BookingsModule), + VehiclesModule, + DriversModule, + NotificationsModule, + ], controllers: [FirstMileController], providers: [FirstMileRepository, FirstMileService], exports: [FirstMileRepository, FirstMileService], diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index 904ebf92f..88edcc752 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -2,6 +2,9 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; +import { DriversService } from '../drivers/drivers.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { VehiclesService } from '../vehicles/vehicles.service'; import { CreateFirstMileDto } from './dto/create-first-mile.dto'; import { UpdateFirstMileDto } from './dto/update-first-mile.dto'; import { FirstMile, FirstMileStatus } from './entities/first-mile.entity'; @@ -29,6 +32,9 @@ export class FirstMileService { constructor( private readonly firstMileRepository: FirstMileRepository, private readonly bookingsRepository: BookingsRepository, + private readonly vehiclesService: VehiclesService, + private readonly driversService: DriversService, + private readonly notificationsService: NotificationsService, ) {} /** @@ -119,7 +125,7 @@ export class FirstMileService { } async update(id: string, dto: UpdateFirstMileDto): Promise { - await this.findById(id); + const existing = await this.findById(id); const updated = await this.firstMileRepository.update(id, { ...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}), @@ -135,9 +141,37 @@ export class FirstMileService { throw new NotFoundException(`First-mile record ${id} not found`); } + // Notify assigned driver when a vehicle is newly assigned + if (dto.vehicleId && dto.vehicleId !== existing.vehicleId) { + void this.notifyDriverAssignment(dto.vehicleId, existing); + } + return updated; } + private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise { + try { + const vehicle = await this.vehiclesService.findById(vehicleId); + if (!vehicle.assignedDriverId) return; + + const driver = await this.driversService.findById(vehicle.assignedDriverId); + if (!driver.phoneNumber) return; + + const booking = (record as FirstMile & { booking?: { reference?: string; firstMilePickupAddress?: string | null; originYard?: { label?: string } | null } }).booking; + + await this.notificationsService.notifyDriverVehicleAssignment({ + driverPhone: driver.phoneNumber, + driverName: `${driver.firstName} ${driver.lastName}`, + vehiclePlateNumber: vehicle.plateNumber ?? vehicleId, + bookingReference: booking?.reference ?? record.bookingId, + pickupAddress: booking?.firstMilePickupAddress, + destinationYard: booking?.originYard?.label, + }); + } catch (err) { + // Notification failure must never break the assignment flow + } + } + async remove(id: string): Promise { await this.findById(id); await this.firstMileRepository.softDelete(id); diff --git a/apps/edr-freight-api/src/modules/notifications/notifications.service.ts b/apps/edr-freight-api/src/modules/notifications/notifications.service.ts index 35e8ff07d..088f2bbe0 100644 --- a/apps/edr-freight-api/src/modules/notifications/notifications.service.ts +++ b/apps/edr-freight-api/src/modules/notifications/notifications.service.ts @@ -27,9 +27,29 @@ export class NotificationsService { if (!strategy) { throw new NotFoundException(); } - const sent = await strategy.send(recipient, message) - this.logger.log(`is sent - ${sent}`) + const sent = await strategy.send(recipient, message); + this.logger.log(`is sent - ${sent}`); } + async notifyDriverVehicleAssignment(params: { + driverPhone: string; + driverName: string; + vehiclePlateNumber: string; + bookingReference: string; + pickupAddress?: string | null; + destinationYard?: string | null; + }): Promise { + const { driverPhone, driverName, vehiclePlateNumber, bookingReference, pickupAddress, destinationYard } = params; + const message = + `Dear ${driverName}, you have been assigned to a first-mile pickup. ` + + `Booking: ${bookingReference}. Vehicle: ${vehiclePlateNumber}. ` + + (pickupAddress ? `Pickup: ${pickupAddress}. ` : '') + + (destinationYard ? `Destination: ${destinationYard}.` : ''); + try { + await this.directSend('sms', driverPhone, message); + } catch (err) { + this.logger.error(`Failed to notify driver ${driverName} (${driverPhone}): ${String(err)}`); + } + } } diff --git a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts index 2f8916845..d77aac350 100644 --- a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts +++ b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts @@ -8,16 +8,19 @@ import { firstValueFrom } from 'rxjs'; export class SmsNotificationStrategy implements NotificationStrategy { constructor(private readonly httpService: HttpService, private readonly configService: ConfigService) { } async send(recipient: string, message: string) { - const url = this.configService.get("OZIKING_SMS_URL") + const url = this.configService.get("OZIKING_SMS_URL"); const body = { to: recipient, - text: message - } + sourceId: this.configService.get("OZIKING_SOURCE_ID") ?? "EDR", + sourceName: this.configService.get("OZIKING_SOURCE_NAME") ?? "EDR Freight", + appKey: this.configService.get("OZIKING_APP_KEY") ?? "", + text: message, + callbackUrl: "", + }; const response = await firstValueFrom( - this.httpService.post( - url, - body, - ), + this.httpService.post(url, body, { + headers: { accept: "*/*", "Content-Type": "application/json" }, + }), ); return response.status === 201; From f1b99897308dc4902d2ffa657dab070acf7d1f1e Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 11:47:02 +0000 Subject: [PATCH 09/10] fix --- .../modules/first-mile/first-mile.service.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index 88edcc752..2d4079a8b 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; @@ -29,6 +29,8 @@ const SORTABLE_FIELDS: (keyof FirstMile)[] = [ @Injectable() export class FirstMileService { + private readonly logger = new Logger(FirstMileService.name); + constructor( private readonly firstMileRepository: FirstMileRepository, private readonly bookingsRepository: BookingsRepository, @@ -141,8 +143,8 @@ export class FirstMileService { throw new NotFoundException(`First-mile record ${id} not found`); } - // Notify assigned driver when a vehicle is newly assigned - if (dto.vehicleId && dto.vehicleId !== existing.vehicleId) { + // Notify assigned driver on every explicit vehicle assignment or reassignment + if (dto.vehicleId) { void this.notifyDriverAssignment(dto.vehicleId, existing); } @@ -152,23 +154,31 @@ export class FirstMileService { private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise { try { const vehicle = await this.vehiclesService.findById(vehicleId); - if (!vehicle.assignedDriverId) return; + if (!vehicle.assignedDriverId) { + this.logger.warn(`Vehicle ${vehicleId} has no assigned driver — skipping SMS`); + return; + } const driver = await this.driversService.findById(vehicle.assignedDriverId); - if (!driver.phoneNumber) return; + if (!driver.phoneNumber) { + this.logger.warn(`Driver ${vehicle.assignedDriverId} has no phone number — skipping SMS`); + return; + } const booking = (record as FirstMile & { booking?: { reference?: string; firstMilePickupAddress?: string | null; originYard?: { label?: string } | null } }).booking; await this.notificationsService.notifyDriverVehicleAssignment({ driverPhone: driver.phoneNumber, - driverName: `${driver.firstName} ${driver.lastName}`, + driverName: `${driver.firstName ?? ''} ${driver.lastName ?? ''}`.trim(), vehiclePlateNumber: vehicle.plateNumber ?? vehicleId, bookingReference: booking?.reference ?? record.bookingId, pickupAddress: booking?.firstMilePickupAddress, destinationYard: booking?.originYard?.label, }); + + this.logger.log(`SMS sent to driver ${driver.phoneNumber} for vehicle ${vehicleId} assignment`); } catch (err) { - // Notification failure must never break the assignment flow + this.logger.error(`Failed to notify driver for vehicle ${vehicleId}: ${String(err)}`); } } From 23e47c7319090d9dabe9b40b0a5d56a91f44b401 Mon Sep 17 00:00:00 2001 From: natib21 Date: Wed, 24 Jun 2026 11:52:17 +0000 Subject: [PATCH 10/10] fix --- .../notifications/notifications.module.ts | 6 +-- .../strategies/notification.sms.strategy.ts | 52 +++++++++++-------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/apps/edr-freight-api/src/modules/notifications/notifications.module.ts b/apps/edr-freight-api/src/modules/notifications/notifications.module.ts index 2ff2f9727..70e00c9ac 100644 --- a/apps/edr-freight-api/src/modules/notifications/notifications.module.ts +++ b/apps/edr-freight-api/src/modules/notifications/notifications.module.ts @@ -1,14 +1,14 @@ import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; import { NotificationsService } from "./notifications.service"; import { EmailNotificationStrategy } from "./strategies/notification.email.strategy"; import { SmsNotificationStrategy } from "./strategies/notification.sms.strategy"; -import { HttpModule } from "@nestjs/axios"; @Module({ - imports: [HttpModule], + imports: [ConfigModule], controllers: [], providers: [EmailNotificationStrategy, SmsNotificationStrategy, NotificationsService], exports: [NotificationsService], }) -export class NotificationsModule { } +export class NotificationsModule {} diff --git a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts index d77aac350..127eae5cd 100644 --- a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts +++ b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts @@ -1,28 +1,36 @@ -import { Injectable} from "@nestjs/common"; -import { NotificationStrategy } from "./notification.strategy"; -import { HttpService } from '@nestjs/axios'; +import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { firstValueFrom } from 'rxjs'; +import axios from "axios"; + +import { NotificationStrategy } from "./notification.strategy"; @Injectable() export class SmsNotificationStrategy implements NotificationStrategy { - constructor(private readonly httpService: HttpService, private readonly configService: ConfigService) { } - async send(recipient: string, message: string) { - const url = this.configService.get("OZIKING_SMS_URL"); - const body = { - to: recipient, - sourceId: this.configService.get("OZIKING_SOURCE_ID") ?? "EDR", - sourceName: this.configService.get("OZIKING_SOURCE_NAME") ?? "EDR Freight", - appKey: this.configService.get("OZIKING_APP_KEY") ?? "", - text: message, - callbackUrl: "", - }; - const response = await firstValueFrom( - this.httpService.post(url, body, { - headers: { accept: "*/*", "Content-Type": "application/json" }, - }), - ); + constructor(private readonly configService: ConfigService) {} - return response.status === 201; - } + async send(recipient: string, message: string): Promise { + const url = + this.configService.get("OZIKING_SMS_URL") ?? + "https://notification-dev.license.aafda.gov.et/api/sms-services/ozeking/sms"; + + await axios.post( + url, + { + to: recipient, + sourceId: this.configService.get("OZIKING_SOURCE_ID") ?? "EDR", + sourceName: this.configService.get("OZIKING_SOURCE_NAME") ?? "EDR Freight", + appKey: this.configService.get("OZIKING_APP_KEY") ?? "", + text: message, + callbackUrl: "", + }, + { + headers: { + accept: "*/*", + "Content-Type": "application/json", + }, + }, + ); + + return true; + } }