Repository: oaknational/Oak-Web-Application
Pull request #3316: [PUPIL-1226] Display related subjects based on overrides
Author: login: BTitterington
Labels: name: released
Created: 2025-03-24T12:08:25Z | Merged: 2025-03-28T13:25:39Z

## Description

Music year: {owa_music_year}

- List of changes

## Issue(s)

Fixes #

## How to test

1. Go to https://deploy-preview-3316--oak-web-application.netlify.thenational.academy
2. Click on \_\_\_\_\_\_
3. You should see \_\_\_\_\_\_

## Screenshots

How it used to look (delete if n/a):
{screenshots}

How it should now look:
{screenshots}

## Checklist

- [ ] Added or updated tests where appropriate
- [ ] Manually tested across browsers / devices
- [ ] Considered impact on accessibility
- [ ] Design sign-off
- [ ] Approved by product owner
- [ ] Does this PR update a package with a breaking change



Diff:
diff --git a/package-lock.json b/package-lock.json
index 8b69fed791..cc881342fa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7192,9 +7192,21 @@
       }
     },
     "node_modules/@oaknational/oak-components": {
+<<<<<<< HEAD
+<<<<<<< HEAD
       "version": "1.97.0",
       "resolved": "https://registry.npmjs.org/@oaknational/oak-components/-/oak-components-1.97.0.tgz",
       "integrity": "sha512-sQvLg/i5TVv54pHPyRCgPJCn4HWZxlnuXTAyr42EH9Uo7bcGWRauW74jIe0NWbm/mp6PXpRtqv7ekUp9YqGaCw==",
+=======
+      "version": "1.95.1",
+      "resolved": "https://registry.npmjs.org/@oaknational/oak-components/-/oak-components-1.95.1.tgz",
+      "integrity": "sha512-3kgFm0PNylYVOltEDQ1yTcT62AwCQunAwOEKRghAv1lDYjhStpJ0gDE0iSs0OhzW2ZF545fMDMw/CBorge0KBw==",
+>>>>>>> main
+=======
+      "version": "1.97.0",
+      "resolved": "https://registry.npmjs.org/@oaknational/oak-components/-/oak-components-1.97.0.tgz",
+      "integrity": "sha512-sQvLg/i5TVv54pHPyRCgPJCn4HWZxlnuXTAyr42EH9Uo7bcGWRauW74jIe0NWbm/mp6PXpRtqv7ekUp9YqGaCw==",
+>>>>>>> main
       "peerDependencies": {
         "next": "^14.2.12",
         "next-cloudinary": "^5.20.0",
diff --git a/src/__tests__/pages/teachers/programmes/[programmeSlug]/units/unitListing.test.tsx b/src/__tests__/pages/teachers/programmes/[programmeSlug]/units/unitListing.test.tsx
index 1832ce42f8..0dc7727356 100644
--- a/src/__tests__/pages/teachers/programmes/[programmeSlug]/units/unitListing.test.tsx
+++ b/src/__tests__/pages/teachers/programmes/[programmeSlug]/units/unitListing.test.tsx
@@ -82,6 +82,34 @@ describe("pages/programmes/[programmeSlug]/units", () => {
     );
   });
 
+  it("finance banners are displayed", () => {
+    const { getAllByTestId } = render(
+      <UnitListingPage
+        curriculumData={{
+          ...unitListingFixture(),
+          examBoardTitle: "OCR",
+          relatedSubjects: ["financial-education"],
+        }}
+      />,
+    );
+
+    expect(getAllByTestId("financial-education-banner").length).toBeGreaterThan(
+      0,
+    );
+  });
+
+  it("finance banners are not displayed", () => {
+    const { queryAllByTestId } = render(
+      <UnitListingPage
+        curriculumData={{
+          ...unitListingFixture(),
+          examBoardTitle: "OCR",
+        }}
+      />,
+    );
+    expect(queryAllByTestId("financial-education-banner")).toHaveLength(0);
+  });
+
   describe("SEO", () => {
     it("renders the correct SEO details for programme", async () => {
       const { seo } = renderWithSeo()(
diff --git a/src/components/PupilComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.test.tsx b/src/components/PupilComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.test.tsx
new file mode 100644
index 0000000000..533732f87a
--- /dev/null
+++ b/src/components/PupilComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from "@testing-library/react";
+
+import RelatedSubjectsBanner from "./RelatedSubjectsBanner";
+
+import { unitBrowseDataFixture } from "@/node-lib/curriculum-api-2023/fixtures/unitBrowseData.fixture";
+
+// Mock the FinancialEducationBanner component
+jest.mock("./banners/FinancialEducationBanner", () => {
+  return jest.fn(() => (
+    <div data-testid="financial-education-banner">
+      Financial Education Description
+    </div>
+  ));
+});
+
+describe("RelatedSubjectsBanner", () => {
+  const programmeFields = unitBrowseDataFixture({}).programmeFields;
+
+  it('renders FinancialEducationBanner when subjectSlug is "financial-education"', () => {
+    render(
+      <RelatedSubjectsBanner
+        subjectSlug="financial-education"
+        programmeFields={programmeFields}
+      />,
+    );
+    expect(
+      screen.getByTestId("financial-education-banner"),
+    ).toBeInTheDocument();
+  });
+
+  it("renders nothing when subjectSlug is not 'financial-education'", () => {
+    const { container } = render(
+      <RelatedSubjectsBanner
+        subjectSlug="testing-not-for-publication"
+        programmeFields={programmeFields}
+      />,
+    );
+    expect(container.firstChild).toBeNull();
+  });
+});
diff --git a/src/components/PupilComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.tsx b/src/components/PupilComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.tsx
new file mode 100644
index 0000000000..9f39e3cb66
--- /dev/null
+++ b/src/components/PupilComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.tsx
@@ -0,0 +1,34 @@
+import FinancialEducationBanner from "./banners/FinancialEducationBanner";
+
+import { UnitListingBrowseData } from "@/node-lib/curriculum-api-2023/queries/pupilUnitListing/pupilUnitListing.schema";
+import { SubjectSlugs } from "@/node-lib/curriculum-api-2023/queries/pupilSubjectListing/pupilSubjectListing.schema";
+
+type ProgrammeFields = UnitListingBrowseData[number]["programmeFields"];
+
+type OptionalSubjectDescriptionsType = {
+  [key in SubjectSlugs]?: (args: Readonly<BannerProps>) => JSX.Element;
+};
+
+export interface BannerProps {
+  readonly programmeFields: ProgrammeFields;
+}
+
+const OptionalSubjectDescriptions: OptionalSubjectDescriptionsType = {
+  "financial-education": ({ programmeFields }) => (
+    <FinancialEducationBanner programmeFields={programmeFields} />
+  ),
+};
+
+function RelatedSubjectsBanner({
+  subjectSlug,
+  programmeFields,
+}: {
+  subjectSlug: SubjectSlugs;
+  programmeFields: ProgrammeFields;
+}) {
+  const descriptionFunction = OptionalSubjectDescriptions[subjectSlug];
+
+  return descriptionFunction ? descriptionFunction({ programmeFields }) : null;
+}
+
+export default RelatedSubjectsBanner;
diff --git a/src/components/PupilComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.test.tsx b/src/components/PupilComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.test.tsx
new file mode 100644
index 0000000000..057824f6de
--- /dev/null
+++ b/src/components/PupilComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.test.tsx
@@ -0,0 +1,69 @@
+import { screen } from "@testing-library/react";
+import { oakDefaultTheme, OakThemeProvider } from "@oaknational/oak-components";
+
+import FinancialEducationBanner from "./FinancialEducationBanner";
+
+import { unitBrowseDataFixture } from "@/node-lib/curriculum-api-2023/fixtures/unitBrowseData.fixture";
+import renderWithTheme from "@/__tests__/__helpers__/renderWithTheme";
+
+// Mock resolveOakHref to generate a predictable URL
+jest.mock("@/common-lib/urls", () => ({
+  resolveOakHref: jest.fn(
+    ({ page, programmeSlug }) => `/${page}/pupil-unit-index/${programmeSlug}`,
+  ),
+}));
+
+// Only mock OakLinkCard while keeping other Oak components as they are.
+jest.mock("@oaknational/oak-components", () => {
+  const actualComponents = jest.requireActual("@oaknational/oak-components");
+  return {
+    ...actualComponents,
+    OakLinkCard: jest.fn(
+      ({ mainSection, iconName, iconAlt, href, showNew }) => (
+        <div data-testid="oak-link-card" data-href={href}>
+          {mainSection}
+          <span data-testid="icon-name">{iconName}</span>
+          <span data-testid="icon-alt">{iconAlt}</span>
+          <span data-testid="show-new">{showNew ? "true" : "false"}</span>
+        </div>
+      ),
+    ),
+  };
+});
+
+describe("FinancialEducationBanner", () => {
+  const programmeFields = unitBrowseDataFixture({}).programmeFields;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("renders the banner with the correct OakLinkCard props", () => {
+    renderWithTheme(
+      <OakThemeProvider theme={oakDefaultTheme}>
+        <FinancialEducationBanner programmeFields={programmeFields} />
+      </OakThemeProvider>,
+    );
+
+    // Verify that the outer banner element is rendered
+    expect(
+      screen.getByTestId("financial-education-banner"),
+    ).toBeInTheDocument();
+
+    // Verify the mocked OakLinkCard received the correct href
+    const card = screen.getByTestId("oak-link-card");
+    expect(card).toBeInTheDocument();
+    expect(card).toHaveAttribute(
+      "data-href",
+      "/pupil-unit-index/pupil-unit-index/financial-education-secondary-year-1",
+    );
+
+    expect(screen.getByTestId("icon-name")).toHaveTextContent(
+      "subject-financial-education",
+    );
+    expect(screen.getByTestId("icon-alt")).toHaveTextContent(
+      "Illustration of persons head with finance ideas",
+    );
+    expect(screen.getByTestId("show-new")).toHaveTextContent("true");
+  });
+});
diff --git a/src/components/PupilComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx b/src/components/PupilComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx
new file mode 100644
index 0000000000..4739741928
--- /dev/null
+++ b/src/components/PupilComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx
@@ -0,0 +1,53 @@
+import {
+  OakFlex,
+  OakHeading,
+  OakLinkCard,
+  OakP,
+  OakTertiaryButton,
+} from "@oaknational/oak-components";
+
+import { BannerProps } from "../RelatedSubjectsBanner";
+
+import { resolveOakHref } from "@/common-lib/urls";
+
+function FinancialEducationBanner({ programmeFields }: Readonly<BannerProps>) {
+  const programmeSlug = `financial-education-secondary-${programmeFields.yearSlug}`;
+
+  const href = resolveOakHref({
+    page: "pupil-unit-index",
+    programmeSlug,
+  });
+
+  const mainSection = (
+    <OakFlex $flexDirection="column" $gap="space-between-xs">
+      <OakHeading tag="h1" $font="heading-5">
+        Check out our new finance lessons!
+      </OakHeading>
+      <OakP>
+        Learn fun and easy ways to understand money and how to use it in real
+        life.
+      </OakP>
+      <OakTertiaryButton iconName="chevron-right" width="100%" isTrailingIcon>
+        Go to new finance lessons
+      </OakTertiaryButton>
+    </OakFlex>
+  );
+
+  return (
+    <OakFlex
+      $width={["100%", "all-spacing-22", "all-spacing-23"]}
+      $mt="space-between-m"
+      data-testid="financial-education-banner"
+    >
+      <OakLinkCard
+        mainSection={mainSection}
+        iconName={"subject-financial-education"}
+        iconAlt="Illustration of persons head with finance ideas"
+        href={href}
+        showNew
+      />
+    </OakFlex>
+  );
+}
+
+export default FinancialEducationBanner;
diff --git a/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.test.tsx b/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.test.tsx
index d8faba28bc..04303a194b 100644
--- a/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.test.tsx
+++ b/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.test.tsx
@@ -226,7 +226,6 @@ describe("PupilViewsUnitListing", () => {
     expect(getByText("Optionality Title 1")).toBeInTheDocument();
     expect(getByText("Optionality Title 2")).toBeInTheDocument();
   });
-
   it("does render subject description for financial education", async () => {
     const { queryByTestId } = await renderWithTheme(
       <OakThemeProvider theme={oakDefaultTheme}>
@@ -265,4 +264,41 @@ describe("PupilViewsUnitListing", () => {
     );
     expect(financeSubjectDescription).not.toBeInTheDocument();
   });
+  it("renders a related subject banner if present", async () => {
+    const { queryByTestId } = await renderWithTheme(
+      <OakThemeProvider theme={oakDefaultTheme}>
+        <PupilViewsUnitListing
+          unitSections={unitsWithOptionality}
+          phase="secondary"
+          backHrefSlugs={backHrefSlugs}
+          subjectCategories={[]}
+          programmeFields={programmeFields}
+          relatedSubjects={["financial-education"]}
+        />
+      </OakThemeProvider>,
+    );
+    const financeSubjectDescription = await queryByTestId(
+      "financial-education-banner",
+    );
+    expect(financeSubjectDescription).toBeInTheDocument();
+  });
+
+  it("renders no related subject banners if no related subjects", async () => {
+    const { queryByTestId } = await renderWithTheme(
+      <OakThemeProvider theme={oakDefaultTheme}>
+        <PupilViewsUnitListing
+          unitSections={unitsWithOptionality}
+          phase="secondary"
+          backHrefSlugs={backHrefSlugs}
+          subjectCategories={[]}
+          programmeFields={programmeFields}
+          relatedSubjects={[]}
+        />
+      </OakThemeProvider>,
+    );
+    const financeSubjectDescription = await queryByTestId(
+      "financial-education-banner",
+    );
+    expect(financeSubjectDescription).not.toBeInTheDocument();
+  });
 });
diff --git a/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.tsx b/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.tsx
index ea556f27fc..1db969279f 100644
--- a/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.tsx
+++ b/src/components/PupilViews/PupilUnitListing/PupilUnitListing.view.tsx
@@ -17,6 +17,7 @@ import useAnalytics from "@/context/Analytics/useAnalytics";
 import { UnitListingBrowseData } from "@/node-lib/curriculum-api-2023/queries/pupilUnitListing/pupilUnitListing.schema";
 import { generateKeyStageTitle } from "@/components/PupilComponents/PupilAnalyticsProvider/PupilAnalyticsProvider";
 import { SubjectSlugs } from "@/node-lib/curriculum-api-2023/queries/pupilSubjectListing/pupilSubjectListing.schema";
+import RelatedSubjectsBanner from "@/components/PupilComponents/RelatedSubjectsBanner/RelatedSubjectsBanner";
 import PupilSubjectDescription from "@/components/PupilComponents/PupilSubjectDescription/PupilSubjectDescription";
 
 export type PupilViewsUnitListingProps = {
@@ -34,6 +35,7 @@ export const PupilViewsUnitListing = ({
   backHrefSlugs,
   subjectCategories,
   programmeFields,
+  relatedSubjects = [],
 }: PupilViewsUnitListingProps) => {
   const { track } = useAnalytics();
   const [backHref, backLabel] = useBackHref(backHrefSlugs);
@@ -156,6 +158,13 @@ export const PupilViewsUnitListing = ({
             />
           );
         })}
+        {relatedSubjects.map((subjectSlug) => (
+          <RelatedSubjectsBanner
+            key={subjectSlug}
+            subjectSlug={subjectSlug}
+            programmeFields={programmeFields}
+          />
+        ))}
         <OakBox $mt={"space-between-m2"}>
           <SignpostTeachersInlineBanner />
         </OakBox>
diff --git a/src/components/TeacherComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.test.tsx b/src/components/TeacherComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.test.tsx
new file mode 100644
index 0000000000..513ad962b5
--- /dev/null
+++ b/src/components/TeacherComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen } from "@testing-library/react";
+
+import RelatedSubjectsBanner from "./RelatedSubjectsBanner";
+
+// Mock the FinancialEducationBanner component
+jest.mock("./banners/FinancialEducationBanner", () => {
+  return jest.fn(({ isDesktop }) => (
+    <>
+      <div data-testid="financial-education-banner">
+        Financial Education Description
+      </div>
+      <div data-testid="financial-education-banner-isDesktop">
+        {isDesktop ? "true" : "false"}
+      </div>
+    </>
+  ));
+});
+
+describe("RelatedSubjectsBanner", () => {
+  it('renders FinancialEducationBanner when subjectSlug is "financial-education"', () => {
+    render(
+      <RelatedSubjectsBanner
+        subjectSlug="financial-education"
+        keyStageSlug="ks4"
+        phase="secondary"
+        isDesktop
+      />,
+    );
+    expect(
+      screen.getByTestId("financial-education-banner"),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByTestId("financial-education-banner-isDesktop"),
+    ).toHaveTextContent("true");
+  });
+
+  it("renders nothing when subjectSlug is not 'financial-education'", () => {
+    const { container } = render(
+      <RelatedSubjectsBanner
+        subjectSlug="testing-not-for-publication"
+        keyStageSlug="ks4"
+        phase="secondary"
+        isDesktop
+      />,
+    );
+    expect(container.firstChild).toBeNull();
+  });
+  it("passes through isDesktop value", () => {
+    render(
+      <RelatedSubjectsBanner
+        subjectSlug="financial-education"
+        keyStageSlug="ks4"
+        phase="secondary"
+        isDesktop={false}
+      />,
+    );
+    expect(
+      screen.getByTestId("financial-education-banner"),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByTestId("financial-education-banner-isDesktop"),
+    ).toHaveTextContent("false");
+  });
+});
diff --git a/src/components/TeacherComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.tsx b/src/components/TeacherComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.tsx
new file mode 100644
index 0000000000..e4bf06d583
--- /dev/null
+++ b/src/components/TeacherComponents/RelatedSubjectsBanner/RelatedSubjectsBanner.tsx
@@ -0,0 +1,53 @@
+import FinancialEducationBanner from "./banners/FinancialEducationBanner";
+
+import { UnitListingBrowseData } from "@/node-lib/curriculum-api-2023/queries/pupilUnitListing/pupilUnitListing.schema";
+import { SubjectSlugs } from "@/node-lib/curriculum-api-2023/queries/pupilSubjectListing/pupilSubjectListing.schema";
+
+type KeyStageSlug =
+  UnitListingBrowseData[number]["programmeFields"]["keystageSlug"];
+
+type Phase = UnitListingBrowseData[number]["programmeFields"]["phase"];
+
+type OptionalSubjectDescriptionsType = {
+  [key in SubjectSlugs]?: (args: BannerProps) => JSX.Element;
+};
+
+export interface BannerProps {
+  readonly keyStageSlug: KeyStageSlug;
+  readonly phase: Phase;
+  readonly isDesktop: boolean;
+}
+
+const OptionalSubjectDescriptions: OptionalSubjectDescriptionsType = {
+  "financial-education": ({
+    keyStageSlug,
+    phase,
+    isDesktop,
+  }: Readonly<BannerProps>) => (
+    <FinancialEducationBanner
+      keyStageSlug={keyStageSlug}
+      phase={phase}
+      isDesktop={isDesktop}
+    />
+  ),
+};
+
+function RelatedSubjectsBanner({
+  subjectSlug,
+  keyStageSlug,
+  phase,
+  isDesktop,
+}: {
+  subjectSlug: SubjectSlugs;
+  keyStageSlug: KeyStageSlug;
+  phase: Phase;
+  isDesktop: boolean;
+}) {
+  const descriptionFunction = OptionalSubjectDescriptions[subjectSlug];
+
+  return descriptionFunction
+    ? descriptionFunction({ keyStageSlug, phase, isDesktop })
+    : null;
+}
+
+export default RelatedSubjectsBanner;
diff --git a/src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.test.tsx b/src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.test.tsx
new file mode 100644
index 0000000000..0650b8c745
--- /dev/null
+++ b/src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.test.tsx
@@ -0,0 +1,88 @@
+import { screen } from "@testing-library/react";
+import { oakDefaultTheme, OakThemeProvider } from "@oaknational/oak-components";
+
+import FinancialEducationBanner from "./FinancialEducationBanner";
+
+import renderWithTheme from "@/__tests__/__helpers__/renderWithTheme";
+
+// Mock resolveOakHref to generate a predictable URL
+jest.mock("@/common-lib/urls", () => ({
+  resolveOakHref: jest.fn(
+    ({ page, programmeSlug }) => `/${page}/pupil-unit-index/${programmeSlug}`,
+  ),
+}));
+
+// Only mock OakLinkCard while keeping other Oak components as they are.
+jest.mock("@oaknational/oak-components", () => {
+  const actualComponents = jest.requireActual("@oaknational/oak-components");
+  return {
+    ...actualComponents,
+    OakLinkCard: jest.fn(
+      ({ mainSection, iconName, iconAlt, href, showNew }) => (
+        <div data-testid="oak-link-card" data-href={href}>
+          {mainSection}
+          <span data-testid="icon-name">{iconName}</span>
+          <span data-testid="icon-alt">{iconAlt}</span>
+          <span data-testid="show-new">{showNew ? "true" : "false"}</span>
+        </div>
+      ),
+    ),
+  };
+});
+
+describe("FinancialEducationBanner", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("renders the banner with the correct OakLinkCard props", () => {
+    renderWithTheme(
+      <OakThemeProvider theme={oakDefaultTheme}>
+        <FinancialEducationBanner
+          keyStageSlug="ks4"
+          phase="secondary"
+          isDesktop
+        />
+      </OakThemeProvider>,
+    );
+
+    // Verify that the outer banner element is rendered
+    expect(
+      screen.getByTestId("financial-education-banner"),
+    ).toBeInTheDocument();
+
+    // Verify the mocked OakLinkCard received the correct href
+    const card = screen.getByTestId("oak-link-card");
+    expect(card).toBeInTheDocument();
+    expect(card).toHaveAttribute(
+      "data-href",
+      "/unit-index/pupil-unit-index/financial-education-secondary-ks4",
+    );
+
+    expect(screen.getByTestId("icon-name")).toHaveTextContent(
+      "subject-financial-education",
+    );
+    expect(screen.getByTestId("icon-alt")).toHaveTextContent(
+      "Illustration of persons head with finance ideas",
+    );
+    expect(screen.getByTestId("show-new")).toHaveTextContent("true");
+  });
+  it("renders the banner with the correct href", () => {
+    renderWithTheme(
+      <OakThemeProvider theme={oakDefaultTheme}>
+        <FinancialEducationBanner
+          keyStageSlug="ks1"
+          phase="primary"
+          isDesktop
+        />
+      </OakThemeProvider>,
+    );
+
+    const card = screen.getByTestId("oak-link-card");
+    expect(card).toBeInTheDocument();
+    expect(card).toHaveAttribute(
+      "data-href",
+      "/unit-index/pupil-unit-index/financial-education-primary-ks1",
+    );
+  });
+});
diff --git a/src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx b/src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx
new file mode 100644
index 0000000000..2059149bbd
--- /dev/null
+++ b/src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx
@@ -0,0 +1,58 @@
+import {
+  OakFlex,
+  OakHeading,
+  OakLinkCard,
+  OakP,
+  OakTertiaryButton,
+} from "@oaknational/oak-components";
+
+import { BannerProps } from "../RelatedSubjectsBanner";
+
+import { resolveOakHref } from "@/common-lib/urls";
+
+function FinancialEducationBanner({
+  keyStageSlug,
+  phase,
+  isDesktop,
+}: Readonly<BannerProps>) {
+  const programmeSlug = `financial-education-${phase}-${keyStageSlug}`;
+
+  const href = resolveOakHref({
+    page: "unit-index",
+    programmeSlug,
+  });
+
+  const mainSection = (
+    <OakFlex
+      $flexDirection="column"
+      $gap="space-between-xs"
+      data-testid="financial-education-banner"
+    >
+      <OakHeading tag="h1" $font="heading-5">
+        Try our new financial education teaching resources
+      </OakHeading>
+      <OakP>
+        A series of sequenced teaching resources offering practical knowledge
+        and skills to help pupils with their financial education.
+      </OakP>
+      <OakTertiaryButton iconName="chevron-right" width="100%" isTrailingIcon>
+        Go to new finance lessons
+      </OakTertiaryButton>
+    </OakFlex>
+  );
+
+  return (
+    <OakFlex $mt="space-between-m">
+      <OakLinkCard
+        mainSection={mainSection}
+        iconName={"subject-financial-education"}
+        iconAlt="Illustration of persons head with finance ideas"
+        href={href}
+        showNew
+        narrow={isDesktop}
+      />
+    </OakFlex>
+  );
+}
+
+export default FinancialEducationBanner;
diff --git a/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.test.ts b/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.test.ts
index dce94bf8a3..94cdbfd731 100644
--- a/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.test.ts
+++ b/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.test.ts
@@ -196,6 +196,7 @@ describe("unitListing()", () => {
       ],
       learningThemes: [{ themeTitle: "Theme 1", themeSlug: "theme1" }],
       hasNewContent: true,
+      relatedSubjects: ["physics"],
     });
   });
 });
diff --git a/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.ts b/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.ts
index fc8be6ba79..f2a80981af 100644
--- a/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.ts
+++ b/src/node-lib/curriculum-api-2023/queries/unitListing/unitListing.query.ts
@@ -115,7 +115,7 @@ const unitListingQuery =
       subjectCategories,
       yearGroups,
       pathwayTitle: programmeFields.pathway,
-      ...(relatedSubjectsSet.size > 1 && {
+      ...(relatedSubjectsSet.size >= 1 && {
         relatedSubjects: Array.from(relatedSubjectsSet),
       }),
     };
diff --git a/src/pages/teachers/programmes/[programmeSlug]/units.tsx b/src/pages/teachers/programmes/[programmeSlug]/units.tsx
index f1ff08131a..f044fe6d13 100644
--- a/src/pages/teachers/programmes/[programmeSlug]/units.tsx
+++ b/src/pages/teachers/programmes/[programmeSlug]/units.tsx
@@ -6,8 +6,6 @@ import {
   GetStaticPropsResult,
   NextPage,
 } from "next";
-import { examboards, tierSlugs } from "@oaknational/oak-curriculum-schema";
-import { z } from "zod";
 import {
   OakBox,
   OakGrid,
@@ -18,6 +16,8 @@ import {
   OakFlex,
   OakMaxWidth,
 } from "@oaknational/oak-components";
+import { examboards, tierSlugs } from "@oaknational/oak-curriculum-schema";
+import { z } from "zod";
 
 import {
   getFallbackBlockingConfig,
@@ -47,6 +47,7 @@ import NewContentBanner from "@/components/TeacherComponents/NewContentBanner/Ne
 import PaginationHead from "@/components/SharedComponents/Pagination/PaginationHead";
 import MobileUnitFilters from "@/components/TeacherComponents/MobileUnitFilters";
 import DesktopUnitFilters from "@/components/TeacherComponents/DesktopUnitFilters/DesktopUnitFilters";
+import RelatedSubjectsBanner from "@/components/TeacherComponents/RelatedSubjectsBanner/RelatedSubjectsBanner";
 
 export type UnitListingPageProps = {
   curriculumData: UnitListingData;
@@ -69,6 +70,8 @@ const UnitListingPage: NextPage<UnitListingPageProps> = ({
     subjectCategories,
     yearGroups,
     pathwayTitle,
+    relatedSubjects,
+    phase,
   } = curriculumData;
 
   const { track } = useAnalytics();
@@ -292,6 +295,17 @@ const UnitListingPage: NextPage<UnitListingPageProps> = ({
                 browseRefined={track.browseRefined}
                 setSelectedThemeSlug={setSelectedThemeSlug}
               />
+              <OakFlex $display={["none", "none", "flex"]}>
+                {relatedSubjects?.map((subjectSlug) => (
+                  <RelatedSubjectsBanner
+                    key={subjectSlug}
+                    subjectSlug={subjectSlug}
+                    keyStageSlug={keyStageSlug}
+                    phase={phase}
+                    isDesktop={true}
+                  />
+                ))}
+              </OakFlex>
             </OakGridArea>
 
             {/* Header Row */}
@@ -341,6 +355,17 @@ const UnitListingPage: NextPage<UnitListingPageProps> = ({
               )}
             </OakGridArea>
           </OakGrid>
+          <OakFlex $display={["flex", "flex", "none"]} $mb="space-between-xl">
+            {relatedSubjects?.map((subjectSlug) => (
+              <RelatedSubjectsBanner
+                key={subjectSlug}
+                subjectSlug={subjectSlug}
+                keyStageSlug={keyStageSlug}
+                phase={phase}
+                isDesktop={false}
+              />
+            ))}
+          </OakFlex>
         </OakMaxWidth>
       </AppLayout>
     </OakThemeProvider>


Reviews:
user: login: weronika-szalas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:28:38Z
user: login: BTitterington
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T08:39:09Z
user: login: BTitterington
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T09:54:44Z
user: login: benprotheroe
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:11:45Z
user: login: RBrownsett
state: APPROVED
author_association: NONE
submitted_at: 2025-03-28T13:24:43Z

Review comments:
diff_hunk: @@ -0,0 +1,54 @@
+import {
+  OakFlex,
+  OakHeading,
+  OakLinkCard,
+  OakP,
+  OakTertiaryButton,
+} from "@oaknational/oak-components";
+
+import { BannerProps } from "../RelatedSubjectsBanner";
+
+import { resolveOakHref } from "@/common-lib/urls";
+
+function FinancialEducationBanner({ keyStageSlug, isDesktop }: BannerProps) {
+  const programmeSlug = `financial-education-secondary-${keyStageSlug}`;
path: src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx
user: login: weronika-szalas
body: will this be always 'secondary', is there no way to pass programmeSlug as prop?
created_at: 2025-03-26T16:28:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import {
+  OakFlex,
+  OakHeading,
+  OakLinkCard,
+  OakP,
+  OakTertiaryButton,
+} from "@oaknational/oak-components";
+
+import { BannerProps } from "../RelatedSubjectsBanner";
+
+import { resolveOakHref } from "@/common-lib/urls";
+
+function FinancialEducationBanner({ keyStageSlug, isDesktop }: BannerProps) {
+  const programmeSlug = `financial-education-secondary-${keyStageSlug}`;
path: src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx
user: login: BTitterington
body: I think you are right here. There either needs to be the phase passed through, or a conditional on the keystage value that updates that part of the slug. I'd say phase seems like the better approach.
created_at: 2025-03-28T08:39:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import {
+  OakFlex,
+  OakHeading,
+  OakLinkCard,
+  OakP,
+  OakTertiaryButton,
+} from "@oaknational/oak-components";
+
+import { BannerProps } from "../RelatedSubjectsBanner";
+
+import { resolveOakHref } from "@/common-lib/urls";
+
+function FinancialEducationBanner({ keyStageSlug, isDesktop }: BannerProps) {
+  const programmeSlug = `financial-education-secondary-${keyStageSlug}`;
path: src/components/TeacherComponents/RelatedSubjectsBanner/banners/FinancialEducationBanner.tsx
user: login: BTitterington
body: I've adjusted to use the phase in addition now. I couldn't use the programmeSlug as in Maths for instance it looks like this "maths-secondary-ks4-foundation" and the "foundation" part wouldn't be applicable to the finance slugs.
created_at: 2025-03-28T09:54:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: notion-workspace[bot]
created_at: 2025-03-24T12:08:30Z
body: <a href="https://www.notion.so/Implement-cross-reference-of-finance-on-both-unit-listing-pages-1a026cc4e1b180719f6bc5169739066e?pvs=8&n=github_linkback">✏️ Implement cross-reference of finance on both unit listing pages</a>
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-24T12:08:46Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *oak-web-application* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0e33330e642a93d639c0d388b0b661f621da3839 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/oak-web-application/deploys/67e66c98b2cdcc00087ecb74 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-3316--oak-web-application.netlify.app](https://deploy-preview-3316--oak-web-application.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTMzMTYtLW9hay13ZWItYXBwbGljYXRpb24ubmV0bGlmeS5hcHAifQ.KN19M98ODLeeqpI7By4TZ05Wb9lm75D--MotbyZxh2Y)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [oak-web-application Preview](https://deploy-preview-3316--oak-web-application.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/oak-web-application/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-24T12:08:46Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *owa-storybook* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0e33330e642a93d639c0d388b0b661f621da3839 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/owa-storybook/deploys/67e66c98cb6b0000088a34f7 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-3316--owa-storybook.netlify.app](https://deploy-preview-3316--owa-storybook.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTMzMTYtLW93YS1zdG9yeWJvb2submV0bGlmeS5hcHAifQ.BXmTqvhk_dyTty-YvXnFXdNM2SwDdpnqIn-b8EzG2CY)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [owa-storybook Preview](https://deploy-preview-3316--owa-storybook.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/owa-storybook/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: RBrownsett
created_at: 2025-03-26T09:33:47Z
body: @BTitterington looking good - just noticed that the text in the pupil area differs from the design (it has the same text as the teacher area in the PR)? cc @benprotheroe 
PR:
![Screenshot 2025-03-26 at 09 22 46](https://github.com/user-attachments/assets/22b70386-ef90-4678-b133-1a324a4b7379)

Design
![Screenshot 2025-03-26 at 09 23 01](https://github.com/user-attachments/assets/b0526fb2-86c0-41a0-a08e-82743efae105)

author_association: NONE
user: login: BTitterington
created_at: 2025-03-26T09:38:44Z
body: @RBrownsett I might have got my wires crossed, and updated the pupil one with the copy I read on this ticket - https://www.notion.so/oaknationalacademy/OakLinkCard-copy-update-teacher-experience-1b426cc4e1b180438b57c9e01d154a6d?pvs=4
author_association: CONTRIBUTOR
user: login: RBrownsett
created_at: 2025-03-26T15:11:13Z
body: > @RBrownsett I might have got my wires crossed, and updated the pupil one with the copy I read on this ticket - https://www.notion.so/oaknationalacademy/OakLinkCard-copy-update-teacher-experience-1b426cc4e1b180438b57c9e01d154a6d?pvs=4

No worries - is it ok to update to match the designs? 
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-28T09:42:51Z
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=oaknational_owa&pullRequest=3316) **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=oaknational_owa&pullRequest=3316&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=oaknational_owa&pullRequest=3316&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=oaknational_owa&pullRequest=3316&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=oaknational_owa&pullRequest=3316&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=oaknational_owa&pullRequest=3316&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=oaknational_owa&pullRequest=3316)


author_association: NONE
user: login: BTitterington
created_at: 2025-03-28T09:55:36Z
body: > > @RBrownsett I might have got my wires crossed, and updated the pupil one with the copy I read on this ticket - https://www.notion.so/oaknationalacademy/OakLinkCard-copy-update-teacher-experience-1b426cc4e1b180438b57c9e01d154a6d?pvs=4
> 
> No worries - is it ok to update to match the designs?

@RBrownsett - I've updated the text based on the designs, hopefully that is the right copy now.
author_association: CONTRIBUTOR
user: login: RBrownsett
created_at: 2025-03-28T13:24:32Z
body: @benprotheroe small regression in live the teacher area some extra padding in mobile/tablet view between the bottom of units and the footer (from percy as well). I'll make a ticket to fix after release 
![image](https://github.com/user-attachments/assets/12206fec-c69c-45f0-b2ee-47648da1afd2)

author_association: NONE
user: login: oak-machine-user
created_at: 2025-03-28T13:32:20Z
body: :tada: This PR is included in version 1.720.0 :tada:

The release is available on [GitHub release](https://github.com/oaknational/Oak-Web-Application/releases/tag/v1.720.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR