Repository: companieshouse/limited-partnerships-web
Pull request #246: Lp 596 intro sign out page functionality
Author: login: mattch1
Created: 2025-03-18T16:29:37Z | Merged: 2025-03-20T14:03:15Z

### JIRA link
https://companieshouse.atlassian.net/browse/LP-596


### Change description

New template for the sign out page
Added new functions in the global controller to handle sign out.
Routing to direct the user back to the page they were working on. 

### Work checklist

- [x] Tests added where applicable
- [ ] UI changes look good on mobile
- [ ] UI changes meet accessibility criteria

### Merge instructions

We are committed to keeping commit history clean, consistent and linear. To achieve this commit should be structured as follows:

```
<type>[optional scope]: <description>
```

and contain the following structural elements:

- fix: a commit that patches a bug in your codebase (this correlates with PATCH in semantic versioning),
- feat: a commit that introduces a new feature to the codebase (this correlates with MINOR in semantic versioning),
- BREAKING CHANGE: a commit that has a footer `BREAKING CHANGE:` introduces a breaking API change (correlating with MAJOR in semantic versioning). A BREAKING CHANGE can be part of commits of any type,
- types other than `fix:` and `feat:` are allowed, for example `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others,
- footers other than `BREAKING CHANGE: <description>` may be provided.


Diff:
diff --git a/locales/cy/translations.json b/locales/cy/translations.json
index d7e386b61..e8b922028 100644
--- a/locales/cy/translations.json
+++ b/locales/cy/translations.json
@@ -322,6 +322,13 @@
 
     "service" : "WELSH - Register a limited partnership",
 
+    "signOutPage": {
+        "title": "WELSH - Are you sure you want to sign out?",
+        "hint": "WELSH - We will save your application.",
+         "saved": "WELSH - How do I find my saved applications?",
+         "filingsInfo": "WELSH - Sign in to your Companies House account and select Your filings to access your saved applications."
+   },
+
     "startPage" : {
         "title" : "Partneriaethau Cyfyngedig yw hyn",
         "errorStartAddressMissing" : "Rhowch gyfeiriad"
@@ -880,5 +887,10 @@
         "october": "WELSH - October",
         "november": "WELSH - November",
         "december": "WELSH - December"
+    },
+
+    "yesAndNo": {
+        "yes": "WELSH - Yes",
+        "no": "WELSH - No"
     }
 }
diff --git a/locales/en/translations.json b/locales/en/translations.json
index 901acce70..fcb41fc81 100644
--- a/locales/en/translations.json
+++ b/locales/en/translations.json
@@ -322,6 +322,13 @@
 
     "service" : "Register a limited partnership",
 
+    "signOutPage": {
+         "title": "Are you sure you want to sign out?",
+         "hint": "We will save your application.",
+         "saved": "How do I find my saved applications?",
+         "filingsInfo": "Sign in to your Companies House account and select Your filings to access your saved applications."
+    },
+
     "startPage" : {
         "title" : "This is Limited Partnerships",
         "errorStartAddressMissing" : "Please enter an address"
@@ -880,5 +887,10 @@
         "october": "October",
         "november": "November",
         "december": "December"
+    },
+
+    "yesAndNo": {
+        "yes": "Yes",
+        "no": "No"
     }
 }
diff --git a/src/config/constants.ts b/src/config/constants.ts
index d9d6edd65..9401e3a2a 100644
--- a/src/config/constants.ts
+++ b/src/config/constants.ts
@@ -67,6 +67,7 @@ export const BASE_URL = "/limited-partnerships";
 export const TRANSITION_BASE_URL = "/limited-partnerships/transition";
 export const BASE_WITH_IDS_URL = `${BASE_URL}/transaction/:${TRANSACTION_ID}/submission/:${SUBMISSION_ID}`;
 export const GENERAL_PARTNER_URL = BASE_WITH_IDS_URL + `/general-partner`;
+export const ACCOUNTS_SIGN_OUT_URL = `${ACCOUNT_URL}/signout`;
 export const GENERAL_PARTNER_WITH_ID_URL =
   GENERAL_PARTNER_URL + `/:${GENERAL_PARTNER_ID}`;
 
diff --git a/src/presentation/controller/AbstractController.ts b/src/presentation/controller/AbstractController.ts
index 3a0bd7abc..8bd0f7148 100644
--- a/src/presentation/controller/AbstractController.ts
+++ b/src/presentation/controller/AbstractController.ts
@@ -17,6 +17,7 @@ import {
   NAME_URL
 } from "./registration/url";
 import UIErrors from "../../domain/entities/UIErrors";
+import { START_URL } from "./global/Routing";
 
 abstract class AbstractController {
   protected getRouting(routing: PagesRouting, pageType: PageType, request: Request) {
@@ -213,6 +214,16 @@ abstract class AbstractController {
 
     return { transactionId, submissionId, generalPartnerId };
   }
+
+  protected getPreviousPageUrl(request: Request) {
+    const headers = request.rawHeaders;
+    const previousPageUrl = headers.filter(item => item.includes(BASE_URL))[0];
+    if (previousPageUrl) {
+      const startingIndexOfRelativePath = previousPageUrl.indexOf(BASE_URL);
+      return previousPageUrl.substring(startingIndexOfRelativePath);
+    }
+    return START_URL;
+  }
 }
 
 export default AbstractController;
diff --git a/src/presentation/controller/global/Controller.ts b/src/presentation/controller/global/Controller.ts
index 8c3acb754..4bf5cbd42 100644
--- a/src/presentation/controller/global/Controller.ts
+++ b/src/presentation/controller/global/Controller.ts
@@ -1,7 +1,8 @@
 import { NextFunction, Request, RequestHandler, Response } from "express";
 
-import registrationsRouting from "./Routing";
+import globalsRouting from "./Routing";
 import AbstractController from "../AbstractController";
+import { ACCOUNTS_SIGN_OUT_URL, BASE_URL, APPLICATION_CACHE_KEY, cookieOptions } from "../../../config/constants";
 
 class GlobalController extends AbstractController {
   constructor() {
@@ -17,7 +18,7 @@ class GlobalController extends AbstractController {
         const pageType = super.pageType(request.path);
 
         const pageRouting = super.getRouting(
-          registrationsRouting,
+          globalsRouting,
           pageType,
           request
         );
@@ -31,6 +32,42 @@ class GlobalController extends AbstractController {
     };
   }
 
+  getSignOut(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        const previousPageUrl = this.getPreviousPageUrl(request);
+        const pageType = super.pageType(request.path);
+        const pageRouting = super.getRouting(
+          globalsRouting,
+          pageType,
+          request
+        );
+        pageRouting.previousUrl = previousPageUrl;
+        response.render(super.templateName(pageRouting.currentUrl), {
+          props: pageRouting
+        });
+      } catch (error) {
+        next(error);
+      }
+    };
+  }
+
+  signOutChoice(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        if (request.body["sign_out"] === 'yes') {
+          this.clearCache(response);
+          return response.redirect(ACCOUNTS_SIGN_OUT_URL);
+        }
+        const previousPage = request.body["previousPage"];
+
+        return redirectWithChecks(response, previousPage);
+      } catch (error) {
+        next(error);
+      }
+    };
+  }
+
   getHealthcheck(): RequestHandler {
     return (_request: Request, response: Response, next: NextFunction) => {
       try {
@@ -40,6 +77,16 @@ class GlobalController extends AbstractController {
       }
     };
   }
+
+  clearCache(response: Response) {
+    return response.clearCookie(APPLICATION_CACHE_KEY, cookieOptions);
+  }
 }
 
+export const redirectWithChecks = (response: Response, url: string): void => {
+  if (url.startsWith(BASE_URL)) {
+    return response.redirect(url);
+  }
+};
+
 export default GlobalController;
diff --git a/src/presentation/controller/global/PageType.ts b/src/presentation/controller/global/PageType.ts
index 4ebad758d..7cf1fa685 100644
--- a/src/presentation/controller/global/PageType.ts
+++ b/src/presentation/controller/global/PageType.ts
@@ -1,6 +1,7 @@
 enum GlobalPageType {
   healthcheck = "healthcheck",
   start = "start",
+  signOut = "sign-out",
 }
 
 export default GlobalPageType;
diff --git a/src/presentation/controller/global/Routing.ts b/src/presentation/controller/global/Routing.ts
index 6e4c8215f..71f2fd180 100644
--- a/src/presentation/controller/global/Routing.ts
+++ b/src/presentation/controller/global/Routing.ts
@@ -4,9 +4,11 @@ import { BASE_URL } from "../../../config/constants";
 
 export const HEALTHCHECK_URL = `${BASE_URL}/healthcheck`;
 export const START_TEMPLATE = GlobalPageType.start;
+export const SIGN_OUT_TEMPLATE = GlobalPageType.signOut;
 
 const WHICH_TYPE_URL = `${BASE_URL}/which-type`;
 export const START_URL = `${BASE_URL}/${START_TEMPLATE}`;
+export const SIGN_OUT_URL = `${BASE_URL}/${SIGN_OUT_TEMPLATE}`;
 
 export const globalRoutingStart = {
   previousUrl: "/",
@@ -15,7 +17,14 @@ export const globalRoutingStart = {
   pageType: GlobalPageType.start,
 };
 
-const list = [globalRoutingStart];
+export const globalRoutingSignOut = {
+  previousUrl: "/",
+  currentUrl: SIGN_OUT_URL,
+  nextUrl: "/",
+  pageType: GlobalPageType.signOut,
+};
+
+const list = [globalRoutingStart, globalRoutingSignOut];
 
 export const globalsRouting: PagesRouting = new Map<
   GlobalPageType,
diff --git a/src/presentation/test/integration/error-500.test.ts b/src/presentation/test/integration/error-500.test.ts
index 739734e65..e6bda6b3c 100644
--- a/src/presentation/test/integration/error-500.test.ts
+++ b/src/presentation/test/integration/error-500.test.ts
@@ -1,6 +1,5 @@
 import request from "supertest";
 import { NextFunction, Request, Response } from "express";
-
 import app from "./app";
 import { HEALTHCHECK_URL } from "../../controller/global/Routing";
 
@@ -13,6 +12,16 @@ jest.mock("../../controller/global/Controller", () => {
           return next();
         };
       }),
+      getSignOut: jest.fn().mockImplementation(() => {
+        return (_req: Request, _res: Response, next: NextFunction) => {
+          return next();
+        };
+      }),
+      signOutChoice: jest.fn().mockImplementation(() => {
+        return (_req: Request, _res: Response, next: NextFunction) => {
+          return next();
+        };
+      }),
       getHealthcheck: jest.fn().mockImplementation(() => {
         return (_req: Request, _res: Response, next: NextFunction) => {
           next(new Error("Error 500"));
diff --git a/src/presentation/test/integration/error-csrf.test.ts b/src/presentation/test/integration/error-csrf.test.ts
index 6b494d06d..d0ee7394d 100644
--- a/src/presentation/test/integration/error-csrf.test.ts
+++ b/src/presentation/test/integration/error-csrf.test.ts
@@ -13,6 +13,16 @@ jest.mock("../../controller/global/Controller", () => {
           return next();
         };
       },
+      getSignOut: jest.fn().mockImplementation(() => {
+        return (_req: Request, _res: Response, next: NextFunction) => {
+          return next();
+        };
+      }),
+      signOutChoice: jest.fn().mockImplementation(() => {
+        return (_req: Request, _res: Response, next: NextFunction) => {
+          return next();
+        };
+      }),
       getHealthcheck: function () {
         return (_req: Request, _res: Response, next: NextFunction) => {
           next(new CsrfError("CSRF token mismatch"));
diff --git a/src/presentation/test/integration/global/sign-out.test.ts b/src/presentation/test/integration/global/sign-out.test.ts
new file mode 100644
index 000000000..87f28bfc9
--- /dev/null
+++ b/src/presentation/test/integration/global/sign-out.test.ts
@@ -0,0 +1,60 @@
+import request from "supertest";
+import enTranslationText from "../../../../../locales/en/translations.json";
+import cyTranslationText from "../../../../../locales/cy/translations.json";
+import app from "../app";
+import {
+  SIGN_OUT_URL
+} from "../../../controller/global/Routing";
+import { getUrl, setLocalesEnabled, testTranslations } from "../../utils";
+import { ACCOUNTS_SIGN_OUT_URL } from "../../../../config";
+import { EMAIL_TEMPLATE } from "presentation/controller/registration/template";
+
+describe("Sign out page", () => {
+
+  const URL = getUrl(SIGN_OUT_URL);
+
+  describe("GET sign out page", () => {
+
+    it("should load the sign out page with Welsh text", async () => {
+      setLocalesEnabled(true);
+
+      const res = await request(app).get(URL + "?lang=cy");
+      expect(res.status).toBe(200);
+      testTranslations(res.text, cyTranslationText.signOutPage);
+    });
+
+    it("should load the sign out page with English text", async () => {
+      setLocalesEnabled(true);
+
+      const res = await request(app).get(URL + "?lang=en");
+
+      expect(res.status).toBe(200);
+      testTranslations(res.text, enTranslationText.signOutPage);
+    });
+  });
+
+  describe("POST sign out page", () => {
+
+    it("should redirect to the CH search page when yes is selected", async () => {
+      const previousPage = "";
+      const res = await request(app).post(URL)
+        .send({
+          sign_out: "yes",
+          previousPage
+        });
+      expect(res.status).toEqual(302);
+      expect(res.header.location).toEqual(ACCOUNTS_SIGN_OUT_URL);
+    });
+
+    it("should redirect to the specified previous page when no is selected", async () => {
+      const previousPage = `/limited-partnerships/transaction/abc-123/submission/456-xyz/${EMAIL_TEMPLATE}`;
+      const res = await request(app).post(URL)
+        .send({
+          sign_out: "no",
+          previousPage
+        });
+      expect(res.status).toEqual(302);
+      expect(res.header.location).toEqual(previousPage);
+    });
+  });
+});
diff --git a/src/routes/global.ts b/src/routes/global.ts
index 1e01d9599..0d68d26e1 100644
--- a/src/routes/global.ts
+++ b/src/routes/global.ts
@@ -5,6 +5,7 @@ import { authentication } from "../middlewares";
 import {
   HEALTHCHECK_URL,
   START_URL,
+  SIGN_OUT_URL,
 } from "../presentation/controller/global/Routing";
 
 export const globalEndpoints = (
@@ -18,6 +19,18 @@ export const globalEndpoints = (
     authentication,
     dependencies.globalController.getPageRouting()
   );
+
+  router.get(
+    SIGN_OUT_URL,
+    authentication,
+    dependencies.globalController.getSignOut()
+  );
+
+  router.post(
+    SIGN_OUT_URL,
+    authentication,
+    dependencies.globalController.signOutChoice()
+  );
 };
 
 export default globalEndpoints;
diff --git a/src/views/includes/sign-out-user-banner.njk b/src/views/includes/sign-out-user-banner.njk
index 574f5f7a0..b6181e544 100644
--- a/src/views/includes/sign-out-user-banner.njk
+++ b/src/views/includes/sign-out-user-banner.njk
@@ -1,11 +1,12 @@
 {% if userEmail %}
 
-  {% set signOutPath = 'sign-out?page=' + templateName %}
+  {% set signOutPath = '/limited-partnerships/sign-out' %}
+
 
   <ul id="navigation" class="sign-out-user-banner">
     <li class="content-email">{{ userEmail }}</li>
     <li>
-      <a class="govuk-link" href="{{ signOutBaseUrl }}{{ signOutPath }}" data-event-id="signout">Sign out</a>
+      <a class="govuk-link" href="{{ signOutPath }}" data-event-id="signout">Sign out</a>
     </li>
   </ul>
 
diff --git a/src/views/sign-out.njk b/src/views/sign-out.njk
new file mode 100644
index 000000000..cab2c65b6
--- /dev/null
+++ b/src/views/sign-out.njk
@@ -0,0 +1,67 @@
+{% extends "layout.njk" %}
+
+{% set pageTitle = i18n.signOutPage.title %}
+
+{% block content %}
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-two-thirds">
+
+      <form class="form" method="post">
+        <input type="hidden" name="pageType" value={{ props.pageType }}>
+
+        {% include "includes/csrf_token.njk" %}
+
+        {{ govukRadios({          
+          classes: "govuk-radios--inline",
+          idPrefix: "sign_out",
+          name: "sign_out",
+          fieldset: {
+          legend: {
+            text: i18n.signOutPage.title,
+            isPageHeading: true,
+            classes: "govuk-fieldset__legend--l"
+          }
+        },
+        hint: {
+          text: i18n.signOutPage.hint
+        },
+        items: [
+          {
+            value: "yes",
+            text: i18n.yesAndNo.yes,
+            attributes: {
+              required: true
+            }
+          },
+          {
+            value: "no",
+            text: i18n.yesAndNo.no,
+            attributes: {
+              required: true
+            }
+          }
+        ]
+        }) }}
+
+        <details class="govuk-details" data-module="govuk-details" data-event-id="find-saved-information-details">
+          <summary class="govuk-details__summary">
+            <span class="govuk-details__summary-text">
+              {{ i18n.signOutPage.saved }}
+            </span>
+          </summary>
+          <div class="govuk-details__text">
+            {{ i18n.signOutPage.filingsInfo }}
+          </div>  
+        </details>
+
+        {{ govukInput({
+          value: props.previousUrl,
+          name: "previousPage",
+          classes: "govuk-visually-hidden"
+        }) }}
+
+        {% include "includes/continue-button.njk" %}
+      </form>
+    </div>
+  </div>
+{% endblock %}
\ No newline at end of file


Reviews:
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T11:21:38Z
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T11:25:01Z
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T11:29:18Z
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T11:29:58Z
user: login: mattch1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T12:21:14Z
user: login: mattch1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T12:21:30Z
user: login: mattch1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T12:21:32Z
user: login: markpit
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:34:09Z
user: login: markpit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:36:46Z
user: login: mattch1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T14:17:37Z
user: login: mattch1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T12:19:47Z
user: login: markpit
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:43:42Z

Review comments:
diff_hunk: @@ -31,6 +32,41 @@ class GlobalController extends AbstractController {
     };
   }
 
+  getSignOut(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        const previousPageUrl = this.getPreviousPageUrl(request);
+        const pageType = super.pageType(request.path);
+        const pageRouting = super.getRouting(
+          registrationsRouting,
path: src/presentation/controller/global/Controller.ts
user: login: markpit
body: i think the sign-out page should live in the global routing rather than registrationsRouting as it will be applicable to all journeys (registration, transition and others)
created_at: 2025-03-19T11:21:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +32,41 @@ class GlobalController extends AbstractController {
     };
   }
 
+  getSignOut(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        const previousPageUrl = this.getPreviousPageUrl(request);
+        const pageType = super.pageType(request.path);
+        const pageRouting = super.getRouting(
+          registrationsRouting,
path: src/presentation/controller/global/Controller.ts
user: login: markpit
body: actually i can see it in the global routing file so should use global routing instead of registrationRouting in the call to super.getRouting
created_at: 2025-03-19T11:25:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+{% extends "layout.njk" %}
+
+{% set pageTitle = i18n.signOutPage.title %}
+
+{% block content %}
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-two-thirds">
+
+      <form class="form" method="post">
+        <input type="hidden" name="pageType" value={{ props.pageType }}>
+
+        {% include "includes/csrf_token.njk" %}
+
+        {{ govukInput({
+          value: props.previousUrl,
+          name: "previousPage",
+          classes: "govuk-visually-hidden"
+        }) }}
+
+        {{ govukRadios({          
+          classes: "govuk-radios--inline",
+          idPrefix: "sign_out",
+          name: "sign_out",
+          fieldset: {
+          legend: {
+            text: i18n.signOutPage.title,
+            isPageHeading: true,
+            classes: "govuk-fieldset__legend--l"
+          }
+        },
+        hint: {
+          text: i18n.signOutPage.hint
+        },
+        items: [
+          {
+            value: "yes",
+            text: "Yes",
path: src/views/sign-out.njk
user: login: markpit
body: i think the "yes" and "no" text will need translating too
created_at: 2025-03-19T11:29:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+{% extends "layout.njk" %}
+
+{% set pageTitle = i18n.signOutPage.title %}
+
+{% block content %}
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-two-thirds">
+
+      <form class="form" method="post">
+        <input type="hidden" name="pageType" value={{ props.pageType }}>
+
+        {% include "includes/csrf_token.njk" %}
+
+        {{ govukInput({
+          value: props.previousUrl,
+          name: "previousPage",
+          classes: "govuk-visually-hidden"
+        }) }}
+
+        {{ govukRadios({          
+          classes: "govuk-radios--inline",
+          idPrefix: "sign_out",
+          name: "sign_out",
+          fieldset: {
+          legend: {
+            text: i18n.signOutPage.title,
+            isPageHeading: true,
+            classes: "govuk-fieldset__legend--l"
+          }
+        },
+        hint: {
+          text: i18n.signOutPage.hint
+        },
+        items: [
+          {
+            value: "yes",
+            text: "Yes",
+            attributes: {
+              required: true
+            }
+          },
+          {
+            value: "no",
+            text: "No",
+            attributes: {
+              required: true
+            }
+          }
+        ]
+        }) }}
+
+        <details class="govuk-details" data-module="govuk-details" data-event-id="find-saved-information-details">
+          <summary class="govuk-details__summary">
+            <span class="govuk-details__summary-text">
+              How do I find my saved applications?
path: src/views/sign-out.njk
user: login: markpit
body: How do I find my saved applications? 
and
Sign in to your Companies House account and select Your filings to access your saved applications.

will also need translation
created_at: 2025-03-19T11:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +32,41 @@ class GlobalController extends AbstractController {
     };
   }
 
+  getSignOut(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        const previousPageUrl = this.getPreviousPageUrl(request);
+        const pageType = super.pageType(request.path);
+        const pageRouting = super.getRouting(
+          registrationsRouting,
path: src/presentation/controller/global/Controller.ts
user: login: mattch1
body: I have had to change this for the start page as well, registrations routing should probably not be in this controller at all.
created_at: 2025-03-19T12:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+{% extends "layout.njk" %}
+
+{% set pageTitle = i18n.signOutPage.title %}
+
+{% block content %}
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-two-thirds">
+
+      <form class="form" method="post">
+        <input type="hidden" name="pageType" value={{ props.pageType }}>
+
+        {% include "includes/csrf_token.njk" %}
+
+        {{ govukInput({
+          value: props.previousUrl,
+          name: "previousPage",
+          classes: "govuk-visually-hidden"
+        }) }}
+
+        {{ govukRadios({          
+          classes: "govuk-radios--inline",
+          idPrefix: "sign_out",
+          name: "sign_out",
+          fieldset: {
+          legend: {
+            text: i18n.signOutPage.title,
+            isPageHeading: true,
+            classes: "govuk-fieldset__legend--l"
+          }
+        },
+        hint: {
+          text: i18n.signOutPage.hint
+        },
+        items: [
+          {
+            value: "yes",
+            text: "Yes",
+            attributes: {
+              required: true
+            }
+          },
+          {
+            value: "no",
+            text: "No",
+            attributes: {
+              required: true
+            }
+          }
+        ]
+        }) }}
+
+        <details class="govuk-details" data-module="govuk-details" data-event-id="find-saved-information-details">
+          <summary class="govuk-details__summary">
+            <span class="govuk-details__summary-text">
+              How do I find my saved applications?
path: src/views/sign-out.njk
user: login: mattch1
body: Added translations.
created_at: 2025-03-19T12:21:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+{% extends "layout.njk" %}
+
+{% set pageTitle = i18n.signOutPage.title %}
+
+{% block content %}
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-two-thirds">
+
+      <form class="form" method="post">
+        <input type="hidden" name="pageType" value={{ props.pageType }}>
+
+        {% include "includes/csrf_token.njk" %}
+
+        {{ govukInput({
+          value: props.previousUrl,
+          name: "previousPage",
+          classes: "govuk-visually-hidden"
+        }) }}
+
+        {{ govukRadios({          
+          classes: "govuk-radios--inline",
+          idPrefix: "sign_out",
+          name: "sign_out",
+          fieldset: {
+          legend: {
+            text: i18n.signOutPage.title,
+            isPageHeading: true,
+            classes: "govuk-fieldset__legend--l"
+          }
+        },
+        hint: {
+          text: i18n.signOutPage.hint
+        },
+        items: [
+          {
+            value: "yes",
+            text: "Yes",
path: src/views/sign-out.njk
user: login: mattch1
body: Added translations.
created_at: 2025-03-19T12:21:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +32,41 @@ class GlobalController extends AbstractController {
     };
   }
 
+  getSignOut(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        const previousPageUrl = this.getPreviousPageUrl(request);
+        const pageType = super.pageType(request.path);
+        const pageRouting = super.getRouting(
+          globalsRouting,
+          pageType,
+          request
+        );
+        pageRouting.previousUrl = previousPageUrl;
+        response.render(super.templateName(pageRouting.currentUrl), {
+          props: pageRouting
+        });
+      } catch (error) {
+        next(error);
+      }
+    };
+  }
+
+  signOutChoice(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        if (request.body["sign_out"] === 'yes') {
+          return response.redirect(ACCOUNTS_SIGN_OUT_URL);
path: src/presentation/controller/global/Controller.ts
user: login: markpit
body: we also have to remove data from the cache when they sign out (it's in an note after the ACs on teh ticket)
created_at: 2025-03-19T13:36:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +32,41 @@ class GlobalController extends AbstractController {
     };
   }
 
+  getSignOut(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        const previousPageUrl = this.getPreviousPageUrl(request);
+        const pageType = super.pageType(request.path);
+        const pageRouting = super.getRouting(
+          globalsRouting,
+          pageType,
+          request
+        );
+        pageRouting.previousUrl = previousPageUrl;
+        response.render(super.templateName(pageRouting.currentUrl), {
+          props: pageRouting
+        });
+      } catch (error) {
+        next(error);
+      }
+    };
+  }
+
+  signOutChoice(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        if (request.body["sign_out"] === 'yes') {
+          return response.redirect(ACCOUNTS_SIGN_OUT_URL);
path: src/presentation/controller/global/Controller.ts
user: login: mattch1
body: I think that note should be an AC itself, otherwise too easy to miss. Will take a look.
created_at: 2025-03-19T14:17:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +32,41 @@ class GlobalController extends AbstractController {
     };
   }
 
+  getSignOut(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        const previousPageUrl = this.getPreviousPageUrl(request);
+        const pageType = super.pageType(request.path);
+        const pageRouting = super.getRouting(
+          globalsRouting,
+          pageType,
+          request
+        );
+        pageRouting.previousUrl = previousPageUrl;
+        response.render(super.templateName(pageRouting.currentUrl), {
+          props: pageRouting
+        });
+      } catch (error) {
+        next(error);
+      }
+    };
+  }
+
+  signOutChoice(): RequestHandler {
+    return (request: Request, response: Response, next: NextFunction) => {
+      try {
+        if (request.body["sign_out"] === 'yes') {
+          return response.redirect(ACCOUNTS_SIGN_OUT_URL);
path: src/presentation/controller/global/Controller.ts
user: login: mattch1
body: Added function to clear cache but unable to add a test that could mock and capture the values.
created_at: 2025-03-20T12:19:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: ch-code-analysis
created_at: 2025-03-18T16:37:02Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-18T16:41:38Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-19T08:20:37Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-19T08:56:27Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-19T09:15:47Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-19T10:03:05Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-19T12:27:26Z
body: CI: No security warnings found
author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-20T08:40:19Z
body: CI: No security warnings found
author_association: NONE
user: login: chsonarqubeprchecks[bot]
created_at: 2025-03-20T12:23:32Z
body: **SonarQube Quality Gate**

[![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](http://code-analysis.platform.aws.chdev.org:9000/dashboard?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246)

[![Bug](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/bug-16px.png 'Bug')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=BUG) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=BUG) [0 Bugs](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=BUG)  
[![Vulnerability](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/vulnerability-16px.png 'Vulnerability')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=VULNERABILITY) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/security_hotspot-16px.png 'Security Hotspot')](http://code-analysis.platform.aws.chdev.org:9000/security_hotspots?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/security_hotspots?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246) [0 Security Hotspots](http://code-analysis.platform.aws.chdev.org:9000/security_hotspots?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246)  
[![Code Smell](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/common/code_smell-16px.png 'Code Smell')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=CODE_SMELL) [![A](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/RatingBadge/A-16px.png 'A')](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=CODE_SMELL) [0 Code Smells](http://code-analysis.platform.aws.chdev.org:9000/project/issues?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&resolved=false&types=CODE_SMELL)

[![87.5%](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/CoverageChart/60-16px.png '87.5%')](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&metric=new_coverage&view=list) [87.5% Coverage](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&metric=new_coverage&view=list)  
[![0.0%](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v86/checks/Duplications/3-16px.png '0.0%')](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](http://code-analysis.platform.aws.chdev.org:9000/component_measures?id=uk.gov.companieshouse%3Alimited-partnerships-web&pullRequest=246&metric=new_duplicated_lines_density&view=list)


author_association: NONE
user: login: ch-code-analysis
created_at: 2025-03-20T12:27:27Z
body: CI: No security warnings found
author_association: NONE