feat: implement document clearance workflow for bookings

- Added ClearanceCard component to display and manage clearance documents in ReadonlyBookingView.
- Introduced new API endpoints for clearance operations: getClearance, submitClearanceDocuments, and proceedToOperation.
- Created BookingDocumentReview entity and migration for document review status tracking.
- Developed GlClearancePage for Global Logistics to review and manage document submissions.
- Implemented utility functions for determining clearance setting codes based on trade direction and freight type.
- Added tests for booking transition clearance logic and clearance utility functions.
This commit is contained in:
Marshal
2026-06-23 21:33:34 +00:00
parent 29f6928059
commit 6485cbdd71
24 changed files with 1924 additions and 6 deletions

View File

@@ -0,0 +1,51 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Booking } from './booking.entity';
export const DOCUMENT_REVIEW_STATUSES = ['PENDING', 'APPROVED', 'QUERIED'] as const;
export type DocumentReviewStatus = (typeof DOCUMENT_REVIEW_STATUSES)[number];
/**
* Per-document GL review for the post-counter-sign clearance gate. One row per
* required clearance document (keyed by fileKey within a setting). GL marks each
* APPROVED or QUERIED (with a note); the booking can only proceed once every
* required customer document is APPROVED. A QUERIED row returns to PENDING when
* the customer re-uploads that file.
*/
@Entity({ schema: 'freight', name: 'booking_document_review' })
@Index(['bookingId'])
@Index(['status'])
@Index(['bookingId', 'settingCode', 'fileKey'], { unique: true })
export class BookingDocumentReview extends BaseEntity {
@Column({ name: 'booking_id', type: 'uuid' })
bookingId!: string;
@ManyToOne(() => Booking, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'booking_id' })
booking?: Booking;
/** The clearance setting this document belongs to (e.g. clearance_import_container_with_customs). */
@Column({ name: 'setting_code', type: 'varchar', length: 128 })
settingCode!: string;
/** The required document's stable key within the setting (e.g. commercial_invoice). */
@Column({ name: 'file_key', type: 'varchar', length: 128 })
fileKey!: string;
/** The uploaded FileRecord backing this review row (null until uploaded). */
@Column({ name: 'file_record_id', type: 'uuid', nullable: true })
fileRecordId?: string | null;
@Column({ name: 'status', type: 'varchar', length: 20, default: 'PENDING' })
status!: DocumentReviewStatus;
/** GL note explaining a QUERIED status. */
@Column({ name: 'note', type: 'text', nullable: true })
note?: string | null;
@Column({ name: 'reviewed_by_staff_id', type: 'uuid', nullable: true })
reviewedByStaffId?: string | null;
@Column({ name: 'reviewed_at', type: 'timestamptz', nullable: true })
reviewedAt?: Date | null;
}

View File

@@ -43,6 +43,11 @@ export const BOOKING_STATUSES = [
'CONSOLIDATED',
'CONTRACT_ACTIVE',
'CONTRACT_CLOSED',
// Post counter-sign document-clearance gate (GL workflow).
'AWAITING_DOCUMENTS',
'DOCUMENTS_UNDER_REVIEW',
'CLEARANCE_READY',
'OPERATION_REQUESTED',
] as const;
export type BookingStatus = (typeof BOOKING_STATUSES)[number];