From 2647776d191d9c92ac3a4a22b21d5343102d94d8 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Wed, 15 Jul 2026 15:24:05 +0000 Subject: [PATCH] feat(warehouse): stamp actor as display name instead of UUID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add actorLabel(user) — resolves the authenticated user to a readable name (name → username → email → id) — and use it for the performed_by audit stamp on every warehouse action, so the activity log shows a person, not a UUID. The freight DB has no users table to join, so the name is stamped at write time. approve-delivery keeps the raw user id (it is an id argument, not the audit label). Existing rows keep their prior value; this applies going forward. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../modules/warehouses/current-actor.util.ts | 13 +++++++++ .../warehouse-inventory.controller.ts | 27 ++++++++++--------- .../warehouse-invoice.controller.ts | 3 ++- 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 apps/edr-freight-api/src/modules/warehouses/current-actor.util.ts diff --git a/apps/edr-freight-api/src/modules/warehouses/current-actor.util.ts b/apps/edr-freight-api/src/modules/warehouses/current-actor.util.ts new file mode 100644 index 000000000..f4d04a6a5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/warehouses/current-actor.util.ts @@ -0,0 +1,13 @@ +import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; + +/** + * Human-readable actor label for audit stamps (`performed_by` / `moved_by`). + * Prefers a display name, then username/email, so the activity log shows a + * person rather than a UUID. Returns undefined when there is no authenticated + * user (internal/cron calls), letting callers fall back to their prior value. + */ +export function actorLabel(user?: TCurrentUser | null): string | undefined { + if (!user) return undefined; + const name = user.name?.en?.trim() || user.name?.am?.trim(); + return name || user.username || user.email || user.id || undefined; +} diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts index f8e9208af..499e40f2e 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts @@ -4,6 +4,7 @@ import type { Response } from 'express'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; +import { actorLabel } from './current-actor.util'; import { BookingStaff } from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { BulkReceiveDto } from './dto/bulk-receive.dto'; @@ -130,7 +131,7 @@ export class WarehouseInventoryController { @BookingStaff(FREIGHT_PERMS.warehouseInventory.receive) @ApiOperation({ summary: 'Bulk-receive selected eligible PAID bookings into a location' }) receiveBulk(@Body() dto: BulkReceiveDto, @CurrentUser() user: TCurrentUser) { - dto.performedBy = user?.id ?? dto.performedBy; + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.bulkReceive(dto); } @@ -178,14 +179,14 @@ export class WarehouseInventoryController { @Body() dto: { inventoryIds: string[]; performedBy?: string }, @CurrentUser() user: TCurrentUser, ) { - return this.inventoryService.loadItemsOntoTrain(scheduleId, dto.inventoryIds ?? [], user?.id ?? dto.performedBy); + return this.inventoryService.loadItemsOntoTrain(scheduleId, dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy); } @Post('bulk-dispatch-export') @BookingStaff(FREIGHT_PERMS.warehouseInventory.dispatch) @ApiOperation({ summary: 'Bulk-dispatch loaded EXPORT inventory (LOADED → DISPATCHED)' }) bulkDispatchExport(@Body() dto: { inventoryIds: string[]; performedBy?: string }, @CurrentUser() user: TCurrentUser) { - return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], user?.id ?? dto.performedBy); + return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy); } @Post('bulk-mark-inspected') @@ -213,7 +214,7 @@ export class WarehouseInventoryController { @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { - return this.inventoryService.gateClearance(id, user?.id ?? performedBy); + return this.inventoryService.gateClearance(id, actorLabel(user) ?? performedBy); } @Get('import/arrive-queue') @@ -241,7 +242,7 @@ export class WarehouseInventoryController { }, @CurrentUser() user: TCurrentUser) { return this.inventoryService.autoUnloadArrivedBookings( dto.scheduleId, - user?.id ?? dto.performedBy, + actorLabel(user) ?? dto.performedBy, dto.warehouseId, dto.assignments, ); @@ -284,7 +285,7 @@ export class WarehouseInventoryController { @BookingStaff(FREIGHT_PERMS.warehouseInventory.unload) @ApiOperation({ summary: 'Unload all eligible export items assigned to an arrived Djibouti-side train' }) autoUnloadExportAtDjibouti(@Body() dto: { scheduleId: string; performedBy?: string }, @CurrentUser() user: TCurrentUser) { - return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, user?.id ?? dto.performedBy); + return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, actorLabel(user) ?? dto.performedBy); } @Get('import/pickup-ready-queue') @@ -312,7 +313,7 @@ export class WarehouseInventoryController { @BookingStaff(FREIGHT_PERMS.warehouseInventory.receive) @ApiOperation({ summary: 'Receive inventory at a warehouse location' }) receive(@Body() dto: ReceiveWarehouseInventoryDto, @CurrentUser() user: TCurrentUser) { - dto.performedBy = user?.id ?? dto.performedBy; + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.receive(dto); } @@ -320,7 +321,7 @@ export class WarehouseInventoryController { @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Reserve stored inventory for a PAID booking' }) reserve(@Body() dto: ReserveInventoryDto, @CurrentUser() user: TCurrentUser) { - dto.performedBy = user?.id ?? dto.performedBy; + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.reserve(dto); } @@ -356,7 +357,7 @@ export class WarehouseInventoryController { @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Mark received inventory as STORED (optional explicit warehouse/yard/zone)' }) store(@Param('id', ParseUUIDPipe) id: string, @Body() dto: StoreInventoryDto, @CurrentUser() user: TCurrentUser) { - return this.inventoryService.store(id, user?.id ?? dto.performedBy, dto); + return this.inventoryService.store(id, actorLabel(user) ?? dto.performedBy, dto); } @Post(':id/ready-for-loading') @@ -367,7 +368,7 @@ export class WarehouseInventoryController { @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { - return this.inventoryService.readyForLoading(id, user?.id ?? performedBy); + return this.inventoryService.readyForLoading(id, actorLabel(user) ?? performedBy); } @Post(':id/load') @@ -385,7 +386,7 @@ export class WarehouseInventoryController { @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { - return this.inventoryService.readyForPickup(id, user?.id ?? performedBy); + return this.inventoryService.readyForPickup(id, actorLabel(user) ?? performedBy); } @Post(':id/release') @@ -514,7 +515,7 @@ export class WarehouseInventoryController { @BookingStaff(FREIGHT_PERMS.warehouseInventory.deliver) @ApiOperation({ summary: 'Deliver import goods to the customer + capture proof of delivery' }) deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DeliverInventoryDto, @CurrentUser() user: TCurrentUser) { - dto.performedBy = user?.id ?? dto.performedBy; + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.deliver(id, dto); } @@ -526,6 +527,6 @@ export class WarehouseInventoryController { @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { - return this.inventoryService.dispatch(id, user?.id ?? performedBy); + return this.inventoryService.dispatch(id, actorLabel(user) ?? performedBy); } } diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts index 6ea50db1b..4ad469ce0 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts @@ -4,6 +4,7 @@ import type { Response } from 'express'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; +import { actorLabel } from './current-actor.util'; import { BookingStaff } from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { PayInvoiceDto as GatewayPayInvoiceDto } from '../billing/dto/pay-invoice.dto'; @@ -20,7 +21,7 @@ export class WarehouseInvoiceController { @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.generate) @ApiOperation({ summary: 'Generate a warehouse fee invoice from Batch 5 fee calculation' }) generate(@Param('id', ParseUUIDPipe) id: string, @Body() dto: GenerateInvoiceDto, @CurrentUser() user: TCurrentUser) { - dto.performedBy = user?.id ?? dto.performedBy; + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.invoiceService.generateForInventory(id, dto); }