mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 12:30:58 +00:00
finilize gl
This commit is contained in:
@@ -699,6 +699,7 @@ export class BookingTransitionService {
|
||||
bookingId,
|
||||
booking.tradeDirection ?? 'IMPORT',
|
||||
);
|
||||
await this.workflowService.onDocumentReviewReopenedForBooking(bookingId);
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
clearanceCurrentPhase: ContractDocPhase.GlEtReview,
|
||||
} as never);
|
||||
@@ -763,6 +764,15 @@ export class BookingTransitionService {
|
||||
if (status === 'QUERIED' && !note?.trim()) {
|
||||
throw new BadRequestException('A note is required when querying a document');
|
||||
}
|
||||
if (
|
||||
status === 'QUERIED' &&
|
||||
this.isPhasedGeneralCustoms(booking) &&
|
||||
booking.preClearanceFinalizedAt
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Customer documents cannot be queried after pre-clearance is finalized.',
|
||||
);
|
||||
}
|
||||
|
||||
await this.bookingsRepository.setDocumentReviewStatus(
|
||||
bookingId,
|
||||
@@ -779,10 +789,10 @@ export class BookingTransitionService {
|
||||
'CHANGES_REQUESTED',
|
||||
staffId,
|
||||
);
|
||||
if (this.isPhasedGeneralCustoms(booking) && booking.preClearanceFinalizedAt) {
|
||||
if (this.isPhasedGeneralCustoms(booking)) {
|
||||
await this.workflowService.onDocumentReviewReopenedForBooking(bookingId);
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
preClearanceFinalizedAt: null,
|
||||
clearanceCurrentPhase: ContractDocPhase.GlEtPostClearance,
|
||||
clearanceCurrentPhase: ContractDocPhase.GlEtReview,
|
||||
} as never);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,16 +543,16 @@ export class BookingsController {
|
||||
|
||||
@Post(':id/clearance/transit-permit')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
async uploadBookingTransitPermit(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
const booking = await this.bookingClearanceService.uploadTransitPermit(
|
||||
id,
|
||||
file,
|
||||
files ?? [],
|
||||
resolveAuthUserId(user),
|
||||
);
|
||||
return this.transitionService.enrichBookingResponse(booking);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { clearanceCodesForBooking } from '../bookings/clearance.util';
|
||||
import { ClearanceWorkflowService } from './clearance-workflow.service';
|
||||
import { ClearanceMilestoneService } from './clearance-milestone.service';
|
||||
import { AdviseContractDutyDto } from './dto/phased-clearance.dto';
|
||||
import { assertDeclarationFiles, buildWorkflowFiles } from './phased-clearance.util';
|
||||
import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue, DJ_BOOKING_QUEUE_STATUSES, persistDeclarationUploads, persistTransitPermitUploads, PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES } from './phased-clearance.util';
|
||||
|
||||
const RO_VESSEL_MIN_DAYS_CODE = 'ro_vessel_min_days';
|
||||
|
||||
@@ -157,11 +157,9 @@ export class BookingClearanceService {
|
||||
booking.tradeDirection ?? 'IMPORT',
|
||||
);
|
||||
const dutyAdvice = this.buildDutyAdvice(files, milestones);
|
||||
const documentFileKeys = new Set(documents.map((d) => d.fileKey));
|
||||
const workflowFiles = buildWorkflowFiles(
|
||||
files,
|
||||
booking.tradeDirection ?? 'IMPORT',
|
||||
documentFileKeys,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -279,16 +277,8 @@ export class BookingClearanceService {
|
||||
if (files.length === 0) {
|
||||
throw new BadRequestException('No declaration documents uploaded');
|
||||
}
|
||||
assertDeclarationFiles(files, tradeDirection);
|
||||
|
||||
for (const file of files) {
|
||||
await this.filesService.upsertByCode({
|
||||
resourceId: bookingId,
|
||||
resource: 'bookings',
|
||||
code: file.fieldname,
|
||||
file,
|
||||
});
|
||||
}
|
||||
await persistDeclarationUploads(this.filesService, bookingId, 'bookings', files);
|
||||
|
||||
await this.workflowService.onDeclarationUploadedForBooking(bookingId, userId);
|
||||
await this.bookingsRepository.update(bookingId, {
|
||||
@@ -380,7 +370,7 @@ export class BookingClearanceService {
|
||||
|
||||
async uploadTransitPermit(
|
||||
bookingId: string,
|
||||
file: Express.Multer.File,
|
||||
files: Express.Multer.File[],
|
||||
userId?: string,
|
||||
): Promise<Booking> {
|
||||
const booking = await this.loadBooking(bookingId);
|
||||
@@ -392,14 +382,11 @@ export class BookingClearanceService {
|
||||
'IMPORT',
|
||||
'TRANSIT_PERMIT_UPLOADED',
|
||||
);
|
||||
if (!file) throw new BadRequestException('No transit permit uploaded');
|
||||
if (files.length === 0) {
|
||||
throw new BadRequestException('No transit permit documents uploaded');
|
||||
}
|
||||
|
||||
await this.filesService.upsertByCode({
|
||||
resourceId: bookingId,
|
||||
resource: 'bookings',
|
||||
code: 'transit_permitted',
|
||||
file,
|
||||
});
|
||||
await persistTransitPermitUploads(this.filesService, bookingId, 'bookings', files);
|
||||
|
||||
await this.workflowService.completeMilestoneForBooking(
|
||||
bookingId,
|
||||
@@ -598,32 +585,31 @@ export class BookingClearanceService {
|
||||
|
||||
async etQueue(): Promise<Booking[]> {
|
||||
const candidates = await this.bookingsRepository.findByStatuses([
|
||||
'AWAITING_DOCUMENTS',
|
||||
'DOCUMENTS_UNDER_REVIEW',
|
||||
'CLEARANCE_READY',
|
||||
...PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES,
|
||||
]);
|
||||
const filtered: Booking[] = [];
|
||||
for (const b of candidates) {
|
||||
if (!this.isPhasedGeneralCustomsBooking(b)) continue;
|
||||
const milestones = await this.workflowService.listMilestonesForBooking(b.id);
|
||||
const next = this.workflowService.computeNextActionForBooking(b, milestones);
|
||||
if (next?.actor === 'GL_ET') filtered.push(b);
|
||||
if (belongsOnEtClearanceQueue(milestones)) filtered.push(b);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async djQueue(): Promise<Booking[]> {
|
||||
const candidates = await this.bookingsRepository.findByStatuses([
|
||||
'AWAITING_DOCUMENTS',
|
||||
'DOCUMENTS_UNDER_REVIEW',
|
||||
'CLEARANCE_READY',
|
||||
...DJ_BOOKING_QUEUE_STATUSES,
|
||||
]);
|
||||
const filtered: Booking[] = [];
|
||||
for (const b of candidates) {
|
||||
if (!this.isPhasedGeneralCustomsBooking(b)) continue;
|
||||
const milestones = await this.workflowService.listMilestonesForBooking(b.id);
|
||||
const pending = this.workflowService.djPendingMilestoneCodes(milestones);
|
||||
if (b.roHoldReason || pending || this.workflowService.computeNextActionForBooking(b, milestones)?.actor === 'GL_DJ') {
|
||||
if (
|
||||
belongsOnDjClearanceQueue(b.tradeDirection, null, milestones, {
|
||||
roHoldReason: b.roHoldReason,
|
||||
preClearanceFinalizedAt: b.preClearanceFinalizedAt,
|
||||
})
|
||||
) {
|
||||
filtered.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +197,26 @@ export class ClearanceMilestoneService {
|
||||
}
|
||||
|
||||
/** Skip optional milestones (e.g. duty when not required). */
|
||||
/** Reopen a completed contract milestone so review can continue after a query. */
|
||||
async reopenForContract(contractId: string, code: string): Promise<void> {
|
||||
const milestone = await this.repo.findOne({ where: { contractId, milestoneCode: code } });
|
||||
if (!milestone || milestone.status !== 'COMPLETED') return;
|
||||
milestone.status = 'PENDING';
|
||||
milestone.triggeredAt = null;
|
||||
milestone.triggeredByUserId = null;
|
||||
await this.repo.save(milestone);
|
||||
}
|
||||
|
||||
/** Reopen a completed booking milestone so review can continue after a query. */
|
||||
async reopenForBooking(bookingId: string, code: string): Promise<void> {
|
||||
const milestone = await this.repo.findOne({ where: { bookingId, milestoneCode: code } });
|
||||
if (!milestone || milestone.status !== 'COMPLETED') return;
|
||||
milestone.status = 'PENDING';
|
||||
milestone.triggeredAt = null;
|
||||
milestone.triggeredByUserId = null;
|
||||
await this.repo.save(milestone);
|
||||
}
|
||||
|
||||
async skipForContract(contractId: string, code: string): Promise<ClearanceMilestone> {
|
||||
const milestone = await this.repo.findOne({ where: { contractId, milestoneCode: code } });
|
||||
if (!milestone) {
|
||||
|
||||
@@ -204,6 +204,15 @@ export class ClearanceWorkflowService {
|
||||
await this.completeMilestoneForBooking(bookingId, 'DOCUMENTS_APPROVED');
|
||||
}
|
||||
|
||||
/** Customer doc queried or re-uploaded — document approval milestone must reopen. */
|
||||
async onDocumentReviewReopened(contractId: string): Promise<void> {
|
||||
await this.milestoneService.reopenForContract(contractId, 'DOCUMENTS_APPROVED');
|
||||
}
|
||||
|
||||
async onDocumentReviewReopenedForBooking(bookingId: string): Promise<void> {
|
||||
await this.milestoneService.reopenForBooking(bookingId, 'DOCUMENTS_APPROVED');
|
||||
}
|
||||
|
||||
async onDeclarationUploaded(contractId: string, userId?: string): Promise<void> {
|
||||
await this.completeMilestone(contractId, 'UNDER_CUSTOMS_CLEARANCE');
|
||||
await this.completeMilestone(contractId, 'DECLARED', userId);
|
||||
@@ -441,7 +450,7 @@ export class ClearanceWorkflowService {
|
||||
if (!isDone('DECLARED')) {
|
||||
return {
|
||||
actor: 'GL_ET',
|
||||
action: 'Upload customs declaration (EX3/EX8)',
|
||||
action: 'Upload customs declaration documents',
|
||||
milestoneCode: 'DECLARED',
|
||||
};
|
||||
}
|
||||
@@ -452,6 +461,30 @@ export class ClearanceWorkflowService {
|
||||
milestoneCode: EXPORT_BOUNDARY,
|
||||
};
|
||||
}
|
||||
if (terminalScope === 'booking') {
|
||||
if (!isDone('FREIGHT_PAYMENT_SETTLED')) {
|
||||
return {
|
||||
actor: 'CUSTOMER',
|
||||
action: 'Pay freight charges',
|
||||
milestoneCode: 'FREIGHT_PAYMENT_SETTLED',
|
||||
};
|
||||
}
|
||||
if (!isDone('WAGON_ALLOCATED')) {
|
||||
return {
|
||||
actor: 'OPERATIONS',
|
||||
action: 'Allocate wagon',
|
||||
milestoneCode: 'WAGON_ALLOCATED',
|
||||
};
|
||||
}
|
||||
if (!isDone('EXPORT_TRANSPORT_ISSUED')) {
|
||||
return {
|
||||
actor: 'GL_ET',
|
||||
action: 'Upload transit permit',
|
||||
milestoneCode: 'EXPORT_TRANSPORT_ISSUED',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
actor: terminalScope === 'contract' ? 'GL_ET' : 'CUSTOMER',
|
||||
action: terminalAction,
|
||||
@@ -462,7 +495,7 @@ export class ClearanceWorkflowService {
|
||||
if (!isDone('DECLARED')) {
|
||||
return {
|
||||
actor: 'GL_ET',
|
||||
action: 'Upload customs declaration (IM4/IM5)',
|
||||
action: 'Upload customs declaration documents',
|
||||
milestoneCode: 'DECLARED',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FileUploadSettingsService } from '../file-upload-settings/file-upload-s
|
||||
import { FilesService } from '../files/files.service';
|
||||
import { ContractsRepository } from './contracts.repository';
|
||||
import { ContractsService, PaginatedContracts } from './contracts.service';
|
||||
import { BookingsService } from '../bookings/bookings.service';
|
||||
import { contractClearanceCodes } from './contract-clearance.util';
|
||||
import { ClearanceWorkflowService } from './clearance-workflow.service';
|
||||
import { ClearanceMilestoneService } from './clearance-milestone.service';
|
||||
@@ -14,7 +15,7 @@ import { Contract } from './entities/contract.entity';
|
||||
import { ContractDocReviewStatus } from './entities/contract-document-review.entity';
|
||||
import { FilterContractDto } from './dto/filter-contract.dto';
|
||||
import { AdviseContractDutyDto } from './dto/phased-clearance.dto';
|
||||
import { assertDeclarationFiles, buildWorkflowFiles } from './phased-clearance.util';
|
||||
import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue, DJ_CONTRACT_QUEUE_STATUSES, persistDeclarationUploads, persistTransitPermitUploads, PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES } from './phased-clearance.util';
|
||||
|
||||
const RO_VESSEL_MIN_DAYS_CODE = 'ro_vessel_min_days';
|
||||
|
||||
@@ -66,6 +67,9 @@ export interface ContractClearanceView {
|
||||
roAmendmentRequestedAt?: string | null;
|
||||
bookingReady?: boolean;
|
||||
preClearanceFinalized?: boolean;
|
||||
/** Export post-booking clearance finalized (transit permit uploaded + GL confirmed). */
|
||||
exportClearanceFinalized?: boolean;
|
||||
linkedBookingId?: string | null;
|
||||
dutyAdvice?: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
@@ -80,6 +84,7 @@ export class ContractClearanceService {
|
||||
constructor(
|
||||
private readonly contractsRepository: ContractsRepository,
|
||||
private readonly contractsService: ContractsService,
|
||||
private readonly bookingsService: BookingsService,
|
||||
private readonly filesService: FilesService,
|
||||
private readonly fileUploadSettingsService: FileUploadSettingsService,
|
||||
private readonly workflowService: ClearanceWorkflowService,
|
||||
@@ -198,14 +203,40 @@ export class ContractClearanceService {
|
||||
);
|
||||
contract = await this.reconcilePrematureBookingReady(contractId, contract, boundary);
|
||||
const phase = this.workflowService.resolvePhase(contract, cycle, milestones);
|
||||
const nextAction = this.workflowService.computeNextAction(contract, cycle, milestones);
|
||||
const dutyAdvice = this.buildDutyAdvice(files, milestones);
|
||||
const documentFileKeys = new Set(documents.map((d) => d.fileKey));
|
||||
const workflowFiles = buildWorkflowFiles(
|
||||
let workflowFiles = buildWorkflowFiles(
|
||||
files,
|
||||
contract.tradeDirection ?? 'IMPORT',
|
||||
documentFileKeys,
|
||||
);
|
||||
if (cycle?.bookingId) {
|
||||
const bookingFiles = await this.filesService.findByResource(
|
||||
cycle.bookingId,
|
||||
'bookings',
|
||||
);
|
||||
const bookingWorkflow = buildWorkflowFiles(
|
||||
bookingFiles,
|
||||
contract.tradeDirection ?? 'IMPORT',
|
||||
);
|
||||
const byCode = new Map(workflowFiles.map((f) => [f.code, f]));
|
||||
for (const row of bookingWorkflow) {
|
||||
if (row.file) byCode.set(row.code, row);
|
||||
}
|
||||
workflowFiles = [...byCode.values()];
|
||||
}
|
||||
|
||||
let nextAction = this.workflowService.computeNextAction(contract, cycle, milestones);
|
||||
if (cycle?.bookingId && contract.tradeDirection === 'EXPORT') {
|
||||
const bookingMilestones = await this.workflowService.listMilestonesForBooking(
|
||||
cycle.bookingId,
|
||||
);
|
||||
const booking = await this.bookingsService.findById(cycle.bookingId);
|
||||
if (booking) {
|
||||
nextAction = this.workflowService.computeNextActionForBooking(
|
||||
booking,
|
||||
bookingMilestones,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contractId,
|
||||
@@ -237,6 +268,8 @@ export class ContractClearanceService {
|
||||
: null,
|
||||
bookingReady: boundary,
|
||||
preClearanceFinalized: Boolean(cycle?.preClearanceFinalizedAt),
|
||||
exportClearanceFinalized: Boolean(cycle?.completedAt),
|
||||
linkedBookingId: cycle?.bookingId ?? null,
|
||||
dutyAdvice,
|
||||
workflowFiles,
|
||||
};
|
||||
@@ -413,6 +446,7 @@ export class ContractClearanceService {
|
||||
|
||||
if (contract.customsClearingEnabled && contract.contractKind === 'ONE_TIME') {
|
||||
await this.workflowService.onCustomerDocsUploaded(contractId, contract.tradeDirection);
|
||||
await this.workflowService.onDocumentReviewReopened(contractId);
|
||||
}
|
||||
|
||||
return this.contractsService.findById(contractId);
|
||||
@@ -505,6 +539,15 @@ export class ContractClearanceService {
|
||||
|
||||
const { inputCode, outputCode } = contractClearanceCodes(contract);
|
||||
const cycle = await this.contractsRepository.currentCycle(contractId);
|
||||
if (
|
||||
status === 'QUERIED' &&
|
||||
this.isPhasedCustoms(contract) &&
|
||||
cycle?.preClearanceFinalizedAt
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Customer documents cannot be queried after pre-clearance is finalized.',
|
||||
);
|
||||
}
|
||||
const reviews = await this.contractsRepository.findDocumentReviews(
|
||||
contractId,
|
||||
cycle?.id ?? null,
|
||||
@@ -539,10 +582,12 @@ export class ContractClearanceService {
|
||||
} as never);
|
||||
if (cycle) {
|
||||
await this.contractsRepository.setCycleStatus(cycle.id, 'AWAITING_DOCUMENTS');
|
||||
if (this.isPhasedCustoms(contract) && cycle.preClearanceFinalizedAt) {
|
||||
}
|
||||
if (this.isPhasedCustoms(contract)) {
|
||||
await this.workflowService.onDocumentReviewReopened(contractId);
|
||||
if (cycle) {
|
||||
await this.contractsRepository.updateCycle(cycle.id, {
|
||||
preClearanceFinalizedAt: null,
|
||||
currentPhase: ContractDocPhase.GlEtPostClearance,
|
||||
currentPhase: ContractDocPhase.GlEtReview,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -704,19 +749,14 @@ export class ContractClearanceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* GL ET clearance hub: every customs (Path B) contract that still needs
|
||||
* customs clearance — awaiting the customer's documents, under GL review, or
|
||||
* finalized and waiting for the customer to create the booking in the portal.
|
||||
* GL ET clearance hub: every customs (Path B) contract in phased clearance,
|
||||
* including after booking is created.
|
||||
*/
|
||||
async queue(filter: FilterContractDto): Promise<PaginatedContracts> {
|
||||
return this.contractsRepository.findAllPaginated({
|
||||
page: filter.page ?? 1,
|
||||
pageSize: filter.pageSize ?? 100,
|
||||
statuses: [
|
||||
'AWAITING_CLEARANCE_DOCUMENTS',
|
||||
'CLEARANCE_UNDER_REVIEW',
|
||||
'CLEARANCE_READY_FOR_BOOKING',
|
||||
],
|
||||
statuses: [...PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES],
|
||||
customsClearingEnabled: true,
|
||||
sortBy: filter.sortBy,
|
||||
sortOrder: filter.sortOrder,
|
||||
@@ -799,16 +839,13 @@ export class ContractClearanceService {
|
||||
if (files.length === 0) {
|
||||
throw new BadRequestException('No declaration documents uploaded');
|
||||
}
|
||||
assertDeclarationFiles(files, contract.tradeDirection);
|
||||
|
||||
for (const file of files) {
|
||||
await this.filesService.upsertByCode({
|
||||
resourceId: contractId,
|
||||
resource: 'contracts',
|
||||
code: file.fieldname,
|
||||
file,
|
||||
});
|
||||
}
|
||||
await persistDeclarationUploads(
|
||||
this.filesService,
|
||||
contractId,
|
||||
'contracts',
|
||||
files,
|
||||
);
|
||||
|
||||
await this.workflowService.onDeclarationUploaded(contractId, userId);
|
||||
|
||||
@@ -913,7 +950,7 @@ export class ContractClearanceService {
|
||||
|
||||
async uploadTransitPermit(
|
||||
contractId: string,
|
||||
file: Express.Multer.File,
|
||||
files: Express.Multer.File[],
|
||||
userId?: string,
|
||||
): Promise<Contract> {
|
||||
const contract = await this.contractsService.findById(contractId);
|
||||
@@ -927,14 +964,16 @@ export class ContractClearanceService {
|
||||
'TRANSIT_PERMIT_UPLOADED',
|
||||
);
|
||||
|
||||
if (!file) throw new BadRequestException('No transit permit uploaded');
|
||||
if (files.length === 0) {
|
||||
throw new BadRequestException('No transit permit documents uploaded');
|
||||
}
|
||||
|
||||
await this.filesService.upsertByCode({
|
||||
resourceId: contractId,
|
||||
resource: 'contracts',
|
||||
code: 'transit_permitted',
|
||||
file,
|
||||
});
|
||||
await persistTransitPermitUploads(
|
||||
this.filesService,
|
||||
contractId,
|
||||
'contracts',
|
||||
files,
|
||||
);
|
||||
|
||||
await this.workflowService.completeMilestone(contractId, 'TRANSIT_PERMIT_UPLOADED', userId);
|
||||
|
||||
@@ -1135,12 +1174,51 @@ export class ContractClearanceService {
|
||||
return this.contractsService.findById(contractId);
|
||||
}
|
||||
|
||||
/** GL ET queue: customs ONE_TIME contracts with a pending ET-owned milestone. */
|
||||
/** GL ET finalizes export clearance after post-booking transit permit is uploaded. */
|
||||
async finalizeExportClearance(contractId: string, userId?: string): Promise<Contract> {
|
||||
const contract = await this.contractsService.findById(contractId);
|
||||
this.assertPhasedCustoms(contract);
|
||||
if (contract.tradeDirection !== 'EXPORT') {
|
||||
throw new BadRequestException('Export clearance finalize applies only to export contracts.');
|
||||
}
|
||||
|
||||
const cycle = await this.contractsRepository.currentCycle(contractId);
|
||||
if (!cycle?.bookingId) {
|
||||
throw new BadRequestException(
|
||||
'A shipment booking must exist before export clearance can be finalized.',
|
||||
);
|
||||
}
|
||||
if (cycle.completedAt) {
|
||||
return this.contractsService.findById(contractId);
|
||||
}
|
||||
|
||||
const bookingMilestones = await this.workflowService.listMilestonesForBooking(
|
||||
cycle.bookingId,
|
||||
);
|
||||
const transportDone = bookingMilestones.some(
|
||||
(m) => m.milestoneCode === 'EXPORT_TRANSPORT_ISSUED' && m.status === 'COMPLETED',
|
||||
);
|
||||
if (!transportDone) {
|
||||
throw new BadRequestException(
|
||||
'Upload the transit permit before finalizing export clearance.',
|
||||
);
|
||||
}
|
||||
|
||||
await this.contractsRepository.updateCycle(cycle.id, {
|
||||
completedAt: new Date(),
|
||||
currentPhase: ContractDocPhase.GlEtPostClearance,
|
||||
});
|
||||
|
||||
void userId;
|
||||
return this.contractsService.findById(contractId);
|
||||
}
|
||||
|
||||
/** GL ET queue: customs ONE_TIME contracts in phased clearance (persistent after booking). */
|
||||
async etQueue(filter: FilterContractDto): Promise<PaginatedContracts> {
|
||||
const base = await this.contractsRepository.findAllPaginated({
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
statuses: ['AWAITING_CLEARANCE_DOCUMENTS', 'CLEARANCE_UNDER_REVIEW', 'CLEARANCE_READY_FOR_BOOKING'],
|
||||
statuses: [...PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES],
|
||||
customsClearingEnabled: true,
|
||||
contractKind: 'ONE_TIME',
|
||||
sortBy: filter.sortBy,
|
||||
@@ -1150,13 +1228,7 @@ export class ContractClearanceService {
|
||||
const filtered: typeof base.items = [];
|
||||
for (const c of base.items) {
|
||||
const milestones = await this.workflowService.listMilestones(c.id);
|
||||
const pending = this.workflowService.etPendingMilestoneCodes(milestones);
|
||||
const next = this.workflowService.computeNextAction(
|
||||
c,
|
||||
await this.contractsRepository.currentCycle(c.id),
|
||||
milestones,
|
||||
);
|
||||
if (pending || next?.actor === 'GL_ET') filtered.push(c);
|
||||
if (belongsOnEtClearanceQueue(milestones)) filtered.push(c);
|
||||
}
|
||||
|
||||
const page = filter.page ?? 1;
|
||||
@@ -1178,12 +1250,12 @@ export class ContractClearanceService {
|
||||
};
|
||||
}
|
||||
|
||||
/** GL DJ queue: customs ONE_TIME contracts with a pending DJ-owned milestone or RO hold. */
|
||||
/** GL DJ queue: customs ONE_TIME contracts handed off to or handled by Djibouti GL. */
|
||||
async djQueue(filter: FilterContractDto): Promise<PaginatedContracts> {
|
||||
const base = await this.contractsRepository.findAllPaginated({
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
statuses: ['AWAITING_CLEARANCE_DOCUMENTS', 'CLEARANCE_UNDER_REVIEW', 'CLEARANCE_READY_FOR_BOOKING'],
|
||||
statuses: [...DJ_CONTRACT_QUEUE_STATUSES],
|
||||
customsClearingEnabled: true,
|
||||
contractKind: 'ONE_TIME',
|
||||
sortBy: filter.sortBy,
|
||||
@@ -1194,9 +1266,9 @@ export class ContractClearanceService {
|
||||
for (const c of base.items) {
|
||||
const cycle = await this.contractsRepository.currentCycle(c.id);
|
||||
const milestones = await this.workflowService.listMilestones(c.id);
|
||||
const pending = this.workflowService.djPendingMilestoneCodes(milestones);
|
||||
const next = this.workflowService.computeNextAction(c, cycle, milestones);
|
||||
if (cycle?.roHoldReason || pending || next?.actor === 'GL_DJ') filtered.push(c);
|
||||
if (belongsOnDjClearanceQueue(c.tradeDirection, cycle, milestones)) {
|
||||
filtered.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
const page = filter.page ?? 1;
|
||||
|
||||
@@ -532,7 +532,7 @@ export class ContractsController {
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'GL ET uploads customs declaration (IM4/IM5 or EX3/EX8)' })
|
||||
@ApiOperation({ summary: 'GL ET uploads customs declaration documents (multi-file)' })
|
||||
uploadDeclaration(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
@@ -591,15 +591,15 @@ export class ContractsController {
|
||||
|
||||
@Post(':id/clearance/transit-permit')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'GL ET uploads transit permit screenshot (import)' })
|
||||
@ApiOperation({ summary: 'GL ET uploads import transit permit documents (multi-file)' })
|
||||
uploadTransitPermit(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
) {
|
||||
return this.clearanceService.uploadTransitPermit(id, file, resolveAuthUserId(user));
|
||||
return this.clearanceService.uploadTransitPermit(id, files ?? [], resolveAuthUserId(user));
|
||||
}
|
||||
|
||||
@Post(':id/clearance/delivery-order')
|
||||
@@ -655,16 +655,28 @@ export class ContractsController {
|
||||
return this.clearanceService.confirmExportRelease(id, resolveAuthUserId(user));
|
||||
}
|
||||
|
||||
@Post(':id/clearance/finalize-export-clearance')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
||||
@ApiOperation({
|
||||
summary: 'GL ET finalizes export clearance after post-booking transit permit upload',
|
||||
})
|
||||
finalizeExportClearance(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
) {
|
||||
return this.clearanceService.finalizeExportClearance(id, resolveAuthUserId(user));
|
||||
}
|
||||
|
||||
@Get('clearance/et-queue')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
||||
@ApiOperation({ summary: 'GL Ethiopia phased clearance queue' })
|
||||
@ApiOperation({ summary: 'GL Ethiopia phased clearance list (persistent after booking)' })
|
||||
etClearanceQueue(@Query() filter: FilterContractDto) {
|
||||
return this.clearanceService.etQueue(filter);
|
||||
}
|
||||
|
||||
@Get('clearance/dj-queue')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
|
||||
@ApiOperation({ summary: 'GL Djibouti phased clearance queue' })
|
||||
@ApiOperation({ summary: 'GL Djibouti phased clearance list (persistent after booking)' })
|
||||
djClearanceQueue(@Query() filter: FilterContractDto) {
|
||||
return this.clearanceService.djQueue(filter);
|
||||
}
|
||||
|
||||
@@ -548,6 +548,7 @@ export class ContractsRepository extends BaseRepository<Contract> {
|
||||
| 'currentPhase'
|
||||
| 'status'
|
||||
| 'preClearanceFinalizedAt'
|
||||
| 'completedAt'
|
||||
>
|
||||
>,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue } from './phased-clearance.util';
|
||||
|
||||
describe('buildWorkflowFiles', () => {
|
||||
const resourceFiles = [
|
||||
{ code: 'im4', id: 'f-im4', name: 'im4.pdf', url: '/files/im4' },
|
||||
{ code: 'im5', id: 'f-im5', name: 'im5.pdf', url: '/files/im5' },
|
||||
{
|
||||
code: 'transit_permitted',
|
||||
id: 'f-transit',
|
||||
name: 'transit.png',
|
||||
url: '/files/transit',
|
||||
},
|
||||
{
|
||||
code: 'duty_tax_notice',
|
||||
id: 'f-duty',
|
||||
name: 'notice.pdf',
|
||||
url: '/files/duty',
|
||||
},
|
||||
{ code: 'commercial_invoice', id: 'f-inv', name: 'inv.pdf', url: '/files/inv' },
|
||||
];
|
||||
|
||||
it('includes declaration and transit files even when they also appear in GL output document settings', () => {
|
||||
const result = buildWorkflowFiles(resourceFiles, 'IMPORT');
|
||||
|
||||
expect(result.map((f) => f.code)).toEqual(
|
||||
expect.arrayContaining(['im4', 'im5', 'transit_permitted', 'duty_tax_notice']),
|
||||
);
|
||||
});
|
||||
|
||||
it('includes multi-file declaration uploads alongside catalog codes', () => {
|
||||
const result = buildWorkflowFiles(
|
||||
[
|
||||
...resourceFiles,
|
||||
{
|
||||
code: 'declaration_0',
|
||||
id: 'f-dec-0',
|
||||
name: 'decl-a.pdf',
|
||||
url: '/files/decl-a',
|
||||
},
|
||||
{
|
||||
code: 'declaration_1',
|
||||
id: 'f-dec-1',
|
||||
name: 'decl-b.pdf',
|
||||
url: '/files/decl-b',
|
||||
},
|
||||
],
|
||||
'IMPORT',
|
||||
);
|
||||
|
||||
expect(result.map((f) => f.code)).toEqual(
|
||||
expect.arrayContaining(['im4', 'im5', 'declaration_0', 'declaration_1']),
|
||||
);
|
||||
expect(result.find((f) => f.code === 'declaration_0')?.label).toBe(
|
||||
'Declaration document 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('includes multi-file import transit permit uploads', () => {
|
||||
const result = buildWorkflowFiles(
|
||||
[
|
||||
...resourceFiles,
|
||||
{
|
||||
code: 'transit_permit_0',
|
||||
id: 'f-tp-0',
|
||||
name: 'permit-a.pdf',
|
||||
url: '/files/tp-a',
|
||||
},
|
||||
{
|
||||
code: 'transit_permit_1',
|
||||
id: 'f-tp-1',
|
||||
name: 'permit-b.pdf',
|
||||
url: '/files/tp-b',
|
||||
},
|
||||
],
|
||||
'IMPORT',
|
||||
);
|
||||
|
||||
expect(result.map((f) => f.code)).toEqual(
|
||||
expect.arrayContaining(['transit_permitted', 'transit_permit_0', 'transit_permit_1']),
|
||||
);
|
||||
expect(result.find((f) => f.code === 'transit_permit_0')?.label).toBe('Transit permit 1');
|
||||
});
|
||||
|
||||
it('does not include non-catalog customer document codes', () => {
|
||||
const result = buildWorkflowFiles(resourceFiles, 'IMPORT');
|
||||
|
||||
expect(result.some((f) => f.code === 'commercial_invoice')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('belongsOnDjClearanceQueue', () => {
|
||||
it('keeps import contracts after pre-clearance is finalized (even post-booking)', () => {
|
||||
expect(
|
||||
belongsOnDjClearanceQueue(
|
||||
'IMPORT',
|
||||
{ preClearanceFinalizedAt: new Date('2026-01-01') },
|
||||
[],
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps contracts with completed Djibouti milestones', () => {
|
||||
expect(
|
||||
belongsOnDjClearanceQueue('IMPORT', null, [
|
||||
{ ownerRegion: 'DJ', status: 'COMPLETED' },
|
||||
]),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('excludes import contracts still on Ethiopia-side clearance only', () => {
|
||||
expect(belongsOnDjClearanceQueue('IMPORT', null, [])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('belongsOnEtClearanceQueue', () => {
|
||||
it('keeps contracts once phased clearance milestones exist', () => {
|
||||
expect(
|
||||
belongsOnEtClearanceQueue([{ ownerRegion: 'ET', status: 'COMPLETED' }]),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('excludes contracts with no clearance milestones', () => {
|
||||
expect(belongsOnEtClearanceQueue([])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,53 +1,213 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import {
|
||||
catalogEntriesForTradeDirection,
|
||||
declarationFileLabel,
|
||||
isDeclarationFileCode,
|
||||
isImportTransitPermitFileCode,
|
||||
transitPermitFileLabel,
|
||||
type ClearanceWorkflowFile,
|
||||
} from '@edr/types';
|
||||
|
||||
const IMPORT_DECLARATION_CODES = new Set(['im4', 'im5']);
|
||||
const EXPORT_DECLARATION_CODES = new Set(['ex3', 'ex8']);
|
||||
|
||||
/** Require at least one declaration file for the trade direction (IM4 or IM5, EX3 or EX8). */
|
||||
export function assertDeclarationFiles(
|
||||
files: Express.Multer.File[],
|
||||
tradeDirection: string,
|
||||
): void {
|
||||
/** Require at least one declaration file in the upload batch. */
|
||||
export function assertDeclarationFiles(files: Express.Multer.File[]): void {
|
||||
if (files.length === 0) {
|
||||
throw new BadRequestException('No declaration documents uploaded');
|
||||
}
|
||||
}
|
||||
|
||||
const allowed =
|
||||
tradeDirection === 'EXPORT' ? EXPORT_DECLARATION_CODES : IMPORT_DECLARATION_CODES;
|
||||
const labels = tradeDirection === 'EXPORT' ? 'EX3 or EX8' : 'IM4 or IM5';
|
||||
/** Assign stable `declaration_*` codes so multi-file uploads always pass validation. */
|
||||
export function normalizeDeclarationFieldNames(
|
||||
files: Express.Multer.File[],
|
||||
): Express.Multer.File[] {
|
||||
return files.map((file, index) => ({
|
||||
...file,
|
||||
fieldname: `declaration_${index}`,
|
||||
}));
|
||||
}
|
||||
|
||||
const uploaded = new Set(files.map((f) => f.fieldname?.toLowerCase()));
|
||||
const hasValid = [...allowed].some((code) => uploaded.has(code));
|
||||
if (!hasValid) {
|
||||
throw new BadRequestException(`Upload at least one declaration document (${labels}).`);
|
||||
type DeclarationFileStore = {
|
||||
findByResource(
|
||||
resourceId: string,
|
||||
resource: string,
|
||||
): Promise<Array<{ code?: string | null }>>;
|
||||
deleteByCode(resourceId: string, resource: string, code: string): Promise<void>;
|
||||
upload(input: {
|
||||
resourceId: string;
|
||||
resource: string;
|
||||
code: string;
|
||||
file: Express.Multer.File;
|
||||
}): Promise<unknown>;
|
||||
};
|
||||
|
||||
/** Replace all declaration files on a resource with a new multi-file upload batch. */
|
||||
export async function persistDeclarationUploads(
|
||||
store: DeclarationFileStore,
|
||||
resourceId: string,
|
||||
resource: string,
|
||||
files: Express.Multer.File[],
|
||||
): Promise<void> {
|
||||
const normalized = normalizeDeclarationFieldNames(files);
|
||||
assertDeclarationFiles(normalized);
|
||||
|
||||
const existing = await store.findByResource(resourceId, resource);
|
||||
await Promise.all(
|
||||
existing
|
||||
.filter((f) => f.code && isDeclarationFileCode(f.code))
|
||||
.map((f) => store.deleteByCode(resourceId, resource, f.code!)),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
normalized.map((file, index) =>
|
||||
store.upload({
|
||||
resourceId,
|
||||
resource,
|
||||
code: `declaration_${index}`,
|
||||
file,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Require at least one transit permit file in the upload batch. */
|
||||
export function assertTransitPermitFiles(files: Express.Multer.File[]): void {
|
||||
if (files.length === 0) {
|
||||
throw new BadRequestException('No transit permit documents uploaded');
|
||||
}
|
||||
}
|
||||
|
||||
/** Assign stable `transit_permit_*` codes for multi-file import transit uploads. */
|
||||
export function normalizeTransitPermitFieldNames(
|
||||
files: Express.Multer.File[],
|
||||
): Express.Multer.File[] {
|
||||
return files.map((file, index) => ({
|
||||
...file,
|
||||
fieldname: `transit_permit_${index}`,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Replace all import transit permit files on a resource with a new multi-file batch. */
|
||||
export async function persistTransitPermitUploads(
|
||||
store: DeclarationFileStore,
|
||||
resourceId: string,
|
||||
resource: string,
|
||||
files: Express.Multer.File[],
|
||||
): Promise<void> {
|
||||
const normalized = normalizeTransitPermitFieldNames(files);
|
||||
assertTransitPermitFiles(normalized);
|
||||
|
||||
const existing = await store.findByResource(resourceId, resource);
|
||||
await Promise.all(
|
||||
existing
|
||||
.filter((f) => f.code && isImportTransitPermitFileCode(f.code))
|
||||
.map((f) => store.deleteByCode(resourceId, resource, f.code!)),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
normalized.map((file, index) =>
|
||||
store.upload({
|
||||
resourceId,
|
||||
resource,
|
||||
code: `transit_permit_${index}`,
|
||||
file,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function parseDutyRequiredForm(value: string | boolean | undefined): boolean {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (value === undefined || value === '') return false;
|
||||
return value === 'true' || value === '1';
|
||||
}
|
||||
|
||||
type DjQueueMilestone = {
|
||||
ownerRegion?: string | null;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type DjQueueCycle = {
|
||||
preClearanceFinalizedAt?: Date | null;
|
||||
roHoldReason?: string | null;
|
||||
} | null | undefined;
|
||||
|
||||
/** Whether a customs clearance item belongs on the persistent GL Djibouti list. */
|
||||
export function belongsOnDjClearanceQueue(
|
||||
tradeDirection: string | null | undefined,
|
||||
cycle: DjQueueCycle,
|
||||
milestones: DjQueueMilestone[],
|
||||
extras?: {
|
||||
roHoldReason?: string | null;
|
||||
preClearanceFinalizedAt?: Date | null;
|
||||
},
|
||||
): boolean {
|
||||
const roHold = cycle?.roHoldReason ?? extras?.roHoldReason;
|
||||
if (roHold) return true;
|
||||
|
||||
const hasDjActivity = milestones.some(
|
||||
(m) => m.ownerRegion === 'DJ' && (m.status === 'COMPLETED' || m.status === 'PENDING'),
|
||||
);
|
||||
if (hasDjActivity) return true;
|
||||
|
||||
const preFinalized =
|
||||
cycle?.preClearanceFinalizedAt ?? extras?.preClearanceFinalizedAt ?? null;
|
||||
if (tradeDirection === 'IMPORT' && preFinalized) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Contract statuses for persistent phased customs clearance lists (ET + DJ). */
|
||||
export const PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES = [
|
||||
'AWAITING_CLEARANCE_DOCUMENTS',
|
||||
'CLEARANCE_UNDER_REVIEW',
|
||||
'CLEARANCE_READY_FOR_BOOKING',
|
||||
'ACTIVE_SHIPMENT_IN_PROGRESS',
|
||||
'FULLY_EXECUTED',
|
||||
'CONTRACT_ACTIVE',
|
||||
'CONTRACT_CLOSED',
|
||||
] as const;
|
||||
|
||||
/** Whether a customs clearance item belongs on the persistent GL Ethiopia list. */
|
||||
export function belongsOnEtClearanceQueue(milestones: DjQueueMilestone[]): boolean {
|
||||
return milestones.some((m) => m.status === 'PENDING' || m.status === 'COMPLETED');
|
||||
}
|
||||
|
||||
/** Contract statuses that may appear on the GL Djibouti clearance list (includes post-booking). */
|
||||
export const DJ_CONTRACT_QUEUE_STATUSES = PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES;
|
||||
|
||||
/** Booking statuses for persistent phased customs clearance lists (ET + DJ). */
|
||||
export const PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES = [
|
||||
'AWAITING_DOCUMENTS',
|
||||
'DOCUMENTS_UNDER_REVIEW',
|
||||
'CLEARANCE_READY',
|
||||
'FULLY_EXECUTED',
|
||||
'OPERATION_REQUEST_PENDING',
|
||||
'OPERATION_CHANGES_REQUESTED',
|
||||
'ROAD_DISPATCH_PENDING',
|
||||
'IN_TRANSIT',
|
||||
'PAID',
|
||||
'COMPLETED',
|
||||
'CONTRACT_ACTIVE',
|
||||
'CONTRACT_CLOSED',
|
||||
] as const;
|
||||
|
||||
/** Booking statuses that may appear on the GL Djibouti clearance list (includes post-clearance). */
|
||||
export const DJ_BOOKING_QUEUE_STATUSES = PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES;
|
||||
|
||||
/** Build labeled phased-customs file rows from resource files. */
|
||||
export function buildWorkflowFiles(
|
||||
files: Array<{ code?: string | null; id: string; name: string; url: string }>,
|
||||
tradeDirection: string,
|
||||
documentFileKeys: Set<string> = new Set(),
|
||||
): ClearanceWorkflowFile[] {
|
||||
const fileByCode = new Map(
|
||||
files.filter((f) => f.code).map((f) => [f.code as string, f]),
|
||||
);
|
||||
const out: ClearanceWorkflowFile[] = [];
|
||||
const included = new Set<string>();
|
||||
|
||||
for (const entry of catalogEntriesForTradeDirection(tradeDirection)) {
|
||||
if (documentFileKeys.has(entry.code)) continue;
|
||||
const file = fileByCode.get(entry.code) ?? null;
|
||||
if (!file) continue;
|
||||
included.add(entry.code);
|
||||
out.push({
|
||||
code: entry.code,
|
||||
label: entry.label,
|
||||
@@ -57,5 +217,39 @@ export function buildWorkflowFiles(
|
||||
});
|
||||
}
|
||||
|
||||
const extraDeclarations = files
|
||||
.filter((f) => f.code && isDeclarationFileCode(f.code) && !included.has(f.code))
|
||||
.sort((a, b) => (a.code ?? '').localeCompare(b.code ?? ''));
|
||||
|
||||
extraDeclarations.forEach((file, index) => {
|
||||
if (!file.code) return;
|
||||
included.add(file.code);
|
||||
out.push({
|
||||
code: file.code,
|
||||
label: declarationFileLabel(file.code, index),
|
||||
uploadedBy: 'gl_et',
|
||||
category: 'declaration',
|
||||
file: { id: file.id, name: file.name, url: file.url },
|
||||
});
|
||||
});
|
||||
|
||||
if (tradeDirection === 'IMPORT') {
|
||||
const extraTransit = files
|
||||
.filter((f) => f.code && isImportTransitPermitFileCode(f.code) && !included.has(f.code))
|
||||
.sort((a, b) => (a.code ?? '').localeCompare(b.code ?? ''));
|
||||
|
||||
extraTransit.forEach((file, index) => {
|
||||
if (!file.code) return;
|
||||
included.add(file.code);
|
||||
out.push({
|
||||
code: file.code,
|
||||
label: transitPermitFileLabel(file.code, index),
|
||||
uploadedBy: 'gl_et',
|
||||
category: 'transit',
|
||||
file: { id: file.id, name: file.name, url: file.url },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,14 @@ export class FilesService {
|
||||
return this.upload(input);
|
||||
}
|
||||
|
||||
async deleteByCode(
|
||||
resourceId: string,
|
||||
resource: string,
|
||||
code: string,
|
||||
): Promise<void> {
|
||||
await this.filesRepository.deleteByCode(resourceId, resource, code);
|
||||
}
|
||||
|
||||
async uploadMany(
|
||||
resourceId: string,
|
||||
resource: string,
|
||||
|
||||
@@ -219,13 +219,13 @@ export class PaymentService {
|
||||
|
||||
//////////////// fake
|
||||
|
||||
await this.datasource.manager.update(
|
||||
Booking,
|
||||
{ id: input.referenceId },
|
||||
{ status: "PAID", paymentStatus: "PAID" },
|
||||
);
|
||||
await this.firstMileService.acceptBooking(input.referenceId);
|
||||
await this.bookingBatchService.ensurePaidBookingAllocated(input.referenceId);
|
||||
// await this.datasource.manager.update(
|
||||
// Booking,
|
||||
// { id: input.referenceId },
|
||||
// { status: "PAID", paymentStatus: "PAID" },
|
||||
// );
|
||||
// await this.firstMileService.acceptBooking(input.referenceId);
|
||||
// await this.bookingBatchService.ensurePaidBookingAllocated(input.referenceId);
|
||||
|
||||
|
||||
//////////////// fake
|
||||
|
||||
Reference in New Issue
Block a user