mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 07:51:02 +00:00
fix(portal): show declaration, T1 and Djibouti clearance documents to the customer
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Clearance charges are no longer one-of-each in a fixed order: GL Ethiopia
|
||||
* may raise several MISCELLANEOUS charges, and either level may be created
|
||||
* first. Port charges stay unique per booking (one port bill per shipment),
|
||||
* enforced by a partial index instead of the old blanket (booking_id, type)
|
||||
* uniqueness that also capped miscellaneous at one.
|
||||
*/
|
||||
export class MultipleMiscClearanceCharges3610000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'MultipleMiscClearanceCharges3610000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DROP INDEX IF EXISTS "freight"."uq_booking_clearance_charge_booking_type"
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "uq_booking_clearance_charge_port"
|
||||
ON "freight"."booking_clearance_charge" ("booking_id")
|
||||
WHERE "type" = 'PORT_CHARGES' AND "deleted_at" IS NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS "idx_booking_clearance_charge_booking"
|
||||
ON "freight"."booking_clearance_charge" ("booking_id")
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// No-op on the uniqueness: restoring the blanket (booking_id, type) index
|
||||
// would fail on any booking that has since raised a second miscellaneous
|
||||
// charge, which is exactly what this migration set out to allow.
|
||||
await queryRunner.query(`
|
||||
DROP INDEX IF EXISTS "freight"."uq_booking_clearance_charge_port"
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user