interchange document generation and acknowledgement

This commit is contained in:
hagiye
2026-06-27 13:49:41 +03:00
50 changed files with 2149 additions and 1878 deletions

View File

@@ -20,7 +20,12 @@
"seed:warehouse-demo": "ts-node -r tsconfig-paths/register src/scripts/seed-warehouse-demo.ts",
"seed:export-djibouti-interchange-demo": "ts-node -r tsconfig-paths/register src/scripts/seed-export-djibouti-interchange-demo.ts",
"seed:file-upload-settings": "ts-node -r tsconfig-paths/register src/scripts/seed-file-upload-settings.ts",
"seed:fleet-wagons": "bash ../../../docs/new/seeds/seed-fleet-wagons.sh"
"seed:fleet-wagons": "bash ../../../docs/new/seeds/seed-fleet-wagons.sh",
"iam:typeorm:cli": "cross-env MIGRATIONS_DIR=node_modules/@tria-plc/iamapi-common/dist/db/migrations/*.{ts,js} ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d ./node_modules/@tria-plc/api-common/dist/modules/typeorm/typeorm.config.js",
"iam:migration:run": "pnpm run iam:typeorm:cli migration:run",
"iam:migration:revert": "pnpm run iam:typeorm:cli migration:revert",
"iam:migration:show": "pnpm run iam:typeorm:cli migration:show",
"iam:seed:run": "cross-env APP_MODULE_PATH=./dist/app.module dotenv -- node ./node_modules/@tria-plc/iamapi-common/dist/db/seed.cli.js"
},
"dependencies": {
"@edr/api-common": "workspace:*",
@@ -39,14 +44,15 @@
"@nestjs/swagger": "^11.4.2",
"@nestjs/typeorm": "^11.0.1",
"@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.4.3.tgz",
"@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-0.7.6.tgz",
"@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-0.7.7.tgz",
"amqp-connection-manager": "^5.0.0",
"amqplib": "^2.0.1",
"axios": "^1.16.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cross-env": "^10.1.0",
"dotenv": "^17.4.2",
"dotenv-cli": "^11.0.0",
"handlebars": "^4.7.9",
"libphonenumber-js": "^1.13.6",
"minio": "7.1.3",

View File

@@ -1,4 +1,4 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm';
import { BookingsRepository } from '../bookings/bookings.repository';
@@ -44,30 +44,19 @@ export class FirstMileService {
* paid before any first-mile work proceeds. Throws if the reference is
* unknown or the booking has not reached PAID status.
*/
async acceptBooking(bookingId: string): Promise<FirstMile | null> {
async acceptBooking(bookingId: string): Promise<FirstMile> {
const booking = await this.bookingsRepository.findById(bookingId, {
relations: { serviceType: true },
});
if (!booking) {
return null;
throw new NotFoundException(`Booking ${bookingId} not found`);
}
if (booking.paymentStatus !== 'PAID') {
return null;
}
if (!this.bookingRequestsFirstMile(booking)) {
return null;
}
return this.create({
bookingId: booking.id,
advancedPayment: 0,
});
return this.acceptEligibleBooking(booking);
}
async acceptBookingByReference(bookingReference: string): Promise<FirstMile | null> {
async acceptBookingByReference(bookingReference: string): Promise<FirstMile> {
const [booking] = await this.bookingsRepository.findAll({
where: { reference: bookingReference },
relations: { serviceType: true },
@@ -75,15 +64,39 @@ export class FirstMileService {
});
if (!booking) {
return null;
throw new NotFoundException(`Booking ${bookingReference} not found`);
}
return this.acceptEligibleBooking(booking);
}
/**
* Shared accept path: validates payment + first-mile eligibility, rejects an
* already-assigned booking, then creates the first-mile record. Throws a
* meaningful HTTP error instead of returning null so the client can surface
* why an accept was refused.
*/
private async acceptEligibleBooking(booking: {
id: string;
reference?: string;
paymentStatus?: string | null;
tradeDirection?: string | null;
firstMilePickupAddress?: string | null;
serviceType?: { includesFirstMile?: boolean | null } | null;
}): Promise<FirstMile> {
const label = booking.reference ?? booking.id;
if (booking.paymentStatus !== 'PAID') {
return null;
throw new BadRequestException(`Booking ${label} is not paid`);
}
if (!this.bookingRequestsFirstMile(booking)) {
return null;
throw new BadRequestException(`Booking ${label} does not require a first mile`);
}
const existing = await this.findByBookingId(booking.id);
if (existing) {
throw new ConflictException(`Booking ${label} already has a first-mile assignment`);
}
return this.create({
@@ -174,10 +187,17 @@ export class FirstMileService {
}
private bookingRequestsFirstMile(booking: {
tradeDirection?: string | null;
firstMilePickupAddress?: string | null;
serviceType?: { includesFirstMile?: boolean | null } | null;
}): boolean {
return Boolean(booking.firstMilePickupAddress?.trim() || booking.serviceType?.includesFirstMile);
// Export bookings always need a first mile (pickup → origin yard); the
// pickup address is captured at assignment time, not required upfront.
return Boolean(
booking.tradeDirection === 'EXPORT' ||
booking.firstMilePickupAddress?.trim() ||
booking.serviceType?.includesFirstMile,
);
}
async update(id: string, dto: UpdateFirstMileDto): Promise<FirstMile> {