mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 09:30:59 +00:00
Release Order plus Storage Allocation Rule and fee
This commit is contained in:
@@ -1,14 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
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]), BookingsModule],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([FirstMile]),
|
||||
forwardRef(() => BookingsModule),
|
||||
VehiclesModule,
|
||||
DriversModule,
|
||||
NotificationsModule,
|
||||
],
|
||||
controllers: [FirstMileController],
|
||||
providers: [FirstMileRepository, FirstMileService],
|
||||
exports: [FirstMileRepository, FirstMileService],
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable, Logger, 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';
|
||||
@@ -26,9 +29,14 @@ 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,
|
||||
private readonly vehiclesService: VehiclesService,
|
||||
private readonly driversService: DriversService,
|
||||
private readonly notificationsService: NotificationsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -37,7 +45,7 @@ export class FirstMileService {
|
||||
* unknown or the booking has not reached PAID status.
|
||||
*/
|
||||
async acceptBooking(bookingReference: string): Promise<FirstMile | null> {
|
||||
const booking = await this.bookingsRepository.findByReference(bookingReference);
|
||||
const booking = await this.bookingsRepository.findById(bookingReference);
|
||||
|
||||
if (!booking) {
|
||||
return null;
|
||||
@@ -119,7 +127,7 @@ export class FirstMileService {
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateFirstMileDto): Promise<FirstMile> {
|
||||
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 +143,45 @@ export class FirstMileService {
|
||||
throw new NotFoundException(`First-mile record ${id} not found`);
|
||||
}
|
||||
|
||||
// Notify assigned driver on every explicit vehicle assignment or reassignment
|
||||
if (dto.vehicleId) {
|
||||
void this.notifyDriverAssignment(dto.vehicleId, existing);
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise<void> {
|
||||
try {
|
||||
const vehicle = await this.vehiclesService.findById(vehicleId);
|
||||
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) {
|
||||
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 ?? ''}`.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) {
|
||||
this.logger.error(`Failed to notify driver for vehicle ${vehicleId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findById(id);
|
||||
await this.firstMileRepository.softDelete(id);
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<void> {
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +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,
|
||||
text: message
|
||||
}
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post(
|
||||
url,
|
||||
body,
|
||||
),
|
||||
);
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
return response.status === 201;
|
||||
}
|
||||
async send(recipient: string, message: string): Promise<boolean> {
|
||||
const url =
|
||||
this.configService.get<string>("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<string>("OZIKING_SOURCE_ID") ?? "EDR",
|
||||
sourceName: this.configService.get<string>("OZIKING_SOURCE_NAME") ?? "EDR Freight",
|
||||
appKey: this.configService.get<string>("OZIKING_APP_KEY") ?? "",
|
||||
text: message,
|
||||
callbackUrl: "",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
accept: "*/*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DynamicModule, Module, forwardRef } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { RabbitMQModule } from "@golevelup/nestjs-rabbitmq";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import {
|
||||
PAYMENT_EVENTS_DLX,
|
||||
PAYMENT_EVENTS_EXCHANGE,
|
||||
@@ -10,17 +10,20 @@ import {
|
||||
PaymentService as PaymentServiceEnum,
|
||||
paymentServiceBindingPattern,
|
||||
} from "@edr/types";
|
||||
import { PaymentService } from "./payment.service";
|
||||
|
||||
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
||||
import { DropdownSettingsModule } from "../dropdown-settings/dropdown-settings.module";
|
||||
import { FirstMileModule } from "../first-mile/first-mile.module";
|
||||
import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.module";
|
||||
import { PaymentRefundEntity } from "./entities/payment-refund.entity";
|
||||
import { PaymentWebhookEventEntity } from "./entities/payment-webhook-event.entity";
|
||||
import { PaymentEntity } from "./entities/payment.entity";
|
||||
import { InternalPaymentController } from "./internal-payment.controller";
|
||||
import { PaymentClientService } from "./payment-client.service";
|
||||
import { PaymentController } from "./payment.controller";
|
||||
import { PaymentRepository } from "./payment.repository";
|
||||
import { PaymentEventsConsumer } from "./payment-events.consumer";
|
||||
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 { PaymentWebhookEventEntity } from "./entities/payment-webhook-event.entity";
|
||||
import { PaymentRefundEntity } from "./entities/payment-refund.entity";
|
||||
import { PaymentRepository } from "./payment.repository";
|
||||
import { PaymentService } from "./payment.service";
|
||||
|
||||
const FREIGHT_QUEUE = PAYMENT_QUEUES[PaymentServiceEnum.FREIGHT];
|
||||
|
||||
@@ -31,7 +34,7 @@ function rabbitMQImport(): DynamicModule[] {
|
||||
RabbitMQModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
uri: config.get<string>("rabbitmq.url") as string,
|
||||
uri: config.get<string>("rabbitmq.url") ?? process.env.PAYMENT_RABBITMQ_URL ?? "",
|
||||
exchanges: [
|
||||
{ name: PAYMENT_EVENTS_EXCHANGE, type: "topic", options: { durable: true } },
|
||||
{ name: PAYMENT_EVENTS_DLX, type: "topic", options: { durable: true } },
|
||||
@@ -56,8 +59,13 @@ function rabbitMQImport(): DynamicModule[] {
|
||||
HttpModule.register({ timeout: 10_000 }),
|
||||
ConfigModule,
|
||||
DropdownSettingsModule,
|
||||
forwardRef(() => FirstMileModule),
|
||||
forwardRef(() => TrainSchedulingModule),
|
||||
TypeOrmModule.forFeature([PaymentWebhookEventEntity, PaymentRefundEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
PaymentEntity,
|
||||
PaymentWebhookEventEntity,
|
||||
PaymentRefundEntity,
|
||||
]),
|
||||
...rabbitMQImport(),
|
||||
],
|
||||
providers: [
|
||||
@@ -70,4 +78,4 @@ function rabbitMQImport(): DynamicModule[] {
|
||||
controllers: [PaymentController, InternalPaymentController],
|
||||
exports: [PaymentService],
|
||||
})
|
||||
export class PaymentModule { }
|
||||
export class PaymentModule {}
|
||||
|
||||
@@ -37,4 +37,16 @@ export class CreateVehicleDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assignedDriverName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
powerPlateNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
trailerPlateNo?: string;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user