feat(warehouse): stamp actor as display name instead of UUID

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) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-15 15:24:05 +00:00
parent 2044312d93
commit 2647776d19
3 changed files with 29 additions and 14 deletions

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}