Skip to content

Commit 24b9866

Browse files
VIA-629 AJ/EO Move campaign logic up to the Vaccine and MoreInformation components. WIP
1 parent d4a12c5 commit 24b9866

File tree

5 files changed

+139
-65
lines changed

5 files changed

+139
-65
lines changed

src/app/_components/content/MoreInformation.test.tsx

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,58 @@
11
import { MoreInformation } from "@src/app/_components/content/MoreInformation";
22
import { VaccineType } from "@src/models/vaccine";
3+
import { Campaigns } from "@src/utils/campaigns/types";
4+
import config from "@src/utils/config";
5+
import { ConfigMock, configBuilder } from "@test-data/config/builders";
36
import { mockStyledContent, mockStyledContentWithoutWhatSection } from "@test-data/content-api/data";
47
import { render, screen } from "@testing-library/react";
5-
import React from "react";
8+
9+
jest.mock("sanitize-data", () => ({ sanitize: jest.fn() }));
10+
jest.mock("@src/utils/config");
611

712
describe("MoreInformation component ", () => {
13+
const mockedConfig = config as ConfigMock;
14+
15+
beforeEach(() => {
16+
const defaultConfig = configBuilder()
17+
.withCampaigns(
18+
Campaigns.fromJson(
19+
JSON.stringify({
20+
COVID_19: [
21+
{ start: "2025-11-01T09:00:00Z", end: "2026-01-31T09:00:00Z" },
22+
{ start: "2026-11-01T09:00:00Z", end: "2027-01-31T09:00:00Z" },
23+
],
24+
}),
25+
)!,
26+
)
27+
.build();
28+
Object.assign(mockedConfig, defaultConfig);
29+
});
30+
831
describe("moreInformationHeadersFromContentApi false", () => {
932
it("should display whatItIsFor expander block", async () => {
1033
const vaccineType = VaccineType.RSV;
11-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
34+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
1235

1336
expectExpanderBlockToBePresent("What the vaccine is for", "What Section styled component");
1437
});
1538

1639
it("should display whoVaccineIsFor expander block", async () => {
1740
const vaccineType = VaccineType.RSV;
18-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
41+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
1942

2043
expectExpanderBlockToBePresent("Who should have the vaccine", "Who Section styled component");
2144
});
2245

2346
it("should display howToGet expander block", async () => {
2447
const vaccineType = VaccineType.TD_IPV_3_IN_1;
25-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
48+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
2649

2750
expectExpanderBlockToBePresent("How to get the vaccine", "How Section styled component");
2851
});
2952

3053
it("should display vaccineSideEffects expander block", async () => {
3154
const vaccineType = VaccineType.RSV;
32-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
55+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
3356

3457
expectExpanderBlockToBePresent("Side effects of the vaccine", "Side effects section styled component");
3558
});
@@ -38,36 +61,36 @@ describe("MoreInformation component ", () => {
3861
describe("moreInformationHeadersFromContentApi true", () => {
3962
it("should display whatItIsFor expander block", async () => {
4063
const vaccineType = VaccineType.FLU_IN_PREGNANCY;
41-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
64+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
4265

4366
expectExpanderBlockToBePresent("what-heading", "What Section styled component");
4467
});
4568

4669
it("should display whoVaccineIsFor expander block", async () => {
4770
const vaccineType = VaccineType.FLU_IN_PREGNANCY;
48-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
71+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
4972

5073
expectExpanderBlockToBePresent("who-heading", "Who Section styled component");
5174
});
5275

5376
it("should display howToGet expander block", async () => {
5477
const vaccineType = VaccineType.FLU_IN_PREGNANCY;
55-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
78+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
5679

5780
expectExpanderBlockToBePresent("how-heading", "How Section styled component");
5881
});
5982

6083
it("should display vaccineSideEffects expander block", async () => {
6184
const vaccineType = VaccineType.FLU_IN_PREGNANCY;
62-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
85+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
6386

6487
expectExpanderBlockToBePresent("side-effects-heading", "Side effects section styled component");
6588
});
6689
});
6790

6891
it("should not include 'how to get' section for RSV_PREGNANCY ", async () => {
6992
const vaccineType = VaccineType.RSV_PREGNANCY;
70-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
93+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
7194

7295
const heading: HTMLElement | null = screen.queryByText("How to get the vaccine");
7396

@@ -76,7 +99,7 @@ describe("MoreInformation component ", () => {
7699

77100
it("should not include 'how to get' section for RSV ", async () => {
78101
const vaccineType = VaccineType.RSV;
79-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
102+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
80103

81104
const heading: HTMLElement | null = screen.queryByText("How to get the vaccine");
82105

@@ -85,7 +108,7 @@ describe("MoreInformation component ", () => {
85108

86109
it("should display webpage link to more information about vaccine", async () => {
87110
const vaccineType = VaccineType.RSV;
88-
render(<MoreInformation styledVaccineContent={mockStyledContent} vaccineType={vaccineType} />);
111+
render(await MoreInformation({ styledVaccineContent: mockStyledContent, vaccineType: vaccineType }));
89112

90113
const webpageLink: HTMLElement = screen.getByRole("link", {
91114
name: "Find out more about the RSV vaccine",
@@ -98,7 +121,9 @@ describe("MoreInformation component ", () => {
98121

99122
it("should not display whatItIsFor section if undefined in content", async () => {
100123
const vaccineType = VaccineType.RSV;
101-
render(<MoreInformation styledVaccineContent={mockStyledContentWithoutWhatSection} vaccineType={vaccineType} />);
124+
render(
125+
await MoreInformation({ styledVaccineContent: mockStyledContentWithoutWhatSection, vaccineType: vaccineType }),
126+
);
102127

103128
const whatItIsForHeading: HTMLElement | null = screen.queryByText("What the vaccine is for");
104129
const whatItIsForContent: HTMLElement | null = screen.queryByText("What Section styled component");
@@ -109,7 +134,9 @@ describe("MoreInformation component ", () => {
109134

110135
it("should display whoVaccineIsFor section even if whatItIsFor is undefined in content", async () => {
111136
const vaccineType = VaccineType.RSV;
112-
render(<MoreInformation styledVaccineContent={mockStyledContentWithoutWhatSection} vaccineType={vaccineType} />);
137+
render(
138+
await MoreInformation({ styledVaccineContent: mockStyledContentWithoutWhatSection, vaccineType: vaccineType }),
139+
);
113140

114141
expectExpanderBlockToBePresent("Who should have the vaccine", "Who Section styled component");
115142
});

src/app/_components/content/MoreInformation.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1+
"use server";
2+
13
import { FindOutMoreLink } from "@src/app/_components/content/FindOutMore";
24
import { HEADINGS } from "@src/app/constants";
35
import { VaccineInfo, VaccineType } from "@src/models/vaccine";
46
import { StyledVaccineContent } from "@src/services/content-api/types";
7+
import config from "@src/utils/config";
8+
import { UtcDateTimeFromStringSchema } from "@src/utils/date";
9+
import { headers } from "next/headers";
510
import { Details } from "nhsuk-react-components";
611
import React, { JSX } from "react";
712

813
// Ref: https://main--65aa76b29d00a047fe683b95.chromatic.com/?path=/docs/content-presentation-details--docs#expander-group-2
9-
const MoreInformation = (props: {
14+
const MoreInformation = async (props: {
1015
styledVaccineContent: StyledVaccineContent;
1116
vaccineType: VaccineType;
12-
}): JSX.Element => {
17+
}): Promise<JSX.Element> => {
18+
const campaigns = await config.CAMPAIGNS;
19+
const now = await _getNow();
20+
const isCampaignActive: boolean = campaigns.isActive(props.vaccineType, now);
21+
22+
async function _getNow() {
23+
try {
24+
const headersList = await headers();
25+
return UtcDateTimeFromStringSchema.safeParse(headersList.get("x-e2e-datetime")).data ?? new Date();
26+
} catch {
27+
return new Date();
28+
}
29+
}
30+
1331
const vaccineInfo = VaccineInfo[props.vaccineType];
14-
const showHowToGetExpander =
15-
vaccineInfo.removeHowToGetExpanderFromMoreInformationSection === undefined ||
16-
!vaccineInfo.removeHowToGetExpanderFromMoreInformationSection;
32+
const showHowToGetExpander = vaccineInfo.removeHowToGetExpanderFromMoreInformationSection ? false : !isCampaignActive;
1733

1834
return (
1935
<>

src/app/_components/vaccine/Vaccine.test.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,41 @@ import { VaccineType } from "@src/models/vaccine";
77
import { getContentForVaccine } from "@src/services/content-api/content-service";
88
import { ContentErrorTypes } from "@src/services/content-api/types";
99
import { getEligibilityForPerson } from "@src/services/eligibility-api/domain/eligibility-filter-service";
10-
import {
11-
EligibilityErrorTypes,
12-
EligibilityForPersonType,
13-
EligibilityStatus,
14-
} from "@src/services/eligibility-api/types";
10+
import { EligibilityErrorTypes, EligibilityForPersonType, EligibilityStatus } from "@src/services/eligibility-api/types";
11+
import { Campaigns } from "@src/utils/campaigns/types";
12+
import config from "@src/utils/config";
13+
import { ConfigMock, configBuilder } from "@test-data/config/builders";
1514
import { mockStyledContent } from "@test-data/content-api/data";
1615
import { eligibilityContentBuilder } from "@test-data/eligibility-api/builders";
1716
import { render, screen } from "@testing-library/react";
1817
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
1918
import { headers } from "next/headers";
2019
import React, { JSX } from "react";
2120

21+
22+
23+
24+
25+
26+
27+
28+
29+
30+
31+
32+
33+
34+
35+
36+
37+
38+
39+
40+
41+
42+
43+
44+
2245
jest.mock("@src/services/content-api/content-service", () => ({
2346
getContentForVaccine: jest.fn(),
2447
}));
@@ -58,6 +81,7 @@ jest.mock("next/headers", () => ({
5881
headers: jest.fn(),
5982
}));
6083
jest.mock("sanitize-data", () => ({ sanitize: jest.fn() }));
84+
jest.mock("@src/utils/config");
6185
jest.mock("cheerio", () => ({
6286
load: jest.fn(() => {
6387
const selectorImpl = jest.fn(() => ({
@@ -97,6 +121,24 @@ const contentErrorResponse = {
97121
};
98122

99123
describe("Any vaccine page", () => {
124+
125+
const mockedConfig = config as ConfigMock;
126+
127+
beforeEach(() => {
128+
const defaultConfig = configBuilder()
129+
.withCampaigns(
130+
Campaigns.fromJson(
131+
JSON.stringify({
132+
COVID_19: [
133+
{ start: "2025-11-01T09:00:00Z", end: "2026-01-31T09:00:00Z" }
134+
],
135+
}),
136+
)!,
137+
)
138+
.build();
139+
Object.assign(mockedConfig, defaultConfig);
140+
});
141+
100142
const renderNamedVaccinePage = async (vaccineType: VaccineType) => {
101143
render(await Vaccine({ vaccineType: vaccineType }));
102144
};

src/app/_components/vaccine/Vaccine.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import { getContentForVaccine } from "@src/services/content-api/content-service"
1515
import { ContentErrorTypes, StyledVaccineContent } from "@src/services/content-api/types";
1616
import { getEligibilityForPerson } from "@src/services/eligibility-api/domain/eligibility-filter-service";
1717
import { EligibilityErrorTypes, EligibilityForPersonType } from "@src/services/eligibility-api/types";
18+
import config from "@src/utils/config";
19+
import { UtcDateTimeFromStringSchema } from "@src/utils/date";
1820
import { profilePerformanceEnd, profilePerformanceStart } from "@src/utils/performance";
1921
import { requestScopedStorageWrapper } from "@src/utils/requestScopedStorageWrapper";
2022
import { Session } from "next-auth";
23+
import { headers } from "next/headers";
2124
import React, { JSX } from "react";
2225

2326
import styles from "./styles.module.css";
@@ -39,6 +42,18 @@ const VaccineComponent = async ({ vaccineType }: VaccineProps): Promise<JSX.Elem
3942
const nhsNumber: NhsNumber | undefined = session?.user.nhs_number as NhsNumber;
4043
const vaccineInfo: VaccineDetails = VaccineInfo[vaccineType];
4144

45+
const campaigns = await config.CAMPAIGNS;
46+
const now = await _getNow();
47+
const isCampaignActive: boolean = campaigns.isActive(vaccineType, now);
48+
async function _getNow() {
49+
try {
50+
const headersList = await headers();
51+
return UtcDateTimeFromStringSchema.safeParse(headersList.get("x-e2e-datetime")).data ?? new Date();
52+
} catch {
53+
return new Date();
54+
}
55+
}
56+
4257
let styledVaccineContent: StyledVaccineContent | undefined;
4358
let contentError: ContentErrorTypes | undefined;
4459
let eligibilityForPerson: EligibilityForPersonType | undefined;
@@ -71,8 +86,10 @@ const VaccineComponent = async ({ vaccineType }: VaccineProps): Promise<JSX.Elem
7186
<>
7287
<Overview overview={styledVaccineContent.overview} vaccineType={vaccineType} />
7388
<Recommendation styledVaccineContent={styledVaccineContent} />
74-
<WarningCallout styledVaccineContent={styledVaccineContent} vaccineType={vaccineType} />
75-
<EligibilityActions actions={styledVaccineContent.actions} vaccineType={vaccineType} />
89+
{!isCampaignActive && (
90+
<WarningCallout styledVaccineContent={styledVaccineContent} vaccineType={vaccineType} />
91+
)}
92+
{isCampaignActive && <EligibilityActions actions={styledVaccineContent.actions} vaccineType={vaccineType} />}
7693
<Overview overview={styledVaccineContent.overviewConclusion} vaccineType={vaccineType} />
7794
</>
7895
)}

src/services/content-api/parsers/custom/covid-19.tsx

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,22 @@ import {
1010
Label,
1111
} from "@src/services/eligibility-api/types";
1212
import { buildNbsUrl } from "@src/services/nbs/nbs-service";
13-
import config from "@src/utils/config";
14-
import { UtcDateTimeFromStringSchema } from "@src/utils/date";
15-
import { logger } from "@src/utils/logger";
16-
import { headers } from "next/headers";
17-
import { Logger } from "pino";
18-
19-
const log: Logger = logger.child({ module: "content-api-parsers-custom-covid-19" });
2013

2114
export const buildFilteredContentForCovid19Vaccine = async (apiContent: string): Promise<VaccinePageContent> => {
22-
const campaigns = await config.CAMPAIGNS;
23-
const now = await _getNow();
24-
2515
const standardFilteredContent = await buildFilteredContentForStandardVaccine(apiContent);
2616

27-
let callout: HeadingWithTypedContent | undefined;
17+
const callout: HeadingWithTypedContent = {
18+
heading: "Booking service closed",
19+
content: [
20+
"You can no longer book a COVID-19 vaccination using this online service",
21+
"Bookings can also no longer be made through the 119 service.",
22+
"COVID-19 vaccinations will be available again in spring.",
23+
].join("\n\n"),
24+
contentType: "markdown",
25+
};
2826
const actions: Action[] = [];
29-
if (campaigns.isActive(VaccineType.COVID_19, now)) {
30-
log.debug({ context: { campaigns, vaccineType: VaccineType.COVID_19, now: now.toISOString() } }, "Campaign active");
31-
callout = undefined;
32-
actions.push(...(await _buildActions()));
33-
} else {
34-
log.debug(
35-
{ context: { campaigns, vaccineType: VaccineType.COVID_19, now: now.toISOString() } },
36-
"No campaign active",
37-
);
38-
callout = {
39-
heading: "Booking service closed",
40-
content: [
41-
"You can no longer book a COVID-19 vaccination using this online service",
42-
"Bookings can also no longer be made through the 119 service.",
43-
"COVID-19 vaccinations will be available again in spring.",
44-
].join("\n\n"),
45-
contentType: "markdown",
46-
};
47-
}
27+
actions.push(...(await _buildActions()));
28+
4829
const recommendation: HeadingWithContent = {
4930
heading: "The COVID-19 vaccine is recommended if you:",
5031
content: [
@@ -57,15 +38,6 @@ export const buildFilteredContentForCovid19Vaccine = async (apiContent: string):
5738
return { ...standardFilteredContent, callout, recommendation, actions };
5839
};
5940

60-
async function _getNow() {
61-
try {
62-
const headersList = await headers();
63-
return UtcDateTimeFromStringSchema.safeParse(headersList.get("x-e2e-datetime")).data ?? new Date();
64-
} catch {
65-
return new Date();
66-
}
67-
}
68-
6941
async function _buildActions(): Promise<Action[]> {
7042
const nbsURl = (await buildNbsUrl(VaccineType.COVID_19)) as ButtonUrl;
7143

0 commit comments

Comments
 (0)