mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 11:18:17 +00:00
feat(eims): resolve buyer geography from the MoR location master
BuyerDetails Country/Region/City/Wereda now resolve from the Ministry's own EIMS_COUNTRY_REGION_VW master instead of the EIMS_BUYER_*_CODES env maps and the ethiopia-geo-codes table. Both invented their codes and looked names up globally, so KERSA/GORO/BABILE/BURE — each present in several zones with different LOCALITY_NOs — could be filed against the wrong jurisdiction. Resolution is hierarchical and refuses to guess: an unknown or ambiguous address raises a local validation error naming the level that failed, and never selects the first matching row. Spelling differences between EDR and MoR live in a reviewed, parent-scoped alias layer; the dataset itself stays verbatim so it remains traceable to the Ministry sheet. Resolution now runs before the counter reservation in both the single and bulk paths, so a bad company address no longer burns an EIMS sequence number. Adds eims:import-locations to regenerate the dataset from a future workbook, reporting duplicate rows and same-hierarchy code conflicts.
This commit is contained in:
@@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BookingsModule } from '../bookings/bookings.module';
|
||||
import { NotificationInboxModule } from '../notification-inbox/notification-inbox.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { WarehousesModule } from '../warehouses/warehouses.module';
|
||||
import { DjiboutiIncident } from './entities/djibouti-incident.entity';
|
||||
import { EmptyContainerReturn } from './entities/empty-container-return.entity';
|
||||
@@ -18,9 +20,12 @@ import { ImportOperationsService } from './import-operations.service';
|
||||
]),
|
||||
// WarehouseReleaseDocumentService (the shared PDF renderer) for the
|
||||
// equipment interchange receipt; BookingsModule for the customer
|
||||
// ownership check on that same route.
|
||||
// ownership check on that same route; the notification modules to tell
|
||||
// the customer their receipt is ready at handover.
|
||||
WarehousesModule,
|
||||
BookingsModule,
|
||||
NotificationInboxModule,
|
||||
NotificationsModule,
|
||||
],
|
||||
controllers: [ImportOperationsController],
|
||||
providers: [ImportOperationsService],
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { NotificationAudience, NotificationType } from '@edr/types';
|
||||
import { LogoSettingsService } from '../logo-settings/logo-settings.service';
|
||||
import { logoImageCss, logoMarkup } from '../billing/documents/logo-markup.util';
|
||||
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { sendCompanyChannels } from '../notifications/notify-company.util';
|
||||
import { WarehouseReleaseDocumentService } from '../warehouses/warehouse-release-document.service';
|
||||
import {
|
||||
CreateDjiboutiIncidentDto,
|
||||
@@ -35,6 +39,8 @@ const DAMAGE_INCIDENTS: DjiboutiIncidentType[] = [
|
||||
|
||||
@Injectable()
|
||||
export class ImportOperationsService {
|
||||
private readonly logger = new Logger(ImportOperationsService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(DjiboutiIncident)
|
||||
private readonly incidents: Repository<DjiboutiIncident>,
|
||||
@@ -44,6 +50,8 @@ export class ImportOperationsService {
|
||||
private readonly emptyReturns: Repository<EmptyContainerReturn>,
|
||||
private readonly pdfDocuments: WarehouseReleaseDocumentService,
|
||||
private readonly logoSettings: LogoSettingsService,
|
||||
private readonly inbox: NotificationInboxService,
|
||||
private readonly notifications: NotificationsService,
|
||||
) {}
|
||||
|
||||
listIncidents(bookingId?: string) {
|
||||
@@ -161,7 +169,7 @@ export class ImportOperationsService {
|
||||
|
||||
async createEmptyReturn(dto: CreateEmptyContainerReturnDto) {
|
||||
const returnDate = dto.returnDate ? new Date(dto.returnDate) : new Date();
|
||||
return this.emptyReturns.save(
|
||||
const saved = await this.emptyReturns.save(
|
||||
this.emptyReturns.create({
|
||||
containerNumber: dto.containerNumber,
|
||||
bookingId: dto.bookingId ?? null,
|
||||
@@ -180,6 +188,15 @@ export class ImportOperationsService {
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// RETURNED is the physical interchange itself — the customer's/trucker's
|
||||
// custody of the box ends here, EDR's begins. The receipt exists from this
|
||||
// point on, so tell the customer now, not at some later internal status.
|
||||
// Standalone returns (no booking) have no company to notify.
|
||||
if (saved.bookingId) {
|
||||
await this.notifyEquipmentInterchangeReady(saved);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,6 +274,34 @@ export class ImportOperationsService {
|
||||
return this.emptyReturns.findOneOrFail({ where: { id } });
|
||||
}
|
||||
|
||||
private async notifyEquipmentInterchangeReady(row: EmptyContainerReturn): Promise<void> {
|
||||
try {
|
||||
const [booking]: Array<{ companyId: string | null; reference: string }> =
|
||||
await this.emptyReturns.manager.query(
|
||||
`SELECT company_id AS "companyId", reference FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[row.bookingId],
|
||||
);
|
||||
if (!booking?.companyId) return;
|
||||
const body = `Container ${row.containerNumber} was handed over${
|
||||
row.facility ? ` at ${row.facility}` : ''
|
||||
}. Your equipment interchange receipt for booking ${booking.reference} is ready to download from the portal.`;
|
||||
await this.inbox.notify({
|
||||
recipients: { companyId: booking.companyId },
|
||||
audience: NotificationAudience.PORTAL,
|
||||
type: NotificationType.DOCUMENT_ACTION,
|
||||
title: 'Equipment interchange receipt ready',
|
||||
body,
|
||||
link: `/bookings/${row.bookingId}`,
|
||||
data: { bookingId: row.bookingId, emptyContainerReturnId: row.id },
|
||||
});
|
||||
await sendCompanyChannels(this.emptyReturns.manager.connection, this.notifications, booking.companyId, body);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Failed to notify equipment interchange ready for return ${row.id}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getEmptyReturnOrThrow(id: string): Promise<EmptyContainerReturn> {
|
||||
const row = await this.emptyReturns.findOne({ where: { id } });
|
||||
if (!row) {
|
||||
|
||||
Reference in New Issue
Block a user