Repository: UK-Export-Finance/dtfs2
Pull request #4258: fix(DTFS2-6838): show validation errors on eligibility criteria page
Author: login: vamshidharukef
Labels: name: Type: Bug
description: Something isn't working
Created: 2025-03-12T16:31:46Z | Merged: 2025-03-19T10:08:40Z

## Introduction :pencil2:
On Eligibility Criteria page when user skips eligibility questions and  clicks Next validation errors are not displayed.

## Resolution :heavy_check_mark:
Added condition to check if errors are greater than 0 redirect to eligibility criteria page if not redirect to supporting documentation page.

## Miscellaneous :heavy_plus_sign:
Added Unit test and updated E2E test




Diff:
diff --git a/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-criteria.spec.js b/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-criteria.spec.js
index 594e948620c..a7045e6eca2 100644
--- a/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-criteria.spec.js
+++ b/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-criteria.spec.js
@@ -125,4 +125,37 @@ context('Eligibility Criteria', () => {
     cy.clickSaveGoBackButton();
     contract.eligibilitySubmissionType().should('be.visible');
   });
+
+  it('should display validation errors when submitting without selecting criteria', () => {
+    // Click the "Next" button without selecting any criteria
+    eligibilityCriteria.nextPageButton().click();
+
+    // Check if we're still on the same page
+    cy.url().should('include', '/eligibility/criteria');
+
+    // Check for validation error messages
+    cy.get('.govuk-error-summary').should('be.visible');
+
+    // Check that we have the correct number of error messages
+    errorSummaryLinks().should('have.length', criteriaCount);
+
+    // Check for field-level errors
+    eligibilityCriteria.eligibilityCriteriaItems().each(($el, index) => {
+      cy.wrap($el)
+        .find('.govuk-error-message')
+        .should('be.visible')
+        .and('contain.text', `Eligibility criterion ${index + 11} is required`);
+    });
+  });
+
+  it('should proceed to supporting documentation page when valid criteria are selected', () => {
+    // Select 'Yes' for all criteria
+    eligibilityCriteria.eligibilityCriteriaItemsRadioButtons.trueInput().click({ multiple: true });
+
+    // Click the "Next" button
+    eligibilityCriteria.nextPageButton().click();
+
+    // Check if we're redirected to the supporting documentation page
+    cy.url().should('include', '/eligibility/supporting-documentation');
+  });
 });
diff --git a/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-documentation.spec.js b/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-documentation.spec.js
index 610477cfd1b..1cbd9af2910 100644
--- a/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-documentation.spec.js
+++ b/e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-documentation.spec.js
@@ -32,6 +32,7 @@ context('Eligibility Documentation', () => {
     eligibilityCriteria.eligibilityCriteriaItemsRadioButtons.trueInput().click({ multiple: true });
     eligibilityCriteria.eligibilityCriteriaFalse(14).click();
     eligibilityCriteria.nextPageButton().click();
+    eligibilityDocumentation.saveButton().click();
     eligibilityDocumentation.fieldErrorMessage('exporterQuestionnaire').should('have.length', 1);
   });
 
@@ -42,6 +43,7 @@ context('Eligibility Documentation', () => {
   });
 
   it('should show optional for optional supporting documentations', () => {
+    eligibilityCriteria.eligibilityCriteriaItemsRadioButtons.trueInput().click({ multiple: true });
     eligibilityCriteria.nextPageButton().click();
     eligibilityDocumentation.questionnaireFileInput().should('not.contain', '(optional)');
     eligibilityDocumentation.financialStatements().contains('Financial statements for the past 3 years (optional)');
diff --git a/portal/server/routes/contract/eligibility/index.js b/portal/server/routes/contract/eligibility/index.js
index 6ff4adc3161..48777301356 100644
--- a/portal/server/routes/contract/eligibility/index.js
+++ b/portal/server/routes/contract/eligibility/index.js
@@ -72,8 +72,16 @@ router.post('/contract/:_id/eligibility/criteria', async (req, res) => {
   const { _id, userToken } = requestParams(req);
   const { body } = req;
 
-  await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
+  const updatedEligibilityCriteria = api.updateEligibilityCriteria(_id, body, userToken);
 
+  const updatedDeal = await getApiData(updatedEligibilityCriteria, res);
+
+  if (updatedDeal.eligibility?.validationErrors?.count) {
+    // If there are validation errors, redirect back to the criteria page with a query parameter
+    return res.redirect(`/contract/${_id}/eligibility/criteria?errors=true`);
+  }
+
+  // If no errors, proceed to the supporting-documentation page
   return res.redirect(`/contract/${_id}/eligibility/supporting-documentation`);
 });
 
@@ -101,13 +109,18 @@ router.get('/contract/:_id/eligibility/supporting-documentation', [validateRole(
 
   const documentationValidationErrors = generateErrorSummary(supportingInformation.validationErrors, eligibilityErrorHref);
 
+  const criteriaValidationErrors = generateErrorSummary(eligibility.validationErrors, eligibilityErrorHref);
+
   const completedForms = completedEligibilityForms(deal.eligibility.status, validationErrors);
 
   return res.render('eligibility/eligibility-supporting-documentation.njk', {
     _id: deal._id,
     supportingInformation,
     eligibility,
-    validationErrors: documentationValidationErrors,
+    validationErrors: {
+      ...documentationValidationErrors,
+      ...criteriaValidationErrors, // Include criteria errors
+    },
     additionalRefName: deal.additionalRefName,
     user: req.session.user,
     taskListItems: eligibilityTaskList(completedForms),
diff --git a/portal/server/routes/contract/eligibility/index.test.js b/portal/server/routes/contract/eligibility/index.test.js
new file mode 100644
index 00000000000..6ba0784d52b
--- /dev/null
+++ b/portal/server/routes/contract/eligibility/index.test.js
@@ -0,0 +1,100 @@
+import { HttpStatusCode } from 'axios';
+
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    // Arrange
+    it('should redirect to criteria page with errors when validation fails', async () => {
+      const mockUpdatedDeal = {
+        eligibility: {
+          validationErrors: {
+            count: 8,
+            errorList: {
+              criteriaNotSelected: { order: 1, text: 'Eligibility criterion 11 is required' },
+              criteriaNotSelected2: { order: 2, text: 'Eligibility criterion 12 is required' },
+              criteriaNotSelected3: { order: 3, text: 'Eligibility criterion 13 is required' },
+              criteriaNotSelected4: { order: 4, text: 'Eligibility criterion 14 is required' },
+              criteriaNotSelected5: { order: 5, text: 'Eligibility criterion 15 is required' },
+              criteriaNotSelected6: { order: 6, text: 'Eligibility criterion 16 is required' },
+              criteriaNotSelected7: { order: 7, text: 'Eligibility criterion 17 is required' },
+              criteriaNotSelected8: { order: 8, text: 'Eligibility criterion 18 is required' },
+            },
+          },
+        },
+      };
+
+      api.updateEligibilityCriteria = jest.fn(() => Promise.resolve(mockUpdatedDeal));
+      requestParams.mockReturnValue({ _id: '123456', userToken: 'token' });
+      getApiData.mockResolvedValue(mockUpdatedDeal);
+
+      // Act
+      const response = await request(app).post('/contract/123456/eligibility/criteria').send({});
+
+      // Assert
+      expect(api.updateEligibilityCriteria).toHaveBeenCalledWith('123456', {}, 'token');
+      expect(response.status).toBe(HttpStatusCode.Found);
+      expect(response.headers.location).toBe('/contract/123456/eligibility/criteria?errors=true');
+      expect(mockUpdatedDeal.eligibility.validationErrors.count).toBe(8);
+    });
+
+    it('should redirect to supporting documentation page when validationErrors does not exist', async () => {
+      // Arrange
+      const mockUpdatedDeal = {
+        eligibility: {},
+      };
+
+      api.updateEligibilityCriteria = jest.fn(() => Promise.resolve(mockUpdatedDeal));
+      requestParams.mockReturnValue({ _id: '123456', userToken: 'token' });
+      getApiData.mockResolvedValue(mockUpdatedDeal);
+
+      // Act
+      const response = await request(app).post('/contract/123456/eligibility/criteria').send({});
+
+      // Assert
+      expect(api.updateEligibilityCriteria).toHaveBeenCalledWith('123456', {}, 'token');
+      expect(response.status).toBe(HttpStatusCode.Found);
+      expect(response.headers.location).toBe('/contract/123456/eligibility/supporting-documentation');
+      expect(mockUpdatedDeal.eligibility.validationErrors).toBeUndefined();
+    });
+
+    it('should redirect to supporting documentation page when criteria are valid', async () => {
+      // Arrange
+      const mockUpdatedDeal = {
+        eligibility: {
+          validationErrors: {
+            count: 0,
+          },
+        },
+      };
+
+      api.updateEligibilityCriteria = jest.fn(() => Promise.resolve(mockUpdatedDeal));
+      requestParams.mockReturnValue({ _id: '123456', userToken: 'token' });
+      getApiData.mockResolvedValue(mockUpdatedDeal);
+
+      // Act
+      const response = await request(app).post('/contract/123456/eligibility/criteria').send({ someCriteria: 'selected' });
+
+      // Assert
+      expect(api.updateEligibilityCriteria).toHaveBeenCalledWith('123456', { someCriteria: 'selected' }, 'token');
+      expect(response.status).toBe(HttpStatusCode.Found);
+      expect(response.headers.location).toBe('/contract/123456/eligibility/supporting-documentation');
+      expect(mockUpdatedDeal.eligibility.validationErrors.count).toBe(0);
+    });
+  });
+});


Reviews:
user: login: zainUKEF
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T19:01:27Z
user: login: vamshidharukef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T14:07:59Z
user: login: abhi-markan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:15:02Z
user: login: abhi-markan
body: Nice work, few comments to address please.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:25:50Z
user: login: vamshidharukef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T13:03:51Z
user: login: vamshidharukef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:06:20Z
user: login: vamshidharukef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:06:35Z
user: login: vamshidharukef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:06:52Z
user: login: vamshidharukef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:07:12Z
user: login: vamshidharukef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:08:45Z
user: login: abhi-markan
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:43:51Z
user: login: zainUKEF
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:43:55Z
user: login: zainUKEF
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:44:43Z
user: login: rkontogianni
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T10:01:29Z

Review comments:
diff_hunk: @@ -72,8 +72,14 @@ router.post('/contract/:_id/eligibility/criteria', async (req, res) => {
   const { _id, userToken } = requestParams(req);
   const { body } = req;
 
-  await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
+  const updatedDeal = await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
 
+  if (updatedDeal.eligibility.validationErrors && updatedDeal.eligibility.validationErrors.count > 0) {
path: portal/server/routes/contract/eligibility/index.js
user: login: zainUKEF
body: ```suggestion
  if (updatedDeal.eligibility?.validationErrors?.count) {
```
does this work?
created_at: 2025-03-12T19:01:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,8 +72,14 @@ router.post('/contract/:_id/eligibility/criteria', async (req, res) => {
   const { _id, userToken } = requestParams(req);
   const { body } = req;
 
-  await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
+  const updatedDeal = await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
 
+  if (updatedDeal.eligibility.validationErrors && updatedDeal.eligibility.validationErrors.count > 0) {
path: portal/server/routes/contract/eligibility/index.js
user: login: vamshidharukef
body: Yes, I had changed as suggested.
created_at: 2025-03-13T14:07:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,8 +72,14 @@ router.post('/contract/:_id/eligibility/criteria', async (req, res) => {
   const { _id, userToken } = requestParams(req);
   const { body } = req;
 
-  await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
+  const updatedDeal = await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
path: portal/server/routes/contract/eligibility/index.js
user: login: abhi-markan
body: ```suggestion
  const updatedEligiblityCriteria = api.updateEligibilityCriteria(_id, body, userToken);
  const updatedDeal = await getApiData(updatedEligiblityCriteria, res);
```
created_at: 2025-03-17T16:15:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
path: portal/server/routes/contract/eligibility/index.test.js
user: login: abhi-markan
body: Please write test with `Arrange-Assert-Act` pattern.
created_at: 2025-03-17T16:18:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
+      const mockUpdatedDeal = {
+        eligibility: {
+          validationErrors: {
+            count: 1,
+            errorList: {
+              criteriaNotSelected: { order: 1, text: 'Select at least one eligibility criterion' },
+            },
+          },
+        },
+      };
+
+      api.updateEligibilityCriteria = jest.fn(() => Promise.resolve(mockUpdatedDeal));
+      requestParams.mockReturnValue({ _id: '123456', userToken: 'token' });
+      getApiData.mockResolvedValue(mockUpdatedDeal);
+
+      const response = await request(app).post('/contract/123456/eligibility/criteria').send({});
+
+      expect(api.updateEligibilityCriteria).toHaveBeenCalledWith('123456', {}, 'token');
+      expect(response.status).toBe(302);
path: portal/server/routes/contract/eligibility/index.test.js
user: login: abhi-markan
body: Please use HttpStatusCode for `302`.
created_at: 2025-03-17T16:23:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
+      const mockUpdatedDeal = {
+        eligibility: {
+          validationErrors: {
+            count: 1,
+            errorList: {
+              criteriaNotSelected: { order: 1, text: 'Select at least one eligibility criterion' },
+            },
+          },
+        },
+      };
+
+      api.updateEligibilityCriteria = jest.fn(() => Promise.resolve(mockUpdatedDeal));
+      requestParams.mockReturnValue({ _id: '123456', userToken: 'token' });
+      getApiData.mockResolvedValue(mockUpdatedDeal);
+
+      const response = await request(app).post('/contract/123456/eligibility/criteria').send({});
+
+      expect(api.updateEligibilityCriteria).toHaveBeenCalledWith('123456', {}, 'token');
+      expect(response.status).toBe(302);
+      expect(response.headers.location).toBe('/contract/123456/eligibility/criteria?errors=true');
+    });
+
+    it('should redirect to supporting documentation page when criteria are valid', async () => {
path: portal/server/routes/contract/eligibility/index.test.js
user: login: abhi-markan
body: Can we add assertion where `validationErrors` does not exist at all please.
created_at: 2025-03-17T16:24:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
+      const mockUpdatedDeal = {
path: portal/server/routes/contract/eligibility/index.test.js
user: login: abhi-markan
body: Can we add assertion with all the errors on the page.
created_at: 2025-03-17T16:24:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,8 +72,14 @@ router.post('/contract/:_id/eligibility/criteria', async (req, res) => {
   const { _id, userToken } = requestParams(req);
   const { body } = req;
 
-  await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
+  const updatedDeal = await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
path: portal/server/routes/contract/eligibility/index.js
user: login: abhi-markan
body: Will this also covers the document upload page or that page already has error validation in place? Thanks
created_at: 2025-03-17T16:24:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,4 +125,34 @@ context('Eligibility Criteria', () => {
     cy.clickSaveGoBackButton();
     contract.eligibilitySubmissionType().should('be.visible');
   });
+
+  it('should display validation errors when submitting without selecting criteria', () => {
+    // Click the "Next" button without selecting any criteria
+    eligibilityCriteria.nextPageButton().click();
+
+    // Check if we're still on the same page
+    cy.url().should('include', '/eligibility/criteria');
+
+    // Check for validation error messages
+    cy.get('.govuk-error-summary').should('be.visible');
+
+    // Check that we have the correct number of error messages
+    errorSummaryLinks().should('have.length', criteriaCount);
+
+    // Check for field-level errors (adjust the selector as needed)
+    eligibilityCriteria.eligibilityCriteriaItems().each(($el) => {
path: e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-criteria.spec.js
user: login: abhi-markan
body: Need to assert error mesage text please.
created_at: 2025-03-17T16:25:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,4 +125,34 @@ context('Eligibility Criteria', () => {
     cy.clickSaveGoBackButton();
     contract.eligibilitySubmissionType().should('be.visible');
   });
+
+  it('should display validation errors when submitting without selecting criteria', () => {
+    // Click the "Next" button without selecting any criteria
+    eligibilityCriteria.nextPageButton().click();
+
+    // Check if we're still on the same page
+    cy.url().should('include', '/eligibility/criteria');
+
+    // Check for validation error messages
+    cy.get('.govuk-error-summary').should('be.visible');
+
+    // Check that we have the correct number of error messages
+    errorSummaryLinks().should('have.length', criteriaCount);
+
+    // Check for field-level errors (adjust the selector as needed)
+    eligibilityCriteria.eligibilityCriteriaItems().each(($el) => {
path: e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-criteria.spec.js
user: login: vamshidharukef
body: Done
created_at: 2025-03-18T13:03:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
path: portal/server/routes/contract/eligibility/index.test.js
user: login: vamshidharukef
body: Done
created_at: 2025-03-18T14:06:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
+      const mockUpdatedDeal = {
path: portal/server/routes/contract/eligibility/index.test.js
user: login: vamshidharukef
body: Done
created_at: 2025-03-18T14:06:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
+      const mockUpdatedDeal = {
+        eligibility: {
+          validationErrors: {
+            count: 1,
+            errorList: {
+              criteriaNotSelected: { order: 1, text: 'Select at least one eligibility criterion' },
+            },
+          },
+        },
+      };
+
+      api.updateEligibilityCriteria = jest.fn(() => Promise.resolve(mockUpdatedDeal));
+      requestParams.mockReturnValue({ _id: '123456', userToken: 'token' });
+      getApiData.mockResolvedValue(mockUpdatedDeal);
+
+      const response = await request(app).post('/contract/123456/eligibility/criteria').send({});
+
+      expect(api.updateEligibilityCriteria).toHaveBeenCalledWith('123456', {}, 'token');
+      expect(response.status).toBe(302);
path: portal/server/routes/contract/eligibility/index.test.js
user: login: vamshidharukef
body: Done
created_at: 2025-03-18T14:06:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+const request = require('supertest');
+const express = require('express');
+const router = require('./index'); // Adjust the path as necessary
+const { getApiData, requestParams } = require('../../../helpers');
+const api = require('../../../api');
+
+jest.mock('../../../helpers');
+jest.mock('../../../api');
+
+const app = express();
+app.use(express.json());
+app.use(router);
+
+describe('eligibility criteria', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('POST /contract/:_id/eligibility/criteria', () => {
+    it('should redirect to criteria page with errors when validation fails', async () => {
+      const mockUpdatedDeal = {
+        eligibility: {
+          validationErrors: {
+            count: 1,
+            errorList: {
+              criteriaNotSelected: { order: 1, text: 'Select at least one eligibility criterion' },
+            },
+          },
+        },
+      };
+
+      api.updateEligibilityCriteria = jest.fn(() => Promise.resolve(mockUpdatedDeal));
+      requestParams.mockReturnValue({ _id: '123456', userToken: 'token' });
+      getApiData.mockResolvedValue(mockUpdatedDeal);
+
+      const response = await request(app).post('/contract/123456/eligibility/criteria').send({});
+
+      expect(api.updateEligibilityCriteria).toHaveBeenCalledWith('123456', {}, 'token');
+      expect(response.status).toBe(302);
+      expect(response.headers.location).toBe('/contract/123456/eligibility/criteria?errors=true');
+    });
+
+    it('should redirect to supporting documentation page when criteria are valid', async () => {
path: portal/server/routes/contract/eligibility/index.test.js
user: login: vamshidharukef
body: Done
created_at: 2025-03-18T14:07:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,8 +72,14 @@ router.post('/contract/:_id/eligibility/criteria', async (req, res) => {
   const { _id, userToken } = requestParams(req);
   const { body } = req;
 
-  await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
+  const updatedDeal = await getApiData(api.updateEligibilityCriteria(_id, body, userToken), res);
path: portal/server/routes/contract/eligibility/index.js
user: login: vamshidharukef
body: That page has error validation in place.
created_at: 2025-03-18T14:08:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,13 +109,18 @@ router.get('/contract/:_id/eligibility/supporting-documentation', [validateRole(
 
   const documentationValidationErrors = generateErrorSummary(supportingInformation.validationErrors, eligibilityErrorHref);
 
+  const criteriaValidationErrors = generateErrorSummary(eligibility.validationErrors, eligibilityErrorHref);
+
   const completedForms = completedEligibilityForms(deal.eligibility.status, validationErrors);
 
   return res.render('eligibility/eligibility-supporting-documentation.njk', {
     _id: deal._id,
     supportingInformation,
     eligibility,
-    validationErrors: documentationValidationErrors,
+    validationErrors: {
path: portal/server/routes/contract/eligibility/index.js
user: login: abhi-markan
body: Not for this PR, but I would be nice to only send `validationError` object if there is an error however looking at DTFS codebase this is not a widely followed practice.
created_at: 2025-03-19T08:43:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,4 +125,37 @@ context('Eligibility Criteria', () => {
     cy.clickSaveGoBackButton();
     contract.eligibilitySubmissionType().should('be.visible');
   });
+
+  it('should display validation errors when submitting without selecting criteria', () => {
+    // Click the "Next" button without selecting any criteria
+    eligibilityCriteria.nextPageButton().click();
+
+    // Check if we're still on the same page
+    cy.url().should('include', '/eligibility/criteria');
+
+    // Check for validation error messages
+    cy.get('.govuk-error-summary').should('be.visible');
+
+    // Check that we have the correct number of error messages
+    errorSummaryLinks().should('have.length', criteriaCount);
+
+    // Check for field-level errors
+    eligibilityCriteria.eligibilityCriteriaItems().each(($el, index) => {
+      cy.wrap($el)
+        .find('.govuk-error-message')
+        .should('be.visible')
+        .and('contain.text', `Eligibility criterion ${index + 11} is required`);
+    });
+  });
+
+  it('should proceed to supporting documentation page when valid criteria are selected', () => {
+    // Select 'Yes' for all criteria
+    eligibilityCriteria.eligibilityCriteriaItemsRadioButtons.trueInput().click({ multiple: true });
+
+    // Click the "Next" button
+    eligibilityCriteria.nextPageButton().click();
+
+    // Check if we're redirected to the supporting documentation page
+    cy.url().should('include', '/eligibility/supporting-documentation');
path: e2e-tests/portal/cypress/e2e/journeys/maker/eligibility/eligibility-criteria.spec.js
user: login: zainUKEF
body: next PR.  Should be full URL
created_at: 2025-03-19T09:43:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-18T14:14:46Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=UK-Export-Finance_dtfs2&pullRequest=4258) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=UK-Export-Finance_dtfs2&pullRequest=4258&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=UK-Export-Finance_dtfs2&pullRequest=4258&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=UK-Export-Finance_dtfs2&pullRequest=4258&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=UK-Export-Finance_dtfs2&pullRequest=4258&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=UK-Export-Finance_dtfs2&pullRequest=4258&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=UK-Export-Finance_dtfs2&pullRequest=4258)


author_association: NONE