fix(portal): show declaration, T1 and Djibouti clearance documents to the customer

This commit is contained in:
Marshal
2026-08-20 08:43:08 +00:00
parent d7752f4386
commit 8d7551bb8e
4 changed files with 112 additions and 64 deletions

View File

@@ -303,22 +303,8 @@ export class BookingClearanceChargeService {
const booking = await this.bookingsService.findById(bookingId);
this.assertClearanceFinalized(booking);
const port = await this.repo().findOne({
where: { bookingId, type: 'PORT_CHARGES' },
});
if (port?.status !== 'PAID') {
throw new ConflictException(
'Miscellaneous charges open after the port charge is paid.',
);
}
const existing = await this.repo().findOne({
where: { bookingId, type: 'MISCELLANEOUS' },
});
if (existing) {
throw new ConflictException(
'This booking already has a miscellaneous charge — revise it instead.',
);
}
// No ordering and no cap: a miscellaneous charge may be raised before,
// after or alongside the port charge, and a booking may carry several.
if (!(input.amount > 0)) {
throw new BadRequestException('Amount must be greater than zero.');
}
@@ -326,21 +312,15 @@ export class BookingClearanceChargeService {
throw new BadRequestException('Currency is required.');
}
const record = await this.filesService.upsertByCode(
{
resourceId: bookingId,
resource: 'bookings',
code: CHARGE_FILE_CODE.MISCELLANEOUS,
file,
},
{ userId: staffId },
);
await this.repo().save(
// Save the row first so its id can key the document. A booking may carry
// several miscellaneous charges, and `upsertByCode` retires whatever sits
// under the same code — a shared code would silently delete the previous
// charge's document.
const charge = await this.repo().save(
this.repo().create({
bookingId,
type: 'MISCELLANEOUS',
status: 'BILLED',
fileRecordId: record.id,
amount: input.amount.toFixed(2),
currency: input.currency.trim().toUpperCase(),
uploadedByStaffId: staffId,
@@ -349,6 +329,16 @@ export class BookingClearanceChargeService {
billedAt: new Date(),
}),
);
const record = await this.filesService.upsertByCode(
{
resourceId: bookingId,
resource: 'bookings',
code: `${CHARGE_FILE_CODE.MISCELLANEOUS}_${charge.id}`,
file,
},
{ userId: staffId },
);
await this.repo().update(charge.id, { fileRecordId: record.id });
await this.clearanceEvents.record({
bookingId,
action: 'CHARGE_MISC_CREATED',

View File

@@ -14,15 +14,15 @@ export const CLEARANCE_CHARGE_STATUSES = [
export type ClearanceChargeStatus = (typeof CLEARANCE_CHARGE_STATUSES)[number];
/**
* Post-finalization clearance charge billed to the customer — at most one
* PORT_CHARGES and one MISCELLANEOUS row per booking. GL Djibouti uploads the
* port-charges document (DOC_UPLOADED); GL Ethiopia sets amount + currency
* (BILLED) and issues the invoice (SENT); the billing `clearance_charge.invoice.paid`
* event marks it PAID. MISCELLANEOUS is created whole by GL Ethiopia and only
* after the port charge is paid.
* Clearance charge billed to the customer. One PORT_CHARGES row per booking
* (enforced by a partial unique index) and any number of MISCELLANEOUS rows.
* GL Djibouti uploads the port-charges document (DOC_UPLOADED); GL Ethiopia
* sets amount + currency (BILLED) and issues the invoice (SENT); the billing
* `clearance_charge.invoice.paid` event marks it PAID. The two levels are
* independent — either may be raised first.
*/
@Entity({ schema: 'freight', name: 'booking_clearance_charge' })
@Index(['bookingId', 'type'], { unique: true })
@Index(['bookingId'])
export class BookingClearanceCharge extends BaseEntity {
@Column({ name: 'booking_id', type: 'uuid' })
bookingId!: string;