mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 12:28:21 +00:00
90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { DataSource, EntityManager } from 'typeorm';
|
|
import { Freight } from '@edr/types';
|
|
|
|
import { resolveIamUserNames } from '../../common/utils/iam-user-name.util';
|
|
import {
|
|
BookingClearanceEvent,
|
|
ClearanceEventActorType,
|
|
} from './entities/booking-clearance-event.entity';
|
|
|
|
export interface RecordClearanceEventInput {
|
|
bookingId: string;
|
|
action: string;
|
|
/** Human sentence for the History tab, frozen at write time. */
|
|
label: string;
|
|
actorType?: ClearanceEventActorType;
|
|
/** IAM user id (staff or portal customer); name is resolved here. */
|
|
actorId?: string | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
/** Join the caller's transaction so the event commits (or rolls back) with the action. */
|
|
manager?: EntityManager;
|
|
}
|
|
|
|
/**
|
|
* The clearance History tab's write/read path. Every clearance mutation calls
|
|
* {@link record} — document reviews, phased workflow steps, customer charges.
|
|
* Recording is deliberately NOT fire-and-forget: the insert shares the caller's
|
|
* transaction when a manager is passed, and otherwise a failed insert fails the
|
|
* action, because a silent gap in an audit trail is worse than a retry.
|
|
*/
|
|
@Injectable()
|
|
export class ClearanceEventService {
|
|
private readonly logger = new Logger(ClearanceEventService.name);
|
|
|
|
constructor(private readonly dataSource: DataSource) {}
|
|
|
|
async record(input: RecordClearanceEventInput): Promise<void> {
|
|
const mg = input.manager ?? this.dataSource.manager;
|
|
const actorName = input.actorId
|
|
? ((await resolveIamUserNames(this.dataSource, [input.actorId])).get(
|
|
input.actorId,
|
|
) ?? null)
|
|
: null;
|
|
await mg.save(
|
|
mg.create(BookingClearanceEvent, {
|
|
bookingId: input.bookingId,
|
|
action: input.action,
|
|
label: input.label,
|
|
actorType: input.actorType ?? 'STAFF',
|
|
actorId: input.actorId ?? null,
|
|
actorName,
|
|
metadata: input.metadata ?? null,
|
|
}),
|
|
);
|
|
this.logger.log(
|
|
`clearance-history ${input.action} on booking ${input.bookingId}${
|
|
actorName ? ` by ${actorName}` : ''
|
|
}`,
|
|
);
|
|
}
|
|
|
|
/** History for one booking, newest first. */
|
|
async list(bookingId: string): Promise<Freight.ClearanceHistoryEvent[]> {
|
|
const rows = await this.dataSource
|
|
.getRepository(BookingClearanceEvent)
|
|
.find({ where: { bookingId }, order: { createdAt: 'DESC' } });
|
|
|
|
// Rows whose actor name failed to resolve at write time get one more try.
|
|
const missing = rows
|
|
.filter((r) => !r.actorName && r.actorId)
|
|
.map((r) => r.actorId as string);
|
|
const names = missing.length
|
|
? await resolveIamUserNames(this.dataSource, missing).catch(
|
|
() => new Map<string, string>(),
|
|
)
|
|
: new Map<string, string>();
|
|
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
action: r.action,
|
|
label: r.label,
|
|
actorType: r.actorType,
|
|
actorName:
|
|
r.actorName ?? (r.actorId ? (names.get(r.actorId) ?? null) : null),
|
|
metadata: r.metadata ?? null,
|
|
at: r.createdAt.toISOString(),
|
|
}));
|
|
}
|
|
}
|