finalize gl flow for import

This commit is contained in:
Marshal
2026-07-02 18:15:16 +00:00
parent dd47068a4b
commit 8d056c5014
17 changed files with 1084 additions and 11 deletions

View File

@@ -645,6 +645,129 @@ export class GlOperationsService {
return summary;
}
/**
* GL ET advises (or skips) the post-arrival additional duty/tax round (import).
* Customer then attaches a slip; SECOND_DUTY_PAID completes on that upload.
*/
async adviseSecondDuty(
bookingId: string,
input: {
dutyRequired: boolean;
amount?: number;
currency?: string;
declarationSerial?: string;
},
attachment?: Express.Multer.File,
userId?: string,
): Promise<{ advised: boolean; skipped: boolean }> {
const booking = await this.getBooking(bookingId);
if (!booking.customsClearingEnabled) {
throw new BadRequestException('Additional duty applies to customs bookings only.');
}
const tradeDirection = booking.tradeDirection ?? 'IMPORT';
if (tradeDirection !== 'IMPORT') {
throw new BadRequestException('Additional duty applies to import shipments only.');
}
await this.milestoneService.ensureForBooking(bookingId, 'SECOND_DUTY_ADVISED', tradeDirection);
await this.milestoneService.ensureForBooking(bookingId, 'SECOND_DUTY_PAID', tradeDirection);
if (!input.dutyRequired) {
await this.milestoneService.skipForBooking(bookingId, 'SECOND_DUTY_ADVISED');
await this.milestoneService.skipForBooking(bookingId, 'SECOND_DUTY_PAID');
return { advised: false, skipped: true };
}
if (!input.amount || input.amount <= 0) {
throw new BadRequestException('Duty amount must be greater than zero.');
}
const files = await this.filesService.findByResource(bookingId, 'bookings');
const hasNotice = files.some((f) => f.code === 'duty_tax_notice_2');
if (!attachment && !hasNotice) {
throw new BadRequestException('Attach the additional duty/tax notice.');
}
if (attachment) {
await this.filesService.upsertByCode({
resourceId: bookingId,
resource: 'bookings',
code: 'duty_tax_notice_2',
file: attachment,
});
}
await this.milestoneService.completeWithMetadataForBooking(
bookingId,
'SECOND_DUTY_ADVISED',
{
dutyAmount: input.amount,
dutyCurrency: input.currency ?? 'ETB',
declarationSerial: input.declarationSerial,
},
userId,
);
return { advised: true, skipped: false };
}
/** Customer attaches the payment slip for the additional duty round. */
async uploadSecondDutySlip(
bookingId: string,
file: Express.Multer.File,
): Promise<{ milestoneCompleted: boolean }> {
const booking = await this.getBooking(bookingId);
if (!file) throw new BadRequestException('No payment slip uploaded');
const milestones = await this.milestoneService.listForBooking(bookingId);
const advised = milestones.find((m) => m.milestoneCode === 'SECOND_DUTY_ADVISED');
if (advised?.status !== 'COMPLETED') {
throw new BadRequestException('No additional duty has been advised for this shipment.');
}
await this.filesService.upsertByCode({
resourceId: bookingId,
resource: 'bookings',
code: 'duty_tax_receipt_2',
file,
});
await this.milestoneService.ensureForBooking(
bookingId,
'SECOND_DUTY_PAID',
booking.tradeDirection ?? 'IMPORT',
);
await this.milestoneService.completeForBooking(bookingId, 'SECOND_DUTY_PAID');
return { milestoneCompleted: true };
}
/** Second duty round state for clearance views. */
secondDutyState(
milestones: Array<{
milestoneCode: string;
status: string;
metadata?: { dutyAmount?: number; dutyCurrency?: string; declarationSerial?: string } | null;
}>,
files: Array<{ code?: string | null; id: string; name: string; url: string }>,
): Freight.ClearanceSecondDuty | null {
const advised = milestones.find((m) => m.milestoneCode === 'SECOND_DUTY_ADVISED');
const paid = milestones.find((m) => m.milestoneCode === 'SECOND_DUTY_PAID');
if (!advised && !paid) return null;
const toRef = (code: string) => {
const f = files.find((x) => x.code === code);
return f ? { id: f.id, name: f.name, url: f.url } : null;
};
return {
advised: advised?.status === 'COMPLETED',
skipped: advised?.status === 'SKIPPED',
amount: advised?.metadata?.dutyAmount ?? null,
currency: advised?.metadata?.dutyCurrency ?? null,
declarationSerial: advised?.metadata?.declarationSerial ?? null,
noticeFile: toRef('duty_tax_notice_2'),
slipFile: toRef('duty_tax_receipt_2'),
paid: paid?.status === 'COMPLETED',
};
}
/** Final-invoice state joined with its document + slip files, for clearance views. */
async finalInvoiceSummary(
bookingId: string,