Skip to content

Commit e2e3647

Browse files
VIA-629 SB & EO Add CAMPAIGNS object to config object, along with converter.
1 parent 0454669 commit e2e3647

File tree

5 files changed

+130
-1
lines changed

5 files changed

+130
-1
lines changed

src/services/content-api/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,10 @@ export type ContentApiVaccinationsResponse = {
220220
];
221221
webpage: string;
222222
};
223+
224+
export interface Campaign {
225+
start: Date;
226+
end: Date;
227+
}
228+
229+
export type Campaigns = Record<string, Campaign[]>;

src/utils/config.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,33 @@ describe("lazyConfig", () => {
8686
expect(actual).toBe(99);
8787
});
8888

89+
it("should convert CAMPAIGNS to a Campaigns ", async () => {
90+
setupTestEnvVars("test/");
91+
process.env.CAMPAIGNS = JSON.stringify({
92+
covid: [
93+
{ start: "20251112", end: "20260301" },
94+
{ start: "20260901", end: "20270301" },
95+
],
96+
flu: [
97+
{ start: "20251112", end: "20260301" },
98+
{ start: "20260901", end: "20270301" },
99+
],
100+
});
101+
102+
const actual = await config.CAMPAIGNS;
103+
104+
expect(actual).toStrictEqual({
105+
covid: [
106+
{ start: new Date("2025-11-12"), end: new Date("2026-03-01") },
107+
{ start: new Date("2026-09-01"), end: new Date("2027-03-01") },
108+
],
109+
flu: [
110+
{ start: new Date("2025-11-12"), end: new Date("2026-03-01") },
111+
{ start: new Date("2026-09-01"), end: new Date("2027-03-01") },
112+
],
113+
});
114+
});
115+
89116
it("should throw for invalid URL", async () => {
90117
setupTestEnvVars("test/");
91118
process.env.APIM_AUTH_URL = "not-a-url";
@@ -104,6 +131,26 @@ describe("lazyConfig", () => {
104131
}).rejects.toThrow(ConfigError);
105132
});
106133

134+
it("should throw for invalid CAMPAIGNS", async () => {
135+
setupTestEnvVars("test/");
136+
process.env.CAMPAIGNS = "Sausages";
137+
138+
await expect(async () => {
139+
await config.CAMPAIGNS;
140+
}).rejects.toThrow(ConfigError);
141+
});
142+
143+
it("should throw for CAMPAIGNS with invalid date", async () => {
144+
setupTestEnvVars("test/");
145+
process.env.CAMPAIGNS = JSON.stringify({
146+
covid: [{ start: "20251312", end: "20260301" }],
147+
});
148+
149+
await expect(async () => {
150+
await config.CAMPAIGNS;
151+
}).rejects.toThrow(ConfigError);
152+
});
153+
107154
it("should reuse config values between subsequent calls", async () => {
108155
setupTestEnvVars("test/");
109156
const mockGetSSMParam = (getSecret as jest.Mock).mockImplementation(() => randomString(5));

src/utils/config.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { Campaigns } from "@src/services/content-api/types";
12
import { EligibilityApiError } from "@src/services/eligibility-api/gateway/exceptions";
3+
import { UtcDateFromStringSchema } from "@src/utils/date";
24
import getSecret from "@src/utils/get-secret";
35
import { logger } from "@src/utils/logger";
46
import { retry } from "es-toolkit";
57
import { Logger } from "pino";
68

79
const log: Logger = logger.child({ module: "lazy-config" });
810

9-
export type ConfigValue = string | number | boolean | URL | undefined;
11+
export type ConfigValue = string | number | boolean | URL | Campaigns | undefined;
1012

1113
export interface AppConfig {
1214
// SecretsManager secrets stored as SecureStrings
@@ -32,6 +34,7 @@ export interface AppConfig {
3234
IS_APIM_AUTH_ENABLED: boolean;
3335
APIM_AUTH_URL: URL;
3436
APIM_KEY_ID: string;
37+
CAMPAIGNS: Campaigns;
3538
}
3639

3740
/**
@@ -86,6 +89,29 @@ class Config {
8689
if (lower === "false") return false;
8790
return undefined;
8891
};
92+
private static readonly toCampaigns = (rawValue: string): Campaigns | undefined => {
93+
try {
94+
interface RawCampaign {
95+
start: string;
96+
end: string;
97+
}
98+
99+
const rawObj: Record<string, RawCampaign[]> = JSON.parse(rawValue);
100+
101+
const parsedSchedule: Campaigns = {};
102+
103+
for (const [vaccineName, campaigns] of Object.entries(rawObj)) {
104+
parsedSchedule[vaccineName] = campaigns.map((c) => ({
105+
start: UtcDateFromStringSchema.parse(c.start),
106+
end: UtcDateFromStringSchema.parse(c.end),
107+
}));
108+
}
109+
110+
return parsedSchedule;
111+
} catch {
112+
return undefined;
113+
}
114+
};
89115
static readonly converters: Record<string, (value: string) => ConfigValue> = {
90116
APIM_AUTH_URL: Config.toUrl,
91117
CONTENT_API_ENDPOINT: Config.toUrl,
@@ -97,6 +123,7 @@ class Config {
97123
NHS_APP_REDIRECT_LOGIN_URL: Config.toUrl,
98124
IS_APIM_AUTH_ENABLED: Config.toBoolean,
99125
MAX_SESSION_AGE_MINUTES: Config.toNumber,
126+
CAMPAIGNS: Config.toCampaigns,
100127
};
101128

102129
/**

src/utils/date.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { UtcDateFromStringSchema } from "@src/utils/date";
2+
3+
describe("UtcDateFromStringSchema", () => {
4+
it("should parse winter date", () => {
5+
const actual = UtcDateFromStringSchema.parse("20260101");
6+
7+
expect(actual).toEqual(new Date("2026-01-01"));
8+
});
9+
10+
it("should parse summer date", () => {
11+
const actual = UtcDateFromStringSchema.parse("20260601");
12+
13+
expect(actual).toEqual(new Date("2026-06-01"));
14+
});
15+
16+
it("should throw on invalid date", () => {
17+
const given = "20261301";
18+
19+
expect(() => {
20+
UtcDateFromStringSchema.parse(given);
21+
}).toThrow();
22+
});
23+
24+
it("should throw on badly formed date", () => {
25+
const given = "Sausages";
26+
27+
expect(() => {
28+
UtcDateFromStringSchema.parse(given);
29+
}).toThrow();
30+
});
31+
});

src/utils/date.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { z } from "zod";
2+
3+
export const UtcDateFromStringSchema = z
4+
.string()
5+
.regex(/^\d{8}$/, "Date must be in YYYYMMDD format")
6+
.transform((str, ctx) => {
7+
const year = parseInt(str.slice(0, 4));
8+
const month = parseInt(str.slice(4, 6)); // 1-indexed (01 = Jan)
9+
const day = parseInt(str.slice(6, 8));
10+
const utcTimestamp = Date.UTC(year, month - 1, day);
11+
const date = new Date(utcTimestamp);
12+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
13+
ctx.addIssue("invalid_value");
14+
return z.NEVER;
15+
}
16+
return date;
17+
});

0 commit comments

Comments
 (0)