Merge branch 'dev'

This commit is contained in:
Marshal
2026-08-13 14:14:31 +00:00
219 changed files with 13585 additions and 5559 deletions

View File

@@ -28,13 +28,18 @@ import type { InterchangeDocument } from '../interchange-documents/entities/inte
import { LastMileService } from '../last-mile/last-mile.service';
import { UpdateLastMileDto } from '../last-mile/dto/update-last-mile.dto';
import { NotificationsService } from '../notifications/notifications.service';
import { sendCompanyChannels } from '../notifications/notify-company.util';
import {
sendCompanyChannels,
notifyCarriageAcceptanceReady as notifyCarriageAcceptanceReadyShared,
} from '../notifications/notify-company.util';
import {
companyNotifyPhoneExpr,
primaryContactUserJoin,
} from '../notifications/resolve-company-phone.util';
import { SignaturesService } from '../signatures/signatures.service';
import { StampSettingsService } from '../stamp-settings/stamp-settings.service';
import { LogoSettingsService } from '../logo-settings/logo-settings.service';
import { logoImageCss, logoMarkup } from '../billing/documents/logo-markup.util';
import { sealClass, sealImageCss, sealMarkup } from '../billing/documents/seal-markup.util';
import { BulkInspectDto } from './dto/bulk-inspect.dto';
import { BulkReceiveDto, TruckEntranceDto } from './dto/bulk-receive.dto';
@@ -417,6 +422,7 @@ export class WarehouseInventoryService {
private readonly inbox: NotificationInboxService,
private readonly events: EventEmitter2,
private readonly stampSettings: StampSettingsService,
private readonly logoSettings: LogoSettingsService,
) {}
/**
@@ -3748,6 +3754,7 @@ export class WarehouseInventoryService {
reference,
issuedAt,
stampImageUrl: await this.stampSettings.getStampImageUrl(),
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
bookingReference,
bookingStatus: row?.bookingStatus ?? null,
customerName: row?.customerName ?? null,
@@ -4219,6 +4226,7 @@ export class WarehouseInventoryService {
const html = this.buildGrnDocumentHtml({
grnNumber: row.grnNumber,
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
receivedAt: row.receivedAt ? new Date(row.receivedAt) : new Date(),
bookingReference: row.bookingReference ?? row.bookingId ?? 'N/A',
bookingStatus: row.bookingStatus ?? null,
@@ -4803,6 +4811,7 @@ export class WarehouseInventoryService {
reference,
handedOverAt,
stampImageUrl: await this.stampSettings.getStampImageUrl(),
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
bookingReference,
bookingStatus: row.bookingStatus ?? null,
customerName: row.customerName ?? null,
@@ -5152,6 +5161,7 @@ export class WarehouseInventoryService {
activityType: 'INVENTORY_DISPATCHED',
description: 'Inventory dispatched',
performedBy,
freeCapacity: true,
});
}
@@ -5436,6 +5446,8 @@ export class WarehouseInventoryService {
description: string;
performedBy?: string;
preloaded?: WarehouseInventory;
/** Cargo physically leaves the warehouse on this transition — free up capacity (mirrors deliver()). */
freeCapacity?: boolean;
},
): Promise<WarehouseInventory> {
const item = opts.preloaded ?? (await this.findById(id));
@@ -5446,6 +5458,20 @@ export class WarehouseInventoryService {
status: to,
[opts.timestampField]: new Date(),
});
if (opts.freeCapacity) {
const weight = Number(item.weight) || 0;
const volume = Number(item.volume) || 0;
const containerCount = item.containerId ? Math.round(Number(item.quantity) || 0) : 0;
await this.applyCapacityDelta(
manager,
{ warehouseId: item.warehouseId, yardId: item.yardId, zoneId: item.zoneId },
-weight,
-volume,
-containerCount,
);
}
await this.activityLog.record(
{
activityType: opts.activityType,
@@ -5484,6 +5510,8 @@ export class WarehouseInventoryService {
zone: string | null;
inventoryStatus: string | null;
receiveSummary: string | null;
/** The one global company logo; null renders the plain text brand. */
logoImageUrl?: string | null;
}): string {
const esc = (value: unknown) =>
String(value ?? '-')
@@ -5540,6 +5568,7 @@ export class WarehouseInventoryService {
.ref { text-align: right; font-size: 11px; color: #334155; padding-top: 8px; }
.ref strong { display: block; color: #061323; font-size: 18px; margin: 5px 0 8px; letter-spacing: .02em; }
.rule { height: 3px; background: #0f766e; margin: 16px 0 22px; }
${logoImageCss()}
.notice { width: 76%; margin: 0 0 18px; padding: 13px 18px; background: #f0fdfa; border: 1px solid #5eead4; border-left: 5px solid #0f766e; font-size: 13px; line-height: 1.45; }
.section-title { margin: 18px 0 8px; font-size: 13px; font-weight: 800; color: #0f766e; text-transform: uppercase; letter-spacing: .12em; }
table { width: 100%; border-collapse: collapse; }
@@ -5553,6 +5582,7 @@ export class WarehouseInventoryService {
<body>
<div class="top">
<div>
${logoMarkup(data.logoImageUrl)}
<div class="brand">Ethio-Djibouti Railway S.C.</div>
<h1>Goods Received Note</h1>
<div class="subtitle">Warehouse receiving confirmation</div>
@@ -5610,6 +5640,8 @@ export class WarehouseInventoryService {
truckWeightTons?: number | null;
/** The one global company stamp; null falls back to the drawn text seal. */
stampImageUrl?: string | null;
/** The one global company logo; null renders the plain text brand. */
logoImageUrl?: string | null;
}): string {
const esc = (value: unknown) =>
String(value ?? '-')
@@ -5697,12 +5729,14 @@ export class WarehouseInventoryService {
.seal::before { content: ""; position: absolute; width: 78px; height: 78px; border: 1px solid #17633a; border-radius: 999px; }
.seal span { position: relative; }
${sealImageCss()}
${logoImageCss()}
</style>
</head>
<body>
<div class="doc">
<div class="top">
<div>
${logoMarkup(data.logoImageUrl)}
<div class="brand">Ethio-Djibouti Railway S.C.</div>
<h1>Warehouse Release / Exit Paper</h1>
<div class="subtitle">Official gate clearance and warehouse exit authorization</div>
@@ -5772,6 +5806,8 @@ export class WarehouseInventoryService {
} | null;
/** The one global company stamp; null falls back to the drawn text seal. */
stampImageUrl?: string | null;
/** The one global company logo; null renders the plain text brand. */
logoImageUrl?: string | null;
}): string {
const esc = (value: unknown) =>
String(value ?? '-')
@@ -5850,11 +5886,13 @@ export class WarehouseInventoryService {
.seal::before { content: ""; position: absolute; width: 78px; height: 78px; border: 1px solid #17633a; border-radius: 999px; }
.seal span { position: relative; }
${sealImageCss()}
${logoImageCss()}
</style>
</head>
<body>
<div class="top">
<div>
${logoMarkup(data.logoImageUrl)}
<div class="brand">Ethio-Djibouti Railway S.C.</div>
<h1>Import Goods Handover Document</h1>
<div class="subtitle">EDR to customer warehouse handover</div>
@@ -6167,26 +6205,13 @@ export class WarehouseInventoryService {
* fires right after receive, not at marshalling.
*/
private async notifyCarriageAcceptanceReady(bookingId: string): Promise<void> {
try {
const [b]: Array<{ companyId: string | null; reference: string }> = await this.dataSource.query(
`SELECT company_id AS "companyId", reference FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL`,
[bookingId],
);
if (!b?.companyId) return;
const body = `Your carriage acceptance sheet for booking ${b.reference} is ready to download from the portal.`;
await this.inbox.notify({
recipients: { companyId: b.companyId },
audience: NotificationAudience.PORTAL,
type: NotificationType.DOCUMENT_ACTION,
title: 'Carriage acceptance sheet ready',
body,
link: `/bookings/${bookingId}`,
data: { bookingId, reference: b.reference },
});
await sendCompanyChannels(this.dataSource, this.notifications, b.companyId, body);
} catch (err) {
this.logger.warn(`Carriage acceptance ready notify failed for ${bookingId}: ${(err as Error).message}`);
}
await notifyCarriageAcceptanceReadyShared(
this.dataSource,
this.notifications,
this.inbox,
bookingId,
this.logger,
);
}
private async notifyOwnerInventoryReceived(params: {