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;