import type { BookingDocumentReview } from './entities/booking-document-review.entity'; import type { BookingReviewNote } from './entities/booking-review-note.entity'; import type { FileRecord } from '../files/entities/file.entity'; /** One entry of a clearance document's per-card audit trail, oldest first. */ export interface ClearanceDocEvent { type: 'UPLOADED' | 'RESUBMITTED' | 'QUERIED' | 'APPROVED'; at: string; byName: string | null; note: string | null; } /** * Query review notes are written as `Document "" queried: ` * (see BookingTransitionService.reviewDocument) — the only place a past query * decision survives after the customer re-uploads and the review row resets. */ const QUERY_NOTE_RE = /^Document "(.+?)" queried: ([\s\S]*)$/; /** * Per-document audit trail assembled from data the flow already persists: * every stored file version (first = customer upload, later ones = the * customer's amendment responses), every query note (who opened it, when, * why), and the review row's current approval. Approvals that were later * reset by a re-upload are the one thing not kept anywhere — the trail shows * the decision that currently stands. */ export function buildClearanceDocHistory(input: { fileKey: string; /** All versions of all files on the booking, createdAt ASC, deleted included. */ allVersions: FileRecord[]; /** CHANGES_REQUESTED review notes for the booking. */ queryNotes: BookingReviewNote[]; review: BookingDocumentReview | null; /** staff id → display name. */ names: Map; }): ClearanceDocEvent[] { const { fileKey, allVersions, queryNotes, review, names } = input; const events: ClearanceDocEvent[] = []; const versions = allVersions.filter((v) => v.code === fileKey); versions.forEach((v, i) => { events.push({ type: i === 0 ? 'UPLOADED' : 'RESUBMITTED', at: v.createdAt.toISOString(), byName: v.uploadedByName ?? null, note: null, }); }); for (const n of queryNotes) { const m = QUERY_NOTE_RE.exec(n.note); if (!m || m[1] !== fileKey) continue; events.push({ type: 'QUERIED', at: n.createdAt.toISOString(), byName: n.authorId ? (names.get(n.authorId) ?? null) : null, note: m[2] || null, }); } if (review?.status === 'APPROVED' && review.reviewedAt) { events.push({ type: 'APPROVED', at: review.reviewedAt.toISOString(), byName: review.reviewedByStaffId ? (names.get(review.reviewedByStaffId) ?? null) : null, note: null, }); } return events.sort((a, b) => a.at.localeCompare(b.at)); }