Repository: companieshouse/penalty-payment-web
Pull request #285: SAN-430 Error Page for 404 Not Found
Author: login: klai-ch
Created: 2025-05-09T13:14:43Z | Merged: 2025-05-12T15:19:59Z

SAN-430 Error Page for 404 Not Found
https://companieshouse.atlassian.net/browse/SAN-430


Diff:
diff --git a/src/main/java/uk/gov/companieshouse/web/pps/PPSWebApplication.java b/src/main/java/uk/gov/companieshouse/web/pps/PPSWebApplication.java
index bb5a7520..c51890d8 100644
--- a/src/main/java/uk/gov/companieshouse/web/pps/PPSWebApplication.java
+++ b/src/main/java/uk/gov/companieshouse/web/pps/PPSWebApplication.java
@@ -32,7 +32,9 @@ public void addInterceptors(InterceptorRegistry registry) {
                 .excludePathPatterns(
                         "/late-filing-penalty",
                         "/pay-penalty",
+                        "/error",
                         "/pay-penalty/ref-starts-with",
+                        "/pay-penalty/page-not-found",
                         "/pay-penalty/unscheduled-service-down"
                 );
     }
diff --git a/src/main/java/uk/gov/companieshouse/web/pps/config/PenaltyConfigurationProperties.java b/src/main/java/uk/gov/companieshouse/web/pps/config/PenaltyConfigurationProperties.java
index 1f54da94..ea01a87f 100644
--- a/src/main/java/uk/gov/companieshouse/web/pps/config/PenaltyConfigurationProperties.java
+++ b/src/main/java/uk/gov/companieshouse/web/pps/config/PenaltyConfigurationProperties.java
@@ -14,9 +14,6 @@ public class PenaltyConfigurationProperties {
     private List<PenaltyReference> allowedRefStartsWith = new ArrayList<>();
     private String refStartsWithPath;
     private String enterDetailsPath;
-    private String bankTransferPath;
-    private String bankTransferLateFilingDetailsPath;
-    private String bankTransferSanctionsPath;
     private String unscheduledServiceDownPath;
     private String signOutPath;
     private String surveyLink;
@@ -24,6 +21,7 @@ public class PenaltyConfigurationProperties {
     private String startPath;
     private String signedOutUrl;
     private String govUkPayPenaltyUrl;
+    private String pageNotFoundPath;
 
     public List<PenaltyReference> getAllowedRefStartsWith() {
         return allowedRefStartsWith;
@@ -50,30 +48,6 @@ public void setEnterDetailsPath(String enterDetailsPath) {
         this.enterDetailsPath = enterDetailsPath;
     }
 
-    public String getBankTransferPath() {
-        return bankTransferPath;
-    }
-
-    public void setBankTransferPath(String bankTransferPath) {
-        this.bankTransferPath = bankTransferPath;
-    }
-
-    public String getBankTransferLateFilingDetailsPath() {
-        return bankTransferLateFilingDetailsPath;
-    }
-
-    public void setBankTransferLateFilingDetailsPath(String bankTransferLateFilingDetailsPath) {
-        this.bankTransferLateFilingDetailsPath = bankTransferLateFilingDetailsPath;
-    }
-
-    public String getBankTransferSanctionsPath() {
-        return bankTransferSanctionsPath;
-    }
-
-    public void setBankTransferSanctionsPath(String bankTransferSanctionsPath) {
-        this.bankTransferSanctionsPath = bankTransferSanctionsPath;
-    }
-
     public String getUnscheduledServiceDownPath() {
         return unscheduledServiceDownPath;
     }
@@ -130,4 +104,11 @@ public void setGovUkPayPenaltyUrl(String govUkPayPenaltyUrl) {
         this.govUkPayPenaltyUrl = govUkPayPenaltyUrl;
     }
 
+    public String getPageNotFoundPath() {
+        return pageNotFoundPath;
+    }
+
+    public void setPageNotFoundPath(String pageNotFoundPath) {
+        this.pageNotFoundPath = pageNotFoundPath;
+    }
 }
diff --git a/src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java b/src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
new file mode 100644
index 00000000..07dc995d
--- /dev/null
+++ b/src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
@@ -0,0 +1,43 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+@RequestMapping(value = "/error")
+public class CustomErrorController implements ErrorController {
+    private static final int ERROR_CODE_NOT_FOUND = 404;
+    private static final Logger LOG = LoggerFactory.getLogger(PPSWebApplication.APPLICATION_NAME_SPACE);
+
+    private final PenaltyConfigurationProperties penaltyConfigurationProperties;
+
+    public CustomErrorController(PenaltyConfigurationProperties penaltyConfigurationProperties) {
+        this.penaltyConfigurationProperties = penaltyConfigurationProperties;
+    }
+
+    @GetMapping
+    public String handleError(HttpServletRequest request) {
+        int httpErrorCode = getErrorCode(request);
+        LOG.errorRequest(request, request.getRequestURI());
+
+        if (httpErrorCode == ERROR_CODE_NOT_FOUND) {
+            return REDIRECT_URL_PREFIX + penaltyConfigurationProperties.getPageNotFoundPath();
+        } else {
+            return REDIRECT_URL_PREFIX + penaltyConfigurationProperties.getUnscheduledServiceDownPath();
+        }
+    }
+
+    private int getErrorCode(HttpServletRequest request) {
+        return (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
+    }
+}
diff --git a/src/main/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundController.java b/src/main/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundController.java
new file mode 100644
index 00000000..9786c648
--- /dev/null
+++ b/src/main/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundController.java
@@ -0,0 +1,37 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+import uk.gov.companieshouse.web.pps.controller.BaseController;
+import uk.gov.companieshouse.web.pps.service.navigation.NavigatorService;
+import uk.gov.companieshouse.web.pps.session.SessionService;
+
+@Controller
+@RequestMapping("/pay-penalty/page-not-found")
+public class PageNotFoundController extends BaseController {
+
+    static final String PAGE_NOT_FOUND_TEMPLATE_NAME = "pps/pageNotFound";
+
+    public PageNotFoundController(
+            NavigatorService navigatorService,
+            SessionService sessionService,
+            PenaltyConfigurationProperties penaltyConfigurationProperties) {
+        super(navigatorService, sessionService, penaltyConfigurationProperties);
+    }
+
+    @Override
+    protected String getTemplateName() {
+        return PAGE_NOT_FOUND_TEMPLATE_NAME;
+    }
+
+    @GetMapping
+    public String getPageNotFound(Model model) {
+        addBaseAttributesToModel(model,
+                penaltyConfigurationProperties.getStartPath(),
+                penaltyConfigurationProperties.getSignOutPath());
+        return getTemplateName();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/uk/gov/companieshouse/web/pps/security/WebSecurity.java b/src/main/java/uk/gov/companieshouse/web/pps/security/WebSecurity.java
index ef9172e2..c7a738b1 100644
--- a/src/main/java/uk/gov/companieshouse/web/pps/security/WebSecurity.java
+++ b/src/main/java/uk/gov/companieshouse/web/pps/security/WebSecurity.java
@@ -61,6 +61,22 @@ public SecurityFilterChain scheduledServiceDownSecurityFilterChain(final HttpSec
 
     @Bean
     @Order(6)
+    public SecurityFilterChain pageNotFoundSecurityFilterChain(final HttpSecurity http) throws Exception {
+        return configureWebCsrfMitigations(
+                http.securityMatcher("/pay-penalty/page-not-found")
+        ).build();
+    }
+
+    @Bean
+    @Order(7)
+    public SecurityFilterChain errorPageSecurityFilterChain(final HttpSecurity http) throws Exception {
+        return configureWebCsrfMitigations(
+                http.securityMatcher("/error")
+        ).build();
+    }
+
+    @Bean
+    @Order(8)
     public SecurityFilterChain ppsWebSecurityFilterConfig(HttpSecurity http) throws Exception {
         return configureWebCsrfMitigations(
                 http.securityMatcher("/pay-penalty/**")
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 0b3ae844..d5126e02 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -34,6 +34,7 @@ penalty.unscheduled-service-down-path=/pay-penalty/unscheduled-service-down
 penalty.start-path=${penalty.gov-uk-pay-penalty-url}
 penalty.signed-out-url=${penalty.gov-uk-pay-penalty-url}
 penalty.gov-uk-pay-penalty-url=${GOV_UK_PAY_PENALTY_URL:https://www.gov.uk/pay-penalty-companies-house}
+penalty.page-not-found-path=/pay-penalty/page-not-found
 
 penalty.bank-transfer-late-filing.account-name=${CH_BANK_ACC_NAME:Companies House}
 penalty.bank-transfer-late-filing.sort-code=${CH_BANK_SORT_CODE:}
diff --git a/src/main/resources/locales/messages.properties b/src/main/resources/locales/messages.properties
index 58a0441b..9e31191f 100644
--- a/src/main/resources/locales/messages.properties
+++ b/src/main/resources/locales/messages.properties
@@ -14,3 +14,9 @@ viewPenalties.reason-for-penalty-label=Reason for penalty
 viewPenalties.company-number-label=Company number
 viewPenalties.issued-to-label=Issued to
 viewPenalties.amount-label=Amount
+
+error.header.page.title=Page not found - Pay a penalty to Companies House - GOV.UK
+error.page.title=This page cannot be found
+error.page.content.title=Check that you have entered the correct web address or try using the search.
+error.page.content.main=If you still can't find what you're looking for email
+error.page.content.email=enquiries@companieshouse.gov.uk
\ No newline at end of file
diff --git a/src/main/resources/templates/pps/pageNotFound.html b/src/main/resources/templates/pps/pageNotFound.html
new file mode 100644
index 00000000..210f814d
--- /dev/null
+++ b/src/main/resources/templates/pps/pageNotFound.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/baseLayout}">
+
+<head>
+    <title th:text="#{error.header.page.title}">Page not found - Pay a penalty to Companies House - GOV.UK</title>
+</head>
+<body>
+<div layout:fragment="content">
+
+    <div class="text">
+        <h1 class="govuk-heading-l" th:text="#{error.page.title}">This page cannot be found</h1>
+    </div>
+
+    <p class="govuk-body" th:text="#{error.page.content.title}">Check that you have entered the correct web address or try using the search.</p>
+    <p class="govuk-body">
+        <span th:text="#{error.page.content.main}">If you still can't find what you're looking for email</span>
+    </p>
+    <p class="govuk-body">
+        <a class="govuk-link" data-event-id="email-us"
+           th:href="@{{enquiries}(enquiries=${@environment.getProperty('enquiries')})}"
+           data-th-text="#{error.page.content.email}"></a>
+    </p>
+</div>
+</body>
+</html>
diff --git a/src/test/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorControllerTest.java b/src/test/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorControllerTest.java
new file mode 100644
index 00000000..09d12f66
--- /dev/null
+++ b/src/test/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorControllerTest.java
@@ -0,0 +1,54 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+@ExtendWith(MockitoExtension.class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class CustomErrorControllerTest {
+
+    @Mock
+    private PenaltyConfigurationProperties mockPenaltyConfigurationProperties;
+
+    @Mock
+    private HttpServletRequest httpServletRequest;
+
+    private static final String REDIRECT_PATH = "redirect:";
+    private static final String PAGE_NOT_FOUND_PATH = "/pay-penalty/page-not-found";
+    private static final String UNSCHEDULE_SERVICE_DOWN_PATH = "/pay-penalty/unscheduled-service-down";
+
+    private static final int ERROR_CODE_NOT_FOUND = 404;
+    private static final int ERROR_CODE_SERVICE_UNAVAILABLE = 503;
+
+    @Test
+    @DisplayName("Test Error - 404 Not Found")
+    void getNotFoundError() {
+        CustomErrorController controller = new CustomErrorController(mockPenaltyConfigurationProperties);
+
+        when(mockPenaltyConfigurationProperties.getPageNotFoundPath()).thenReturn(PAGE_NOT_FOUND_PATH);
+        when(httpServletRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).thenReturn(ERROR_CODE_NOT_FOUND);
+
+        assertEquals(REDIRECT_PATH + PAGE_NOT_FOUND_PATH, controller.handleError(httpServletRequest));
+    }
+
+    @Test
+    @DisplayName("Test Error - Other Unexpected error")
+    void getUnexpectedError() {
+        CustomErrorController controller = new CustomErrorController(mockPenaltyConfigurationProperties);
+
+        when(mockPenaltyConfigurationProperties.getUnscheduledServiceDownPath()).thenReturn(UNSCHEDULE_SERVICE_DOWN_PATH);
+        when(httpServletRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).thenReturn(ERROR_CODE_SERVICE_UNAVAILABLE);
+
+        assertEquals(REDIRECT_PATH + UNSCHEDULE_SERVICE_DOWN_PATH, controller.handleError(httpServletRequest));
+    }
+}
diff --git a/src/test/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundControllerTest.java b/src/test/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundControllerTest.java
new file mode 100644
index 00000000..95725e61
--- /dev/null
+++ b/src/test/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundControllerTest.java
@@ -0,0 +1,66 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+import static uk.gov.companieshouse.web.pps.controller.BaseController.USER_BAR_ATTR;
+import static uk.gov.companieshouse.web.pps.controller.pps.PageNotFoundController.PAGE_NOT_FOUND_TEMPLATE_NAME;
+
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+import uk.gov.companieshouse.web.pps.service.navigation.NavigatorService;
+import uk.gov.companieshouse.web.pps.session.SessionService;
+
+@ExtendWith(MockitoExtension.class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class PageNotFoundControllerTest {
+
+    private MockMvc mockMvc;
+
+    private static final String PAGE_NOT_FOUND_PATH = "/pay-penalty/page-not-found";
+
+    @Mock
+    private NavigatorService mockNavigatorService;
+
+    @Mock
+    private SessionService mockSessionService;
+
+    @Mock
+    private PenaltyConfigurationProperties mockPenaltyConfigurationProperties;
+
+    @BeforeEach
+    void setup() {
+        PageNotFoundController controller = new PageNotFoundController(
+                mockNavigatorService,
+                mockSessionService,
+                mockPenaltyConfigurationProperties
+        );
+        this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    @DisplayName("Get Page Not Found - success path")
+    void getRequestSuccess() throws Exception {
+
+        when(mockSessionService.getSessionDataFromContext()).thenReturn(
+                Map.of("signin_info",
+                        Map.of("user_profile",
+                                Map.of("email", "test@example.com"))));
+
+        this.mockMvc.perform(get(PAGE_NOT_FOUND_PATH))
+                .andExpect(status().isOk())
+                .andExpect(view().name(PAGE_NOT_FOUND_TEMPLATE_NAME))
+                .andExpect(model().attributeExists(USER_BAR_ATTR));
+    }
+}
diff --git a/src/test/java/uk/gov/companieshouse/web/pps/security/WebSecurityTests.java b/src/test/java/uk/gov/companieshouse/web/pps/security/WebSecurityTests.java
index e8181b00..7f99273b 100644
--- a/src/test/java/uk/gov/companieshouse/web/pps/security/WebSecurityTests.java
+++ b/src/test/java/uk/gov/companieshouse/web/pps/security/WebSecurityTests.java
@@ -70,4 +70,17 @@ void scheduledServiceDownSecurityFilterChainTest() throws Exception {
         assertEquals(webSecurity.scheduledServiceDownSecurityFilterChain(httpSecurity), httpSecurity.build());
     }
 
+    @Test
+    @DisplayName(" apply security filter to /pay-penalty/page-not-found")
+    void pageNotFoundSecurityFilterChainTest() throws Exception {
+        when(httpSecurity.securityMatcher("/pay-penalty/page-not-found")).thenReturn(httpSecurity);
+        assertEquals(webSecurity.pageNotFoundSecurityFilterChain(httpSecurity), httpSecurity.build());
+    }
+
+    @Test
+    @DisplayName(" apply security filter to /error")
+    void errorPageSecurityFilterChainTest() throws Exception {
+        when(httpSecurity.securityMatcher("/error")).thenReturn(httpSecurity);
+        assertEquals(webSecurity.errorPageSecurityFilterChain(httpSecurity), httpSecurity.build());
+    }
 }


Reviews:
user: login: dgordon-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:03:23Z
user: login: acallaghan-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:30:39Z
user: login: acallaghan-ch
body: Add test
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:30:51Z
user: login: acallaghan-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T11:36:33Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T12:43:41Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T12:47:54Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T12:58:58Z
user: login: acallaghan-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:12:42Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:36:10Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:50:24Z
user: login: acallaghan-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:51:37Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:08:33Z
user: login: klai-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:37:35Z
user: login: klai-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:37:46Z
user: login: klai-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:38:33Z
user: login: klai-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:38:48Z
user: login: rbull-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:42:41Z
user: login: dgordon-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:13:58Z

Review comments:
diff_hunk: @@ -0,0 +1,40 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+public class CustomErrorController implements ErrorController {
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
user: login: acallaghan-ch
body: Can you add a unit test for this class? Just to test the different return statuses.
created_at: 2025-05-09T15:30:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,37 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+import uk.gov.companieshouse.web.pps.controller.BaseController;
+import uk.gov.companieshouse.web.pps.service.navigation.NavigatorService;
+import uk.gov.companieshouse.web.pps.session.SessionService;
+
+@Controller
+@RequestMapping("/pay-penalty/page-not-found")
+public class PageNotFoundController extends BaseController {
+
+    static final String PAGE_NOT_FOUND_TEMPLATE_NAME = "pps/pageNotFound";
+
+    public PageNotFoundController(
+            NavigatorService navigatorService,
+            SessionService sessionService,
+            PenaltyConfigurationProperties penaltyConfigurationProperties) {
+        super(navigatorService, sessionService, penaltyConfigurationProperties);
+    }
+
+    @Override
+    protected String getTemplateName() {
+        return PAGE_NOT_FOUND_TEMPLATE_NAME;
+    }
+
+    @GetMapping
+    public String getUnscheduledServiceDown(Model model) {
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundController.java
user: login: rbull-ch
body: Should this method be renamed to be `getPageNotFound`?
created_at: 2025-05-12T12:43:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+public class CustomErrorController implements ErrorController {
+    private static final int ERROR_CODE_NOT_FOUND = 404;
+    private static final Logger LOG = LoggerFactory.getLogger(PPSWebApplication.APPLICATION_NAME_SPACE);
+
+    private final PenaltyConfigurationProperties penaltyConfigurationProperties;
+
+    public CustomErrorController(PenaltyConfigurationProperties penaltyConfigurationProperties) {
+        this.penaltyConfigurationProperties = penaltyConfigurationProperties;
+    }
+
+    @RequestMapping(value = "/error")
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
user: login: rbull-ch
body: Should this annotation be above the class line 16 to be consistent with the rest of the -web controllers?
Then would this method be a `GetMapping` like the other pages.
Looks like you'll need to at least specify the methods here - see https://sonarqube.companieshouse.gov.uk/security_hotspots?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285
created_at: 2025-05-12T12:47:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+public class CustomErrorController implements ErrorController {
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
user: login: rbull-ch
body: There is a [GlobalExceptionHandler](https://github.com/search?q=repo%3Acompanieshouse%2Fpenalty-payment-web%20GlobalExceptionHandler&type=code) - I think we should try to use that instead to handle the 404 Not Found requests?  Hopefully that would also help with some of the failing PR checks also
created_at: 2025-05-12T12:58:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+public class CustomErrorController implements ErrorController {
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
user: login: acallaghan-ch
body: @rbull-ch Do you mean throw an exception from in here or not have the CustomErrorHandler? The issue we were having is that the GlobalExceptionHandler wasn't being hit as Spring was already dealing with the error. The Customer ErrorHandler came about in trying to deal with issue (https://www.baeldung.com/custom-error-page-spring-mvc). 
created_at: 2025-05-12T13:12:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+public class CustomErrorController implements ErrorController {
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
user: login: rbull-ch
body: I thought it would be something like the first answer here - https://stackoverflow.com/questions/54116245/404-exception-not-handled-in-spring-controlleradvice

If we can address the failing PR checks, then I guess this approach here is fine also
created_at: 2025-05-12T13:36:10Z
author_association: CONTRIBUTOR
path: src/main/java/uk/gov/companieshouse/web/pps/config/PenaltyConfigurationProperties.java
user: login: rbull-ch
body: Please would you be able to remove the redundant bank-transfer properties here also - [bankTransfer](https://github.com/search?q=repo%3Acompanieshouse%2Fpenalty-payment-web%20bankTransfer&type=code) paths - they should have been removed previously

 * bankTransferPath
 * bankTransferLateFilingDetailsPath
 * bankTransferSanctionsPath
created_at: 2025-05-12T13:50:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+public class CustomErrorController implements ErrorController {
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
user: login: acallaghan-ch
body: Cool, I wasn't aware of the other one, and we just came across the Custom Error Handling first. Good to know that there is another way. 
created_at: 2025-05-12T13:51:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+import static uk.gov.companieshouse.web.pps.controller.BaseController.USER_BAR_ATTR;
+import static uk.gov.companieshouse.web.pps.controller.pps.PageNotFoundController.PAGE_NOT_FOUND_TEMPLATE_NAME;
+
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+import uk.gov.companieshouse.web.pps.service.navigation.NavigatorService;
+import uk.gov.companieshouse.web.pps.session.SessionService;
+
+@ExtendWith(MockitoExtension.class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class PageNotFoundControllerTest {
+
+    private MockMvc mockMvc;
+
+    private static final String PAGE_NOT_FOUND_PATH = "/pay-penalty/page-not-found";
+
+    @Mock
+    private NavigatorService mockNavigatorService;
+
+    @Mock
+    private SessionService mockSessionService;
+
+    @Mock
+    private PenaltyConfigurationProperties mockPenaltyConfigurationProperties;
+
+    @BeforeEach
+    void setup() {
+        PageNotFoundController controller = new PageNotFoundController(
+                mockNavigatorService,
+                mockSessionService,
+                mockPenaltyConfigurationProperties
+        );
+        this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    @DisplayName("Get Page Not Found - success path")
+    void getRequestSuccess() throws Exception {
+
+        when(mockSessionService.getSessionDataFromContext()).thenReturn(
+                Map.of("signin_info",
+                        Map.of("user_profile",
+                                Map.of("email", "test@gmail.com"))));
path: src/test/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundControllerTest.java
user: login: rbull-ch
body: I know this is just a unit test, but think we should get in a habit of using `@example.com` instead
created_at: 2025-05-12T14:08:33Z
author_association: CONTRIBUTOR
path: src/main/java/uk/gov/companieshouse/web/pps/config/PenaltyConfigurationProperties.java
user: login: klai-ch
body: Remove the unused path
created_at: 2025-05-12T14:37:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+import static uk.gov.companieshouse.web.pps.controller.BaseController.USER_BAR_ATTR;
+import static uk.gov.companieshouse.web.pps.controller.pps.PageNotFoundController.PAGE_NOT_FOUND_TEMPLATE_NAME;
+
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+import uk.gov.companieshouse.web.pps.service.navigation.NavigatorService;
+import uk.gov.companieshouse.web.pps.session.SessionService;
+
+@ExtendWith(MockitoExtension.class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class PageNotFoundControllerTest {
+
+    private MockMvc mockMvc;
+
+    private static final String PAGE_NOT_FOUND_PATH = "/pay-penalty/page-not-found";
+
+    @Mock
+    private NavigatorService mockNavigatorService;
+
+    @Mock
+    private SessionService mockSessionService;
+
+    @Mock
+    private PenaltyConfigurationProperties mockPenaltyConfigurationProperties;
+
+    @BeforeEach
+    void setup() {
+        PageNotFoundController controller = new PageNotFoundController(
+                mockNavigatorService,
+                mockSessionService,
+                mockPenaltyConfigurationProperties
+        );
+        this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    @DisplayName("Get Page Not Found - success path")
+    void getRequestSuccess() throws Exception {
+
+        when(mockSessionService.getSessionDataFromContext()).thenReturn(
+                Map.of("signin_info",
+                        Map.of("user_profile",
+                                Map.of("email", "test@gmail.com"))));
path: src/test/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundControllerTest.java
user: login: klai-ch
body: Updated the link
created_at: 2025-05-12T14:37:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import jakarta.servlet.RequestDispatcher;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.logging.Logger;
+import uk.gov.companieshouse.logging.LoggerFactory;
+import uk.gov.companieshouse.web.pps.PPSWebApplication;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+
+import static org.springframework.web.servlet.view.UrlBasedViewResolver.REDIRECT_URL_PREFIX;
+
+@Controller
+public class CustomErrorController implements ErrorController {
+    private static final int ERROR_CODE_NOT_FOUND = 404;
+    private static final Logger LOG = LoggerFactory.getLogger(PPSWebApplication.APPLICATION_NAME_SPACE);
+
+    private final PenaltyConfigurationProperties penaltyConfigurationProperties;
+
+    public CustomErrorController(PenaltyConfigurationProperties penaltyConfigurationProperties) {
+        this.penaltyConfigurationProperties = penaltyConfigurationProperties;
+    }
+
+    @RequestMapping(value = "/error")
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/CustomErrorController.java
user: login: klai-ch
body: Moved the RequestMapping to align with other pages. Also added GetMapping for that
created_at: 2025-05-12T14:38:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,37 @@
+package uk.gov.companieshouse.web.pps.controller.pps;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.companieshouse.web.pps.config.PenaltyConfigurationProperties;
+import uk.gov.companieshouse.web.pps.controller.BaseController;
+import uk.gov.companieshouse.web.pps.service.navigation.NavigatorService;
+import uk.gov.companieshouse.web.pps.session.SessionService;
+
+@Controller
+@RequestMapping("/pay-penalty/page-not-found")
+public class PageNotFoundController extends BaseController {
+
+    static final String PAGE_NOT_FOUND_TEMPLATE_NAME = "pps/pageNotFound";
+
+    public PageNotFoundController(
+            NavigatorService navigatorService,
+            SessionService sessionService,
+            PenaltyConfigurationProperties penaltyConfigurationProperties) {
+        super(navigatorService, sessionService, penaltyConfigurationProperties);
+    }
+
+    @Override
+    protected String getTemplateName() {
+        return PAGE_NOT_FOUND_TEMPLATE_NAME;
+    }
+
+    @GetMapping
+    public String getUnscheduledServiceDown(Model model) {
path: src/main/java/uk/gov/companieshouse/web/pps/controller/pps/PageNotFoundController.java
user: login: klai-ch
body: Method name has been changed.
created_at: 2025-05-12T14:38:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: ch-code-analysis
created_at: 2025-05-09T13:21:53Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-09T15:30:54Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-09T16:12:16Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-12T12:49:13Z
body: CI: No security warnings found
author_association: NONE
user: login: chsonarqubeprchecks[bot]
created_at: 2025-05-12T14:36:02Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarqube.companieshouse.gov.uk/dashboard?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&resolved=false) [0 New issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-web&fixedInPullRequest=285) [0 Fixed issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-web&fixedInPullRequest=285)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&issueStatuses=ACCEPTED) [0 Accepted issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/security_hotspots?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285) [0 Security Hotspots](https://sonarqube.companieshouse.gov.uk/security_hotspots?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&metric=new_coverage&view=list) [89.3% Coverage on New Code](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://sonarqube.companieshouse.gov.uk/dashboard?id=uk.gov.companieshouse%3Apenalty-payment-web&pullRequest=285)


author_association: NONE
user: login: ch-code-analysis
created_at: 2025-05-12T14:42:13Z
body: CI: No security warnings found
author_association: NONE