Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/services/handlers/storage-notes.jest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2467,6 +2467,59 @@ describe("/create-non-manipulation-document/:documentNumber/departure-product-su

expect(errors["catches-0-netWeightFisheryProductDeparture"]).toBeUndefined();
});

it("Scenario 5: returns error when netWeightFisheryProductDeparture exceeds netWeightFisheryProductArrival", async () => {
const currentUrl = "/create-non-manipulation-document/:documentNumber/departure-product-summary";
const handler = StorageNotes[currentUrl];

const data = {
catches: [
{
netWeightProductDeparture: "100",
netWeightProductArrival: "100",
netWeightFisheryProductArrival: "80",
netWeightFisheryProductDeparture: "90",
},
],
};

const { errors } = await handler({ data, errors: {} });

expect(errors["catches-0-netWeightFisheryProductDeparture"]).toBe("sdNetWeightFisheryProductDepartureExceedsArrival");
});

it("Scenario 5: no error when netWeightFisheryProductDeparture is equal to netWeightFisheryProductArrival", async () => {
const currentUrl = "/create-non-manipulation-document/:documentNumber/departure-product-summary";
const handler = StorageNotes[currentUrl];

const dataEqual = {
catches: [
{
netWeightProductDeparture: "100",
netWeightProductArrival: "100",
netWeightFisheryProductArrival: "80",
netWeightFisheryProductDeparture: "80",
},
],
};

const dataLess = {
catches: [
{
netWeightProductDeparture: "100",
netWeightProductArrival: "100",
netWeightFisheryProductArrival: "80",
netWeightFisheryProductDeparture: "70",
},
],
};

const { errors: errorsEqual } = await handler({ data: dataEqual, errors: {} });
const { errors: errorsLess } = await handler({ data: dataLess, errors: {} });

expect(errorsEqual["catches-0-netWeightFisheryProductDeparture"]).toBeUndefined();
expect(errorsLess["catches-0-netWeightFisheryProductDeparture"]).toBeUndefined();
});
});

describe("/create-non-manipulation-document/:documentNumber/add-storage-facility-details", () => {
Expand Down
41 changes: 16 additions & 25 deletions src/services/handlers/storage-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ import { validateCommodityCode } from "../../validators/pssdCommodityCode.valida
import { BusinessError, SpeciesSuggestionError } from "../../validators/validationErrors";
import { ICountry } from "../../persistence/schema/common";
import { isEmpty } from "lodash";
// Import departure vs arrival weight comparisons from the shared validator.
// They are defined there (not here) to avoid a circular dependency with
// orchestration.service.ts, which also needs to call them at submission time.
import {
checkNetWeightProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsProductDeparture,
} from '../../validators/storageWeightValidator';
// Re-export so existing consumers (progress.service, tests) can continue to
// import these from storage-notes without code changes.
export {
checkNetWeightProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsProductDeparture,
} from '../../validators/storageWeightValidator';

export const initialState = {
storageNotes: {
Expand Down Expand Up @@ -60,6 +75,7 @@ export default {
checkNetWeightProductDepartureIsZeroPositive(ctch, index, errors);
checkNetWeightFisheryProductDepartureIsZeroPositive(ctch, index, errors);
checkNetWeightProductDepartureExceedsArrival(ctch, index, errors);
checkNetWeightFisheryProductDepartureExceedsArrival(ctch, index, errors);
checkNetWeightFisheryProductDepartureExceedsProductDeparture(ctch, index, errors);

if (isEmpty(errors)) {
Expand Down Expand Up @@ -148,31 +164,6 @@ export function checkNetWeightFisheryProductDepartureIsZeroPositive(ctch: any, i
}
}

// Scenario 1 & 2: departure weight cannot exceed arrival weight
export function checkNetWeightProductDepartureExceedsArrival(ctch: any, index: number, errors: any) {
if (
!errors[`catches-${index}-netWeightProductDeparture`] &&
ctch.netWeightProductDeparture &&
ctch.netWeightProductArrival &&
(+ctch.netWeightProductDeparture) > (+ctch.netWeightProductArrival)
) {
errors[`catches-${index}-netWeightProductDeparture`] = 'sdNetWeightProductDepartureExceedsArrival';
}
}

// Scenario 3: fishery product departure weight cannot exceed net product departure weight
export function checkNetWeightFisheryProductDepartureExceedsProductDeparture(ctch: any, index: number, errors: any) {
if (
!errors[`catches-${index}-netWeightFisheryProductDeparture`] &&
!errors[`catches-${index}-netWeightProductDeparture`] &&
ctch.netWeightFisheryProductDeparture &&
ctch.netWeightProductDeparture &&
(+ctch.netWeightFisheryProductDeparture) > (+ctch.netWeightProductDeparture)
) {
errors[`catches-${index}-netWeightFisheryProductDeparture`] = 'sdNetWeightFisheryProductDepartureExceedsProductDeparture';
}
}

function checkFacilityArrivalDateError(exportData: any, departureDate: string, errors) {
if (!validateDate(exportData.facilityArrivalDate)) {
errors[`storageFacilities-facilityArrivalDate`] = "sdArrivalDateValidationError";
Expand Down
65 changes: 65 additions & 0 deletions src/services/orchestration.service.jest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2433,6 +2433,71 @@ describe('checkValidationStorageNotes', () => {
expect(mockValidateCompletedDocument).not.toHaveBeenCalled();
expect(data.validationErrors).toHaveLength(0);
});

it('should push error when netWeightProductDeparture exceeds netWeightProductArrival at submission (FI0-11277)', async () => {
const data: any = {
catches: [{
certificateNumber: 'FR-2022-CC-123',
product: 'Atlantic herring',
certificateType: 'non_uk',
netWeightProductArrival: '100',
netWeightFisheryProductArrival: '80',
netWeightProductDeparture: '120',
netWeightFisheryProductDeparture: '70',
}],
validationErrors: [],
};

await OrchestrationService.checkValidationStorageNotes(data, 'user', 'contact', 'GBR-2022-SD-123456789');

expect(data.validationErrors).toHaveLength(1);
expect(data.validationErrors[0]).toMatchObject({
message: 'sdNetWeightProductDepartureExceedsArrival',
key: 'catches-0-netWeightProductDeparture',
});
});

it('should push error when netWeightFisheryProductDeparture exceeds netWeightFisheryProductArrival at submission (FI0-11277)', async () => {
const data: any = {
catches: [{
certificateNumber: 'FR-2022-CC-123',
product: 'Atlantic herring',
certificateType: 'non_uk',
netWeightProductArrival: '100',
netWeightFisheryProductArrival: '80',
netWeightProductDeparture: '100',
netWeightFisheryProductDeparture: '90',
}],
validationErrors: [],
};

await OrchestrationService.checkValidationStorageNotes(data, 'user', 'contact', 'GBR-2022-SD-123456789');

expect(data.validationErrors).toHaveLength(1);
expect(data.validationErrors[0]).toMatchObject({
message: 'sdNetWeightFisheryProductDepartureExceedsArrival',
key: 'catches-0-netWeightFisheryProductDeparture',
});
});

it('should not push weight errors when all weights are valid at submission (FI0-11277)', async () => {
const data: any = {
catches: [{
certificateNumber: 'FR-2022-CC-123',
product: 'Atlantic herring',
certificateType: 'non_uk',
netWeightProductArrival: '100',
netWeightFisheryProductArrival: '80',
netWeightProductDeparture: '100',
netWeightFisheryProductDeparture: '80',
}],
validationErrors: [],
};

await OrchestrationService.checkValidationStorageNotes(data, 'user', 'contact', 'GBR-2022-SD-123456789');

expect(data.validationErrors).toHaveLength(0);
});
});

describe('clearDataFromJourney', () => {
Expand Down
25 changes: 21 additions & 4 deletions src/services/orchestration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ import { toFrontEndStorageDocumentExportData } from "../persistence/schema/stora
import { reportDocumentSubmitted, submitToCatchSystem } from "../services/reference-data.service";
import { invalidateDraftCache, setCatchSubmissionInProgress } from '../persistence/services/catchCert'
import { validateCompletedDocument, validateSpecies } from "../validators/documentValidator";
import {
checkNetWeightProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsProductDeparture,
} from '../validators/storageWeightValidator';
import * as EuCountriesService from './eu-countries.service';

export const catchCerts: string = "catchCertificate";
Expand Down Expand Up @@ -502,24 +507,36 @@ export default class OrchestrationService {

static readonly checkValidationStorageNotes = async (data, userPrincipal: string, contactId: string, documentNumber: string) => {
for (const ctch in data.catches) {
const documentCertificateNumber = data.catches[ctch].certificateNumber;
const species = data.catches[ctch].product;
const singleCatch = data.catches[ctch];
const documentCertificateNumber = singleCatch.certificateNumber;
const species = singleCatch.product;
const speciesCode = null;
if (data.catches[ctch].certificateType === 'uk' && (!await validateCompletedDocument(documentCertificateNumber, userPrincipal, contactId, documentNumber))) {
if (singleCatch.certificateType === 'uk' && (!await validateCompletedDocument(documentCertificateNumber, userPrincipal, contactId, documentNumber))) {
data.validationErrors.push({
message: 'sdAddCatchDetailsErrorUKDocumentInvalid',
key: `catches-${ctch}-certificateNumber`,
certificateNumber: documentCertificateNumber,
product: species
});
} else if (data.catches[ctch].certificateType === 'uk' && !await validateSpecies(documentCertificateNumber, species, speciesCode, userPrincipal, contactId, documentNumber)) {
} else if (singleCatch.certificateType === 'uk' && !await validateSpecies(documentCertificateNumber, species, speciesCode, userPrincipal, contactId, documentNumber)) {
data.validationErrors.push({
message: 'sdAddUKEntryDocumentSpeciesDoesNotExistError',
key: `catches-${ctch}-certificateNumber`,
certificateNumber: documentCertificateNumber,
product: species
});
}

// Re-validate departure vs arrival weight relationships at submission time.
// Catches user-edited arrival weights that bypassed the departure-product-summary
// page (FI0-11277).
const weightErrors: { [key: string]: any } = {};
checkNetWeightProductDepartureExceedsArrival(singleCatch, +ctch, weightErrors);
checkNetWeightFisheryProductDepartureExceedsArrival(singleCatch, +ctch, weightErrors);
checkNetWeightFisheryProductDepartureExceedsProductDeparture(singleCatch, +ctch, weightErrors);
for (const [key, message] of Object.entries(weightErrors)) {
data.validationErrors.push({ message, key });
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions src/services/progress.service.jest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4998,6 +4998,47 @@ describe('getStorageDocumentProgress', () => {
expect(result.progress).toMatchObject({ transportDetails: ProgressStatus.INCOMPLETE });
});

it('will return INCOMPLETE transportDetails when fishery departure weight exceeds fishery arrival weight with valid product departure weight (FI0-11277 Scenario 5)', async () => {
mockStorageDocumentDraft.mockResolvedValue({
exportData: {
catches: [
{
product: 'Atlantic cod (COD)',
productDescription: 'Some product description',
commodityCode: '45345454354',
certificateNumber: 'DSFDSF',
certificateType: 'non_uk',
issuingCountry: {
officialCountryName: 'SPAIN',
isoCodeAlpha2: 'ES',
isoCodeAlpha3: 'ESP',
isoNumericCode: '724',
},
productWeight: '5',
weightOnCC: '5',
placeOfUnloading: 'sdfdf',
dateOfUnloading: '24/01/2022',
transportUnloadedFrom: 'sfdfd',
id: 'dsfdsf-1643629199',
netWeightProductArrival: '100',
netWeightFisheryProductArrival: '80',
netWeightProductDeparture: '100', // valid: equals arrival
netWeightFisheryProductDeparture: '90', // invalid: 90 > 80 (fishery arrival)
},
],
},
});

const result = await ProgressService.getStorageDocumentProgress(
userPrincipal,
documentNumber,
'contactBob'
);

expect(result.progress).toMatchObject({ catches: ProgressStatus.COMPLETED });
expect(result.progress).toMatchObject({ transportDetails: ProgressStatus.INCOMPLETE });
});

it('will return INCOMPLETED catches if any of the weights are invalid', async () => {
mockStorageDocumentDraft.mockResolvedValue({
exportData: {
Expand Down
6 changes: 5 additions & 1 deletion src/services/progress.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ import {
checkEitherNetWeightProductDepartureAndNetWeightFisheryProductDepartureIsPresent,
checkNetWeightProductDepartureIsZeroPositive,
checkNetWeightFisheryProductDepartureIsZeroPositive,
} from './handlers/storage-notes';
import {
checkNetWeightProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsArrival,
checkNetWeightFisheryProductDepartureExceedsProductDeparture,
} from './handlers/storage-notes';
} from '../validators/storageWeightValidator';
import { isInvalidLength, validateWhitespace } from './orchestration.service';
import * as FrontEndCatchCertificateTransport from "../persistence/schema/frontEndModels/catchCertificateTransport";
import catchCertificateTransportDetailsSchema from "../schemas/catchcerts/catchCertificateTransportDetailsSchema";
Expand Down Expand Up @@ -489,6 +492,7 @@ export default class ProgressService {
checkNetWeightProductDepartureIsZeroPositive(singleCatch, index, weightsErrors);
checkNetWeightFisheryProductDepartureIsZeroPositive(singleCatch, index, weightsErrors);
checkNetWeightProductDepartureExceedsArrival(singleCatch, index, weightsErrors);
checkNetWeightFisheryProductDepartureExceedsArrival(singleCatch, index, weightsErrors);
checkNetWeightFisheryProductDepartureExceedsProductDeparture(singleCatch, index, weightsErrors);
if (Object.keys(weightsErrors).length > 0) {
return false;
Expand Down
44 changes: 44 additions & 0 deletions src/validators/storageWeightValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Departure vs arrival weight comparison checks for NMD (Storage Documents).
*
* Extracted into a standalone module to avoid a circular dependency between
* storage-notes.ts (which imports utility validators from orchestration.service)
* and orchestration.service.ts (which needs these checks at submission time).
*/

// Scenario 1 & 2: departure weight cannot exceed arrival weight
export function checkNetWeightProductDepartureExceedsArrival(ctch: any, index: number, errors: any) {
if (
!errors[`catches-${index}-netWeightProductDeparture`] &&
ctch.netWeightProductDeparture &&
ctch.netWeightProductArrival &&
(+ctch.netWeightProductDeparture) > (+ctch.netWeightProductArrival)
) {
errors[`catches-${index}-netWeightProductDeparture`] = 'sdNetWeightProductDepartureExceedsArrival';
}
}

// Scenario 5: fishery product departure weight cannot exceed fishery product arrival weight
export function checkNetWeightFisheryProductDepartureExceedsArrival(ctch: any, index: number, errors: any) {
if (
!errors[`catches-${index}-netWeightFisheryProductDeparture`] &&
ctch.netWeightFisheryProductDeparture &&
ctch.netWeightFisheryProductArrival &&
(+ctch.netWeightFisheryProductDeparture) > (+ctch.netWeightFisheryProductArrival)
) {
errors[`catches-${index}-netWeightFisheryProductDeparture`] = 'sdNetWeightFisheryProductDepartureExceedsArrival';
}
}

// Scenario 3: fishery product departure weight cannot exceed net product departure weight
export function checkNetWeightFisheryProductDepartureExceedsProductDeparture(ctch: any, index: number, errors: any) {
if (
!errors[`catches-${index}-netWeightFisheryProductDeparture`] &&
!errors[`catches-${index}-netWeightProductDeparture`] &&
ctch.netWeightFisheryProductDeparture &&
ctch.netWeightProductDeparture &&
(+ctch.netWeightFisheryProductDeparture) > (+ctch.netWeightProductDeparture)
) {
errors[`catches-${index}-netWeightFisheryProductDeparture`] = 'sdNetWeightFisheryProductDepartureExceedsProductDeparture';
}
}
Loading