fix issue

This commit is contained in:
Marshal
2026-08-20 04:27:16 +00:00
committed by Hagernesh
parent 6adfd10bc9
commit 87ae075031
7 changed files with 244 additions and 14 deletions

View File

@@ -28,6 +28,10 @@ import { ContainerValidationService } from './container-validation.service';
import { BookingsRepository } from './bookings.repository';
import { assertBookingStatus } from './booking-status.util';
import { clearanceCodesForBooking } from './clearance.util';
import {
buildClearanceDocHistory,
type ClearanceDocEvent,
} from './clearance-doc-history.util';
import { computeNextStep, type BookingNextStep } from './booking-next-step.util';
import { SubmitBookingResponseDto } from './dto/submit-booking-response.dto';
import { PriceLineItemDto } from './dto/generate-price-response.dto';
@@ -630,6 +634,7 @@ export class BookingTransitionService {
uploadedAt: string | null;
reviewedAt: string | null;
reviewedByName: string | null;
history: ClearanceDocEvent[];
}>;
allApproved: boolean;
phase?: string | null;
@@ -655,9 +660,18 @@ export class BookingTransitionService {
const reviewByKey = new Map(
reviews.map((r) => [`${r.settingCode}:${r.fileKey}`, r]),
);
const reviewerNames = await this.bookingsRepository.resolveStaffNames(
reviews.map((r) => r.reviewedByStaffId),
const allVersions = await this.filesService.findAllVersionsByResource(
bookingId,
"bookings",
);
const queryNotes = await this.bookingsRepository.findReviewNotes(
bookingId,
"CHANGES_REQUESTED",
);
const reviewerNames = await this.bookingsRepository.resolveStaffNames([
...reviews.map((r) => r.reviewedByStaffId),
...queryNotes.map((n) => n.authorId),
]);
const documents: Awaited<
ReturnType<BookingTransitionService["getClearanceView"]>
@@ -691,6 +705,13 @@ export class BookingTransitionService {
reviewedByName: review?.reviewedByStaffId
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
: null,
history: buildClearanceDocHistory({
fileKey: field.fileKey,
allVersions,
queryNotes,
review,
names: reviewerNames,
}),
});
}
};
@@ -716,6 +737,13 @@ export class BookingTransitionService {
reviewedByName: review?.reviewedByStaffId
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
: null,
history: buildClearanceDocHistory({
fileKey: f.code,
allVersions,
queryNotes,
review,
names: reviewerNames,
}),
});
}

View File

@@ -0,0 +1,74 @@
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 "<fileKey>" queried: <note>`
* (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<string, string>;
}): 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));
}

View File

@@ -35,6 +35,11 @@ import { ContractsRepository } from './contracts.repository';
import { AdviseContractDutyDto } from './dto/phased-clearance.dto';
import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue, DJ_BOOKING_QUEUE_STATUSES, persistDeclarationUploads, persistDeliveryOrderUploads, persistDraftDeclarationUploads, persistReleaseOrderUploads, persistTransitPermitUploads, PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES } from './phased-clearance.util';
import {
buildClearanceDocHistory,
type ClearanceDocEvent,
} from '../bookings/clearance-doc-history.util';
const RO_VESSEL_MIN_DAYS_CODE = 'ro_vessel_min_days';
export interface BookingClearanceView {
@@ -55,6 +60,7 @@ export interface BookingClearanceView {
uploadedAt: string | null;
reviewedAt: string | null;
reviewedByName: string | null;
history: ClearanceDocEvent[];
}>;
allApproved: boolean;
phase?: string | null;
@@ -188,9 +194,18 @@ export class BookingClearanceService {
const fileByCode = new Map(files.map((f) => [f.code, f]));
const reviews = await this.bookingsRepository.findDocumentReviews(bookingId);
const reviewByKey = new Map(reviews.map((r) => [`${r.settingCode}:${r.fileKey}`, r]));
const reviewerNames = await this.bookingsRepository.resolveStaffNames(
reviews.map((r) => r.reviewedByStaffId),
const allVersions = await this.filesService.findAllVersionsByResource(
bookingId,
'bookings',
);
const queryNotes = await this.bookingsRepository.findReviewNotes(
bookingId,
'CHANGES_REQUESTED',
);
const reviewerNames = await this.bookingsRepository.resolveStaffNames([
...reviews.map((r) => r.reviewedByStaffId),
...queryNotes.map((n) => n.authorId),
]);
const documents: BookingClearanceView['documents'] = [];
@@ -219,6 +234,13 @@ export class BookingClearanceService {
reviewedByName: review?.reviewedByStaffId
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
: null,
history: buildClearanceDocHistory({
fileKey: field.fileKey,
allVersions,
queryNotes,
review,
names: reviewerNames,
}),
});
}
};
@@ -243,6 +265,13 @@ export class BookingClearanceService {
reviewedByName: review?.reviewedByStaffId
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
: null,
history: buildClearanceDocHistory({
fileKey: f.code,
allVersions,
queryNotes,
review,
names: reviewerNames,
}),
});
}

View File

@@ -84,6 +84,21 @@ export class FilesRepository extends BaseRepository<FileRecord> {
});
}
/**
* Every version of every document on a resource, oldest first — superseded
* versions included. One query for a whole document grid's upload history.
*/
findAllVersionsByResource(
resourceId: string,
resource: string,
): Promise<FileRecord[]> {
return this.repository.find({
where: { resourceId, resource },
withDeleted: true,
order: { createdAt: "ASC" },
});
}
/**
* Documents belonging to any of the given resources that a reviewer has asked
* the customer to correct. Used by the approval gate, so it takes a list of

View File

@@ -327,6 +327,14 @@ export class FilesService {
return this.filesRepository.findByResource(resourceId, resource);
}
/** All versions of every document on a resource (superseded included), oldest first. */
findAllVersionsByResource(
resourceId: string,
resource: string,
): Promise<FileRecord[]> {
return this.filesRepository.findAllVersionsByResource(resourceId, resource);
}
/**
* Files for many resources of one kind, grouped by resource id. Resources with
* no files are absent from the map (callers should default to `[]`).