mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
Merge branch 'freight_feature/profile' of github.com:Tria-plc/edr-platform into freight_feature/profile
This commit is contained in:
@@ -146,3 +146,94 @@ describe('BookingTransitionService — finalizeClearance customs output gate', (
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The first clearance submission (AWAITING_DOCUMENTS) must include every
|
||||
* required input document; subsequent re-uploads during review only need the
|
||||
* specific files being fixed, so already-uploaded required docs stay in place.
|
||||
*/
|
||||
describe('BookingTransitionService — submitClearanceDocuments required-fields gate', () => {
|
||||
const inputSetting = {
|
||||
code: 'clearance_import_container_without_customs',
|
||||
fields: [
|
||||
{ fileKey: 'commercial_invoice', fileLabel: 'Commercial invoice', isRequired: true },
|
||||
{ fileKey: 'packing_list', fileLabel: 'Packing list', isRequired: true },
|
||||
],
|
||||
};
|
||||
|
||||
function makeService(status: string, existingCodes: string[]) {
|
||||
const bookingsRepository = {
|
||||
upsertDocumentReviewPending: jest.fn().mockResolvedValue(undefined),
|
||||
update: jest.fn().mockResolvedValue({ id: 'b-3' }),
|
||||
};
|
||||
const booking = {
|
||||
id: 'b-3',
|
||||
status,
|
||||
tradeDirection: 'IMPORT',
|
||||
freightType: 'CONTAINER',
|
||||
serviceType: { includesCustoms: false },
|
||||
};
|
||||
const bookingsService = { findById: jest.fn().mockResolvedValue(booking) };
|
||||
const fileUploadSettingsService = {
|
||||
getByCode: jest.fn().mockResolvedValue(inputSetting),
|
||||
};
|
||||
const filesService = {
|
||||
findByResource: jest
|
||||
.fn()
|
||||
.mockResolvedValue(existingCodes.map((code) => ({ code }))),
|
||||
upsertByCode: jest.fn().mockResolvedValue({ id: 'file-rec' }),
|
||||
};
|
||||
|
||||
const service = new BookingTransitionService(
|
||||
bookingsRepository as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
filesService as never,
|
||||
fileUploadSettingsService as never,
|
||||
{} as never,
|
||||
bookingsService as never,
|
||||
);
|
||||
return { service, bookingsRepository, filesService };
|
||||
}
|
||||
|
||||
function fakeFile(fieldname: string): Express.Multer.File {
|
||||
return { fieldname, originalname: `${fieldname}.pdf` } as Express.Multer.File;
|
||||
}
|
||||
|
||||
it('rejects the first submission when a required document is missing', async () => {
|
||||
const { service } = makeService('AWAITING_DOCUMENTS', []);
|
||||
await expect(
|
||||
service.submitClearanceDocuments('b-3', [fakeFile('commercial_invoice')]),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('accepts the first submission when every required document is provided', async () => {
|
||||
const { service, bookingsRepository } = makeService('AWAITING_DOCUMENTS', []);
|
||||
await service.submitClearanceDocuments('b-3', [
|
||||
fakeFile('commercial_invoice'),
|
||||
fakeFile('packing_list'),
|
||||
]);
|
||||
expect(bookingsRepository.update).toHaveBeenCalledWith(
|
||||
'b-3',
|
||||
expect.objectContaining({ status: 'DOCUMENTS_UNDER_REVIEW' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('allows re-uploading a single queried document during review without re-sending the rest', async () => {
|
||||
// packing_list was already uploaded in the first round; the customer is now
|
||||
// only re-uploading the queried commercial_invoice.
|
||||
const { service, bookingsRepository } = makeService(
|
||||
'DOCUMENTS_UNDER_REVIEW',
|
||||
['packing_list'],
|
||||
);
|
||||
await service.submitClearanceDocuments('b-3', [
|
||||
fakeFile('commercial_invoice'),
|
||||
]);
|
||||
// Only the re-uploaded doc is touched — no full re-gate, no rework on the rest.
|
||||
expect(bookingsRepository.upsertDocumentReviewPending).toHaveBeenCalledTimes(1);
|
||||
expect(bookingsRepository.upsertDocumentReviewPending).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ fileKey: 'commercial_invoice' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -645,6 +645,14 @@ export class BookingTransitionService {
|
||||
throw new BadRequestException('No documents uploaded');
|
||||
}
|
||||
|
||||
// First submission (nothing in review yet): every required input field must
|
||||
// be provided. Once review has started (DOCUMENTS_UNDER_REVIEW) the customer
|
||||
// is only fixing queried/pending docs, so the already-uploaded required docs
|
||||
// stay in place and we don't re-gate on the full required set.
|
||||
if (booking.status === 'AWAITING_DOCUMENTS') {
|
||||
await this.assertRequiredInputsPresent(bookingId, inputCode, files);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const record = await this.filesService.upsertByCode({
|
||||
resourceId: bookingId,
|
||||
@@ -670,6 +678,41 @@ export class BookingTransitionService {
|
||||
return this.bookingsService.findById(bookingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard for the first clearance submission: every required field of the
|
||||
* booking's customer-input set must be covered, either by a file already on
|
||||
* the booking or by one in this upload batch. Keeps the customer from starting
|
||||
* review with required documents missing.
|
||||
*/
|
||||
private async assertRequiredInputsPresent(
|
||||
bookingId: string,
|
||||
inputCode: string,
|
||||
files: Express.Multer.File[],
|
||||
): Promise<void> {
|
||||
let setting;
|
||||
try {
|
||||
setting = await this.fileUploadSettingsService.getByCode(inputCode);
|
||||
} catch {
|
||||
return; // setting not seeded — nothing to enforce
|
||||
}
|
||||
const required = (setting.fields ?? []).filter((f) => f.isRequired);
|
||||
if (required.length === 0) return;
|
||||
|
||||
const existing = await this.filesService.findByResource(bookingId, 'bookings');
|
||||
const presentKeys = new Set<string>([
|
||||
...existing.map((f) => f.code),
|
||||
...files.map((f) => f.fieldname),
|
||||
]);
|
||||
|
||||
const missing = required.filter((f) => !presentKeys.has(f.fileKey));
|
||||
if (missing.length > 0) {
|
||||
const labels = missing.map((f) => f.fileLabel).join(', ');
|
||||
throw new BadRequestException(
|
||||
`Please upload all required documents before submitting: ${labels}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** GL reviews a single document: APPROVED or QUERIED (with a note). */
|
||||
async reviewDocument(
|
||||
bookingId: string,
|
||||
|
||||
Reference in New Issue
Block a user