diff --git a/apps/web/src/app.ts b/apps/web/src/app.ts index 1e092c9df..29d00e6e0 100644 --- a/apps/web/src/app.ts +++ b/apps/web/src/app.ts @@ -10,6 +10,9 @@ import { moduleRoot as civilFamilyCauseListModuleRoot } from "@hmcts/civil-and-f import { moduleRoot as civilDailyCauseListModuleRoot } from "@hmcts/civil-daily-cause-list/config"; import { moduleRoot as civilAppealModuleRoot } from "@hmcts/court-of-appeal-civil-daily-cause-list/config"; import { moduleRoot as familyDailyCauseListModuleRoot } from "@hmcts/family-daily-cause-list/config"; +import { moduleRoot as fttLrtModuleRoot } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list/config"; +import { moduleRoot as fttRptModuleRoot } from "@hmcts/ftt-rpt-weekly-hearing-list/config"; +import { moduleRoot as fttTaxChamberModuleRoot } from "@hmcts/ftt-tax-chamber-weekly-hearing-list/config"; import { moduleRoot as listTypesCommonModuleRoot } from "@hmcts/list-types-common/config"; import { apiRoutes as locationApiRoutes } from "@hmcts/location/config"; import { moduleRoot as londonAdminModuleRoot } from "@hmcts/london-administrative-court-daily-cause-list/config"; @@ -19,6 +22,7 @@ import { moduleRoot as publicPagesModuleRoot } from "@hmcts/public-pages/config"; import { moduleRoot as rcjStandardModuleRoot } from "@hmcts/rcj-standard-daily-cause-list/config"; +import { moduleRoot as siacPoacPaacModuleRoot } from "@hmcts/siac-poac-paac-weekly-hearing-list/config"; import { moduleRoot as sjpPressListModuleRoot } from "@hmcts/sjp-press-list/config"; import { moduleRoot as sjpPublicListModuleRoot } from "@hmcts/sjp-public-list/config"; import { fileUploadRoutes as systemAdminFileUploadRoutes, moduleRoot as systemAdminModuleRoot } from "@hmcts/system-admin-pages/config"; @@ -92,6 +96,10 @@ export async function createApp(): Promise { authModuleRoot, listTypesCommonModuleRoot, careStandardsTribunalModuleRoot, + siacPoacPaacModuleRoot, + fttTaxChamberModuleRoot, + fttLrtModuleRoot, + fttRptModuleRoot, civilFamilyCauseListModuleRoot, civilDailyCauseListModuleRoot, familyDailyCauseListModuleRoot, diff --git a/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/ftt-lands-registration-tribunal-weekly-hearing-list.njk b/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/ftt-lands-registration-tribunal-weekly-hearing-list.njk new file mode 100644 index 000000000..69f7e614d --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/ftt-lands-registration-tribunal-weekly-hearing-list.njk @@ -0,0 +1,76 @@ +{% extends "layouts/base-template.njk" %} + + +{% block page_content %} +
+
+ +

{{ header.listTitle }}

+ +

+ {{ t.factLinkText }} {{ t.factAdditionalText }} +

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+ +
+ + + {{ t.importantInformationTitle }} + + +
+

{{ t.importantInformationText }}

+

+ + {{ t.importantInformationLinkText }} + +

+
+
+ +
+

{{ t.searchCasesTitle }}

+ + +
+ +
+ + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.hearingTime }}{{ t.tableHeaders.caseName }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.judge }}{{ t.tableHeaders.venuePlatform }}
{{ hearing.date }}{{ hearing.hearingTime }}{{ hearing.caseName }}{{ hearing.caseReferenceNumber }}{{ hearing.judge }}{{ hearing.venuePlatform }}
+
+ +

{{ t.dataSource }}: {{ dataSource }}

+ + + +
+
+{% endblock %} diff --git a/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.test.ts b/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.test.ts new file mode 100644 index 000000000..cc0219060 --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.test.ts @@ -0,0 +1,146 @@ +import type { Request, Response } from "express"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockValidate = vi.hoisted(() => vi.fn()); + +vi.mock("@hmcts/list-types-common", () => ({ + createJsonValidator: () => mockValidate, + provenanceLabelsEn: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" }, + provenanceLabelsCy: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" } +})); + +vi.mock("@hmcts/publication", () => ({ + getArtefactById: vi.fn(), + getPublicationJson: vi.fn(), + PROVENANCE_LABELS: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" } +})); + +vi.mock("@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list", () => ({ + fttLrtWeeklyHearingListEn: { + pageTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List", + provenanceLabels: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" } + }, + fttLrtWeeklyHearingListCy: { + pageTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List", + provenanceLabels: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist" } + }, + renderFttLrtData: vi.fn() +})); + +import { renderFttLrtData } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list"; +import { getArtefactById, getPublicationJson } from "@hmcts/publication"; +import { GET } from "./index.js"; + +const MOCK_ARTEFACT = { + artefactId: "test-artefact-123", + listTypeId: 32, + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" +}; + +const MOCK_JSON_DATA = [ + { + date: "01/01/2026", + hearingTime: "10:00am", + caseName: "A Vs B", + caseReferenceNumber: "LRT/00001/2026", + judge: "Judge Smith", + venuePlatform: "London" + } +]; + +describe("FTT Land Registration Tribunal Weekly Hearing List page controller", () => { + let req: Partial; + let res: Partial; + + beforeEach(() => { + vi.clearAllMocks(); + req = { query: {} }; + res = { status: vi.fn().mockReturnThis(), render: vi.fn(), locals: { locale: "en" } }; + }); + + describe("GET handler", () => { + it("should render the list successfully with valid data", async () => { + // Arrange + const mockRenderedData = { + header: { + listTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List", + weekCommencingDate: "1 January 2026", + lastUpdatedDate: "1 January 2026", + lastUpdatedTime: "12pm" + }, + hearings: [] + }; + + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderFttLrtData).mockReturnValue(mockRenderedData as any); + + // Act + await GET(req as Request, res as Response); + + // Assert + expect(getPublicationJson).toHaveBeenCalledWith("test-artefact-123"); + expect(renderFttLrtData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ courtName: "First-tier Tribunal (Land Registration Tribunal)" })); + expect(res.render).toHaveBeenCalledWith("ftt-lands-registration-tribunal-weekly-hearing-list", expect.objectContaining({ dataSource: "Manual Upload" })); + }); + + it("should return 400 when artefactId is missing", async () => { + req.query = {}; + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Bad Request" })); + }); + + it("should return 404 when artefact is not found", async () => { + req.query = { artefactId: "non-existent" }; + vi.mocked(getArtefactById).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 404 when blob is not found", async () => { + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any); + vi.mocked(getPublicationJson).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 500 on server error", async () => { + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockRejectedValue(new Error("Database error")); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Server Error" })); + }); + + it("should render in Welsh when locale is cy", async () => { + // Arrange + res.locals = { locale: "cy" }; + const mockRenderedData = { + header: { listTitle: "title", weekCommencingDate: "", lastUpdatedDate: "", lastUpdatedTime: "" }, + hearings: [] + }; + + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderFttLrtData).mockReturnValue(mockRenderedData as any); + + // Act + await GET(req as Request, res as Response); + + // Assert + expect(renderFttLrtData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ locale: "cy" })); + expect(res.render).toHaveBeenCalledWith( + "ftt-lands-registration-tribunal-weekly-hearing-list", + expect.objectContaining({ dataSource: "Lanlwytho â Llaw" }) + ); + }); + }); +}); diff --git a/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.ts b/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.ts new file mode 100644 index 000000000..f141cca27 --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.ts @@ -0,0 +1,34 @@ +import { + fttLrtWeeklyHearingListCy as cy, + fttLrtWeeklyHearingListEn as en, + type FttLrtHearingList, + renderFttLrtData +} from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list"; +import { schemaPath } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list/config"; +import { createJsonValidator } from "@hmcts/list-types-common"; +import { createSimpleListTypeHandler, resolveDataSource } from "../list-type-handler.js"; + +const validate = createJsonValidator(schemaPath); + +export const GET = createSimpleListTypeHandler({ + en, + cy, + validate, + logPrefix: "ftt-lands-registration-tribunal-weekly-hearing-list", + serverError: { errorTitle: "Server Error", errorMessage: "An error occurred while loading the list" }, + render: ({ artefact, jsonData, locale, res }) => { + const t = locale === "cy" ? cy : en; + + const { header, hearings } = renderFttLrtData(jsonData, { + locale, + courtName: "First-tier Tribunal (Land Registration Tribunal)", + contentDate: artefact.contentDate, + lastReceivedDate: artefact.lastReceivedDate.toISOString(), + listTitle: t.pageTitle + }); + + const dataSource = resolveDataSource(artefact.provenance, t as { provenanceLabels?: Record }); + + res.render("ftt-lands-registration-tribunal-weekly-hearing-list", { en, cy, t, title: header.listTitle, header, hearings, dataSource }); + } +}); diff --git a/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/ftt-rpt-weekly-hearing-list.njk b/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/ftt-rpt-weekly-hearing-list.njk new file mode 100644 index 000000000..5da1848af --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/ftt-rpt-weekly-hearing-list.njk @@ -0,0 +1,82 @@ +{% extends "layouts/base-template.njk" %} + + +{% block page_content %} +
+
+ +

{{ header.listTitle }}

+ +

+ {{ t.factLinkText }} {{ t.factAdditionalText }} +

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+ +
+ + + {{ t.importantInformationTitle }} + + +
+

{{ t.importantInformationText }}

+

+ + {{ t.importantInformationLinkText }} + +

+
+
+ +
+

{{ t.searchCasesTitle }}

+ + +
+ +
+ + + + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.time }}{{ t.tableHeaders.venue }}{{ t.tableHeaders.caseType }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.judges }}{{ t.tableHeaders.members }}{{ t.tableHeaders.hearingMethod }}{{ t.tableHeaders.additionalInformation }}
{{ hearing.date }}{{ hearing.time }}{{ hearing.venue }}{{ hearing.caseType }}{{ hearing.caseReferenceNumber }}{{ hearing.judges }}{{ hearing.members }}{{ hearing.hearingMethod }}{{ hearing.additionalInformation }}
+
+ +

{{ t.dataSource }}: {{ dataSource }}

+ + + +
+
+{% endblock %} diff --git a/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/index.test.ts b/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/index.test.ts new file mode 100644 index 000000000..1a3c62da9 --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/index.test.ts @@ -0,0 +1,228 @@ +import type { Request, Response } from "express"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockValidate = vi.hoisted(() => vi.fn()); + +vi.mock("@hmcts/list-types-common", () => ({ + createJsonValidator: () => mockValidate, + provenanceLabelsEn: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" }, + provenanceLabelsCy: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" } +})); + +vi.mock("@hmcts/publication", () => ({ + getArtefactById: vi.fn(), + getPublicationJson: vi.fn(), + PROVENANCE_LABELS: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" } +})); + +vi.mock("@hmcts/ftt-rpt-weekly-hearing-list", () => ({ + fttRptWeeklyHearingListEn: { + provenanceLabels: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" }, + rptEasternPageTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List", + rptLondonPageTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List", + rptMidlandsPageTitle: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List", + rptNorthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List", + rptSouthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List" + }, + fttRptWeeklyHearingListCy: { + provenanceLabels: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist" }, + rptEasternPageTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List", + rptLondonPageTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List", + rptMidlandsPageTitle: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List", + rptNorthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List", + rptSouthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List" + }, + renderFttRptData: vi.fn() +})); + +import { renderFttRptData } from "@hmcts/ftt-rpt-weekly-hearing-list"; +import { getArtefactById, getPublicationJson } from "@hmcts/publication"; +import { GET } from "./index.js"; + +const REGION_CASES = [ + { + listTypeId: 33, + listTypeName: "FTT_RPT_EASTERN_WEEKLY_HEARING_LIST", + courtName: "First-tier Tribunal (Residential Property Tribunal): Eastern region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List" + }, + { + listTypeId: 34, + listTypeName: "FTT_RPT_LONDON_WEEKLY_HEARING_LIST", + courtName: "First-tier Tribunal (Residential Property Tribunal): London region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List" + }, + { + listTypeId: 35, + listTypeName: "FTT_RPT_MIDLANDS_WEEKLY_HEARING_LIST", + courtName: "First-tier Tribunal (Residential Property Tribunal): Midlands region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List" + }, + { + listTypeId: 36, + listTypeName: "FTT_RPT_NORTHERN_WEEKLY_HEARING_LIST", + courtName: "First-tier Tribunal (Residential Property Tribunal): Northern region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List" + }, + { + listTypeId: 37, + listTypeName: "FTT_RPT_SOUTHERN_WEEKLY_HEARING_LIST", + courtName: "First-tier Tribunal (Residential Property Tribunal): Southern region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List" + } +]; + +const MOCK_JSON_DATA = [ + { + date: "01/01/2026", + time: "10:00am", + venue: "Cambridge", + caseType: "Residential", + caseReferenceNumber: "RPT/00001/2026", + judges: "Judge Smith", + members: "Member A", + hearingMethod: "In person", + additionalInformation: "" + } +]; + +describe("FTT RPT Weekly Hearing List page controller", () => { + let req: Partial; + let res: Partial; + + beforeEach(() => { + vi.clearAllMocks(); + req = { query: {} }; + res = { status: vi.fn().mockReturnThis(), render: vi.fn(), locals: { locale: "en" } }; + }); + + describe("GET handler", () => { + it("should return 400 when artefactId is missing", async () => { + req.query = {}; + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Bad Request" })); + }); + + it("should return 404 when artefact is not found", async () => { + req.query = { artefactId: "non-existent" }; + vi.mocked(getArtefactById).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 400 for an unknown listTypeName", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 99, + listTypeName: "UNKNOWN_LIST_TYPE", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Invalid List Type" })); + }); + + it("should return 404 when blob is not found", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 33, + listTypeName: "FTT_RPT_EASTERN_WEEKLY_HEARING_LIST", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + vi.mocked(getPublicationJson).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 400 when JSON validation fails", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 33, + listTypeName: "FTT_RPT_EASTERN_WEEKLY_HEARING_LIST", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: false, errors: ["Invalid data"] }); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("should return 500 on unexpected server error", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockRejectedValue(new Error("Database error")); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Server Error" })); + }); + + it("should render in Welsh when locale is cy", async () => { + // Arrange + res.locals = { locale: "cy" }; + const mockArtefact = { + artefactId: "test-id", + listTypeId: 33, + listTypeName: "FTT_RPT_EASTERN_WEEKLY_HEARING_LIST", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + }; + vi.mocked(getArtefactById).mockResolvedValue(mockArtefact as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderFttRptData).mockReturnValue({ + header: { listTitle: "title", weekCommencingDate: "", lastUpdatedDate: "", lastUpdatedTime: "" }, + hearings: [] + }); + + // Act + req.query = { artefactId: "test-id" }; + await GET(req as Request, res as Response); + + // Assert + expect(renderFttRptData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ locale: "cy" })); + expect(res.render).toHaveBeenCalledWith("ftt-rpt-weekly-hearing-list", expect.objectContaining({ dataSource: "Lanlwytho â Llaw" })); + }); + + for (const { listTypeId, listTypeName, courtName, listTitle } of REGION_CASES) { + it(`should render correctly for ${listTypeName} (${courtName.split(": ")[1]})`, async () => { + // Arrange + const mockArtefact = { + artefactId: `test-artefact-${listTypeId}`, + listTypeId, + listTypeName, + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + }; + const mockRenderedData = { + header: { listTitle, weekCommencingDate: "1 January 2026", lastUpdatedDate: "1 January 2026", lastUpdatedTime: "12pm" }, + hearings: [] + }; + + req.query = { artefactId: `test-artefact-${listTypeId}` }; + vi.mocked(getArtefactById).mockResolvedValue(mockArtefact as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderFttRptData).mockReturnValue(mockRenderedData); + + // Act + await GET(req as Request, res as Response); + + // Assert + expect(getPublicationJson).toHaveBeenCalledWith(`test-artefact-${listTypeId}`); + expect(renderFttRptData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ courtName })); + expect(res.render).toHaveBeenCalledWith("ftt-rpt-weekly-hearing-list", expect.objectContaining({ dataSource: "Manual Upload" })); + }); + } + }); +}); diff --git a/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/index.ts b/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/index.ts new file mode 100644 index 000000000..7aa832dc6 --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-rpt-weekly-hearing-list/index.ts @@ -0,0 +1,75 @@ +import { fttRptWeeklyHearingListCy as cy, fttRptWeeklyHearingListEn as en, type FttRptHearingList, renderFttRptData } from "@hmcts/ftt-rpt-weekly-hearing-list"; +import { schemaPath } from "@hmcts/ftt-rpt-weekly-hearing-list/config"; +import { createJsonValidator } from "@hmcts/list-types-common"; +import type { Artefact } from "@hmcts/publication"; +import type { Response } from "express"; +import { createSimpleListTypeHandler, resolveDataSource } from "../list-type-handler.js"; + +const validate = createJsonValidator(schemaPath); + +const LIST_TYPE_CONFIG: Record = { + FTT_RPT_EASTERN_WEEKLY_HEARING_LIST: { + courtName: "First-tier Tribunal (Residential Property Tribunal): Eastern region", + enTitle: en.rptEasternPageTitle, + cyTitle: cy.rptEasternPageTitle + }, + FTT_RPT_LONDON_WEEKLY_HEARING_LIST: { + courtName: "First-tier Tribunal (Residential Property Tribunal): London region", + enTitle: en.rptLondonPageTitle, + cyTitle: cy.rptLondonPageTitle + }, + FTT_RPT_MIDLANDS_WEEKLY_HEARING_LIST: { + courtName: "First-tier Tribunal (Residential Property Tribunal): Midlands region", + enTitle: en.rptMidlandsPageTitle, + cyTitle: cy.rptMidlandsPageTitle + }, + FTT_RPT_NORTHERN_WEEKLY_HEARING_LIST: { + courtName: "First-tier Tribunal (Residential Property Tribunal): Northern region", + enTitle: en.rptNorthernPageTitle, + cyTitle: cy.rptNorthernPageTitle + }, + FTT_RPT_SOUTHERN_WEEKLY_HEARING_LIST: { + courtName: "First-tier Tribunal (Residential Property Tribunal): Southern region", + enTitle: en.rptSouthernPageTitle, + cyTitle: cy.rptSouthernPageTitle + } +}; + +function guardArtefact(artefact: Artefact, res: Response): boolean { + if (!artefact.listTypeName || !LIST_TYPE_CONFIG[artefact.listTypeName]) { + res.status(400).render("errors/common", { + en, + cy, + errorTitle: "Invalid List Type", + errorMessage: "This list type is not supported by this module" + }); + return true; + } + return false; +} + +export const GET = createSimpleListTypeHandler({ + en, + cy, + validate, + logPrefix: "ftt-rpt-weekly-hearing-list", + serverError: { errorTitle: "Server Error", errorMessage: "An error occurred while loading the list" }, + guardArtefact, + render: ({ artefact, jsonData, locale, res }) => { + const t = locale === "cy" ? cy : en; + const listTypeConfig = LIST_TYPE_CONFIG[artefact.listTypeName!]; + const listTitle = locale === "cy" ? listTypeConfig.cyTitle : listTypeConfig.enTitle; + + const { header, hearings } = renderFttRptData(jsonData, { + locale, + courtName: listTypeConfig.courtName, + contentDate: artefact.contentDate, + lastReceivedDate: artefact.lastReceivedDate.toISOString(), + listTitle + }); + + const dataSource = resolveDataSource(artefact.provenance, t as { provenanceLabels?: Record }); + + res.render("ftt-rpt-weekly-hearing-list", { en, cy, t, title: header.listTitle, header, hearings, dataSource }); + } +}); diff --git a/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/ftt-tax-chamber-weekly-hearing-list.njk b/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/ftt-tax-chamber-weekly-hearing-list.njk new file mode 100644 index 000000000..020b615c4 --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/ftt-tax-chamber-weekly-hearing-list.njk @@ -0,0 +1,80 @@ +{% extends "layouts/base-template.njk" %} + + +{% block page_content %} +
+
+ +

{{ header.listTitle }}

+ +

+ {{ t.factLinkText }} {{ t.factAdditionalText }} +

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+ +
+ + + {{ t.importantInformationTitle }} + + +
+ {% for paragraph in t.importantInformationParagraphs %} +

{{ paragraph }}

+ {% endfor %} +

+ + {{ t.importantInformationLinkText }} + +

+
+
+ +
+

{{ t.searchCasesTitle }}

+ + +
+ +
+ + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.hearingTime }}{{ t.tableHeaders.caseName }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.judges }}{{ t.tableHeaders.members }}{{ t.tableHeaders.venuePlatform }}
{{ hearing.date }}{{ hearing.hearingTime }}{{ hearing.caseName }}{{ hearing.caseReferenceNumber }}{{ hearing.judges }}{{ hearing.members }}{{ hearing.venuePlatform }}
+
+ +

{{ t.dataSource }}: {{ dataSource }}

+ + + +
+
+{% endblock %} diff --git a/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.test.ts b/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.test.ts new file mode 100644 index 000000000..00d76ea88 --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.test.ts @@ -0,0 +1,153 @@ +import type { Request, Response } from "express"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockValidate = vi.hoisted(() => vi.fn()); + +vi.mock("@hmcts/list-types-common", () => ({ + createJsonValidator: () => mockValidate, + provenanceLabelsEn: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" }, + provenanceLabelsCy: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" } +})); + +vi.mock("@hmcts/publication", () => ({ + getArtefactById: vi.fn(), + getPublicationJson: vi.fn(), + PROVENANCE_LABELS: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" } +})); + +vi.mock("@hmcts/ftt-tax-chamber-weekly-hearing-list", () => ({ + fttTaxChamberWeeklyHearingListEn: { + pageTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + provenanceLabels: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" } + }, + fttTaxChamberWeeklyHearingListCy: { + pageTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + provenanceLabels: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist" } + }, + renderFttTaxChamberData: vi.fn() +})); + +import { renderFttTaxChamberData } from "@hmcts/ftt-tax-chamber-weekly-hearing-list"; +import { getArtefactById, getPublicationJson } from "@hmcts/publication"; +import { GET } from "./index.js"; + +const MOCK_ARTEFACT = { + artefactId: "test-artefact-123", + listTypeId: 31, + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" +}; + +const MOCK_JSON_DATA = [ + { + date: "01/01/2026", + hearingTime: "10:00am", + caseName: "A Vs HMRC", + caseReferenceNumber: "TC/00001/2026", + judges: "Judge Smith", + members: "", + venuePlatform: "London" + } +]; + +describe("FTT Tax Chamber Weekly Hearing List page controller", () => { + let req: Partial; + let res: Partial; + + beforeEach(() => { + vi.clearAllMocks(); + req = { query: {} }; + res = { status: vi.fn().mockReturnThis(), render: vi.fn(), locals: { locale: "en" } }; + }); + + describe("GET handler", () => { + it("should render the list successfully with valid data", async () => { + // Arrange + const mockRenderedData = { + header: { + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + weekCommencingDate: "1 January 2026", + lastUpdatedDate: "1 January 2026", + lastUpdatedTime: "12pm" + }, + hearings: [] + }; + + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderFttTaxChamberData).mockReturnValue(mockRenderedData as any); + + // Act + await GET(req as Request, res as Response); + + // Assert + expect(getPublicationJson).toHaveBeenCalledWith("test-artefact-123"); + expect(renderFttTaxChamberData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ courtName: "First-tier Tribunal (Tax Chamber)" })); + expect(res.render).toHaveBeenCalledWith("ftt-tax-chamber-weekly-hearing-list", expect.objectContaining({ dataSource: "Manual Upload" })); + }); + + it("should return 400 when artefactId is missing", async () => { + req.query = {}; + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Bad Request" })); + }); + + it("should return 404 when artefact is not found", async () => { + req.query = { artefactId: "non-existent" }; + vi.mocked(getArtefactById).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 404 when blob is not found", async () => { + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any); + vi.mocked(getPublicationJson).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 400 when JSON validation fails", async () => { + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: false, errors: ["Invalid date format"] }); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("should return 500 on server error", async () => { + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockRejectedValue(new Error("Database error")); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Server Error" })); + }); + + it("should render in Welsh when locale is cy", async () => { + // Arrange + res.locals = { locale: "cy" }; + const mockRenderedData = { + header: { listTitle: "title", weekCommencingDate: "", lastUpdatedDate: "", lastUpdatedTime: "" }, + hearings: [] + }; + + req.query = { artefactId: "test-artefact-123" }; + vi.mocked(getArtefactById).mockResolvedValue(MOCK_ARTEFACT as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderFttTaxChamberData).mockReturnValue(mockRenderedData as any); + + // Act + await GET(req as Request, res as Response); + + // Assert + expect(renderFttTaxChamberData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ locale: "cy" })); + expect(res.render).toHaveBeenCalledWith("ftt-tax-chamber-weekly-hearing-list", expect.objectContaining({ dataSource: "Lanlwytho â Llaw" })); + }); + }); +}); diff --git a/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.ts b/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.ts new file mode 100644 index 000000000..1eef17291 --- /dev/null +++ b/apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.ts @@ -0,0 +1,34 @@ +import { + fttTaxChamberWeeklyHearingListCy as cy, + fttTaxChamberWeeklyHearingListEn as en, + type FttTaxChamberHearingList, + renderFttTaxChamberData +} from "@hmcts/ftt-tax-chamber-weekly-hearing-list"; +import { schemaPath } from "@hmcts/ftt-tax-chamber-weekly-hearing-list/config"; +import { createJsonValidator } from "@hmcts/list-types-common"; +import { createSimpleListTypeHandler, resolveDataSource } from "../list-type-handler.js"; + +const validate = createJsonValidator(schemaPath); + +export const GET = createSimpleListTypeHandler({ + en, + cy, + validate, + logPrefix: "ftt-tax-chamber-weekly-hearing-list", + serverError: { errorTitle: "Server Error", errorMessage: "An error occurred while loading the list" }, + render: ({ artefact, jsonData, locale, res }) => { + const t = locale === "cy" ? cy : en; + + const { header, hearings } = renderFttTaxChamberData(jsonData, { + locale, + courtName: "First-tier Tribunal (Tax Chamber)", + contentDate: artefact.contentDate, + lastReceivedDate: artefact.lastReceivedDate.toISOString(), + listTitle: t.pageTitle + }); + + const dataSource = resolveDataSource(artefact.provenance, t as { provenanceLabels?: Record }); + + res.render("ftt-tax-chamber-weekly-hearing-list", { en, cy, t, title: header.listTitle, header, hearings, dataSource }); + } +}); diff --git a/apps/web/src/pages/(list-types)/siac-poac-paac-weekly-hearing-list/index.test.ts b/apps/web/src/pages/(list-types)/siac-poac-paac-weekly-hearing-list/index.test.ts new file mode 100644 index 000000000..b9f66739c --- /dev/null +++ b/apps/web/src/pages/(list-types)/siac-poac-paac-weekly-hearing-list/index.test.ts @@ -0,0 +1,246 @@ +import type { Request, Response } from "express"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockValidate = vi.hoisted(() => vi.fn()); + +vi.mock("@hmcts/list-types-common", () => ({ + createJsonValidator: () => mockValidate, + provenanceLabelsEn: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" }, + provenanceLabelsCy: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist", COMMON_PLATFORM: "Common Platform", CP_CATH: "Libra", PDDA: "PDDA" } +})); + +vi.mock("@hmcts/publication", () => ({ + getArtefactById: vi.fn(), + getPublicationJson: vi.fn(), + PROVENANCE_LABELS: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" } +})); + +vi.mock("@hmcts/siac-poac-paac-weekly-hearing-list", () => ({ + siacPoacPaacWeeklyHearingListEn: { + siacPageTitle: "Special Immigration Appeals Commission Weekly Hearing List", + poacPageTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List", + paacPageTitle: "Pathogens Access Appeal Commission Weekly Hearing List", + provenanceLabels: { MANUAL_UPLOAD: "Manual Upload", SNL: "ListAssist" }, + importantInformationText: "The tribunal sometimes uses reference numbers or initials to protect the anonymity of those involved in the appeal.", + importantInformationVenue: "All hearings take place at Field House, 15-25 Bream's Buildings, London EC4A 1DZ." + }, + siacPoacPaacWeeklyHearingListCy: { + siacPageTitle: "Special Immigration Appeals Commission Weekly Hearing List", + poacPageTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List", + paacPageTitle: "Pathogens Access Appeal Commission Weekly Hearing List", + provenanceLabels: { MANUAL_UPLOAD: "Lanlwytho â Llaw", SNL: "ListAssist" }, + importantInformationText: "The tribunal sometimes uses reference numbers or initials to protect the anonymity of those involved in the appeal.", + importantInformationVenue: "All hearings take place at Field House, 15-25 Bream's Buildings, London EC4A 1DZ." + }, + renderSiacPoacPaacData: vi.fn() +})); + +import { getArtefactById, getPublicationJson } from "@hmcts/publication"; +import { renderSiacPoacPaacData } from "@hmcts/siac-poac-paac-weekly-hearing-list"; +import { GET } from "./index.js"; + +const LIST_TYPE_CASES = [ + { + listTypeId: 28, + listTypeName: "SIAC_WEEKLY_HEARING_LIST", + courtName: "Special Immigration Appeals Commission", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }, + { + listTypeId: 29, + listTypeName: "POAC_WEEKLY_HEARING_LIST", + courtName: "Proscribed Organisations Appeal Commission", + listTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List" + }, + { + listTypeId: 30, + listTypeName: "PAAC_WEEKLY_HEARING_LIST", + courtName: "Pathogens Access Appeal Commission", + listTitle: "Pathogens Access Appeal Commission Weekly Hearing List" + } +]; + +const MOCK_JSON_DATA = [ + { + date: "01/01/2026", + time: "10:00am", + appellant: "Smith v Secretary of State", + caseReferenceNumber: "SC/00001/2026", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "" + } +]; + +describe("SIAC/POAC/PAAC Weekly Hearing List page controller", () => { + let req: Partial; + let res: Partial; + + beforeEach(() => { + vi.clearAllMocks(); + req = { query: {} }; + res = { status: vi.fn().mockReturnThis(), render: vi.fn(), locals: { locale: "en" } }; + }); + + describe("GET handler", () => { + it("should return 400 when artefactId is missing", async () => { + req.query = {}; + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Bad Request" })); + }); + + it("should return 404 when artefact is not found", async () => { + req.query = { artefactId: "non-existent" }; + vi.mocked(getArtefactById).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 400 for an unknown listTypeName", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 99, + listTypeName: "UNKNOWN_LIST_TYPE", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Invalid List Type" })); + }); + + it("should return 404 when blob is not found", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 28, + listTypeName: "SIAC_WEEKLY_HEARING_LIST", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + vi.mocked(getPublicationJson).mockResolvedValue(null); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("should return 400 when JSON validation fails", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 28, + listTypeName: "SIAC_WEEKLY_HEARING_LIST", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: false, errors: ["Invalid data"] }); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("should return 500 on unexpected server error", async () => { + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockRejectedValue(new Error("Database error")); + await GET(req as Request, res as Response); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.render).toHaveBeenCalledWith("errors/common", expect.objectContaining({ errorTitle: "Server Error" })); + }); + + it("should render in Welsh when locale is cy", async () => { + // Arrange + res.locals = { locale: "cy" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 28, + listTypeName: "SIAC_WEEKLY_HEARING_LIST", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderSiacPoacPaacData).mockReturnValue({ + header: { listTitle: "title", weekCommencingDate: "", lastUpdatedDate: "", lastUpdatedTime: "" }, + hearings: [] + } as any); + + // Act + req.query = { artefactId: "test-id" }; + await GET(req as Request, res as Response); + + // Assert + expect(renderSiacPoacPaacData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ locale: "cy" })); + expect(res.render).toHaveBeenCalledWith("siac-poac-paac-weekly-hearing-list", expect.objectContaining({ dataSource: "Lanlwytho â Llaw" })); + }); + + it("should pass importantInformationText and importantInformationVenue as separate fields to the template", async () => { + // Arrange + req.query = { artefactId: "test-id" }; + vi.mocked(getArtefactById).mockResolvedValue({ + artefactId: "test-id", + listTypeId: 28, + listTypeName: "SIAC_WEEKLY_HEARING_LIST", + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + } as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderSiacPoacPaacData).mockReturnValue({ + header: { listTitle: "title", weekCommencingDate: "", lastUpdatedDate: "", lastUpdatedTime: "" }, + hearings: [] + } as any); + + // Act + await GET(req as Request, res as Response); + + // Assert + expect(res.render).toHaveBeenCalledWith( + "siac-poac-paac-weekly-hearing-list", + expect.objectContaining({ + en: expect.objectContaining({ + importantInformationText: expect.not.stringContaining("Field House"), + importantInformationVenue: "All hearings take place at Field House, 15-25 Bream's Buildings, London EC4A 1DZ." + }) + }) + ); + }); + + for (const { listTypeId, listTypeName, courtName, listTitle } of LIST_TYPE_CASES) { + it(`should render correctly for ${listTypeName} (${courtName})`, async () => { + // Arrange + const mockArtefact = { + artefactId: `test-artefact-${listTypeId}`, + listTypeId, + listTypeName, + contentDate: new Date("2026-01-01"), + lastReceivedDate: new Date("2026-01-01T12:00:00Z"), + provenance: "MANUAL_UPLOAD" + }; + const mockRenderedData = { + header: { listTitle, weekCommencingDate: "1 January 2026", lastUpdatedDate: "1 January 2026", lastUpdatedTime: "12pm" }, + hearings: [] + }; + + req.query = { artefactId: `test-artefact-${listTypeId}` }; + vi.mocked(getArtefactById).mockResolvedValue(mockArtefact as any); + vi.mocked(getPublicationJson).mockResolvedValue(MOCK_JSON_DATA); + mockValidate.mockReturnValue({ isValid: true, errors: [] }); + vi.mocked(renderSiacPoacPaacData).mockReturnValue(mockRenderedData as any); + + // Act + await GET(req as Request, res as Response); + + // Assert + expect(getPublicationJson).toHaveBeenCalledWith(`test-artefact-${listTypeId}`); + expect(renderSiacPoacPaacData).toHaveBeenCalledWith(MOCK_JSON_DATA, expect.objectContaining({ courtName })); + expect(res.render).toHaveBeenCalledWith("siac-poac-paac-weekly-hearing-list", expect.objectContaining({ dataSource: "Manual Upload" })); + }); + } + }); +}); diff --git a/apps/web/src/pages/(list-types)/siac-poac-paac-weekly-hearing-list/index.ts b/apps/web/src/pages/(list-types)/siac-poac-paac-weekly-hearing-list/index.ts new file mode 100644 index 000000000..b5bd436eb --- /dev/null +++ b/apps/web/src/pages/(list-types)/siac-poac-paac-weekly-hearing-list/index.ts @@ -0,0 +1,70 @@ +import { createJsonValidator } from "@hmcts/list-types-common"; +import type { Artefact } from "@hmcts/publication"; +import { + siacPoacPaacWeeklyHearingListCy as cy, + siacPoacPaacWeeklyHearingListEn as en, + renderSiacPoacPaacData, + type SiacPoacPaacHearingList +} from "@hmcts/siac-poac-paac-weekly-hearing-list"; +import { schemaPath } from "@hmcts/siac-poac-paac-weekly-hearing-list/config"; +import type { Response } from "express"; +import { createSimpleListTypeHandler, resolveDataSource } from "../list-type-handler.js"; + +const validate = createJsonValidator(schemaPath); + +const LIST_TYPE_CONFIG: Record = { + SIAC_WEEKLY_HEARING_LIST: { + courtName: "Special Immigration Appeals Commission", + enTitle: en.siacPageTitle, + cyTitle: cy.siacPageTitle + }, + POAC_WEEKLY_HEARING_LIST: { + courtName: "Proscribed Organisations Appeal Commission", + enTitle: en.poacPageTitle, + cyTitle: cy.poacPageTitle + }, + PAAC_WEEKLY_HEARING_LIST: { + courtName: "Pathogens Access Appeal Commission", + enTitle: en.paacPageTitle, + cyTitle: cy.paacPageTitle + } +}; + +function guardArtefact(artefact: Artefact, res: Response): boolean { + if (!artefact.listTypeName || !LIST_TYPE_CONFIG[artefact.listTypeName]) { + res.status(400).render("errors/common", { + en, + cy, + errorTitle: "Invalid List Type", + errorMessage: "This list type is not supported by this module" + }); + return true; + } + return false; +} + +export const GET = createSimpleListTypeHandler({ + en, + cy, + validate, + logPrefix: "siac-poac-paac-weekly-hearing-list", + serverError: { errorTitle: "Server Error", errorMessage: "An error occurred while loading the list" }, + guardArtefact, + render: ({ artefact, jsonData, locale, res }) => { + const t = locale === "cy" ? cy : en; + const listTypeConfig = LIST_TYPE_CONFIG[artefact.listTypeName!]; + const listTitle = locale === "cy" ? listTypeConfig.cyTitle : listTypeConfig.enTitle; + + const { header, hearings } = renderSiacPoacPaacData(jsonData, { + locale, + courtName: listTypeConfig.courtName, + contentDate: artefact.contentDate, + lastReceivedDate: artefact.lastReceivedDate.toISOString(), + listTitle + }); + + const dataSource = resolveDataSource(artefact.provenance, t as { provenanceLabels?: Record }); + + res.render("siac-poac-paac-weekly-hearing-list", { en, cy, t, title: header.listTitle, header, hearings, dataSource }); + } +}); diff --git a/biome.json b/biome.json index 359e8563d..e0219820d 100644 --- a/biome.json +++ b/biome.json @@ -18,7 +18,8 @@ "!**/.turbo", "!**/storage", "!**/generated", - "!e2e-tests/playwright-report" + "!e2e-tests/playwright-report", + "!**/.claude" ] }, "formatter": { diff --git a/docs/tickets/428/plan.md b/docs/tickets/428/plan.md new file mode 100644 index 000000000..fe62d0892 --- /dev/null +++ b/docs/tickets/428/plan.md @@ -0,0 +1,368 @@ +# Technical Plan: Issue #428 — Non-strategic list types for SIAC, POAC, PAAC, FTT Tax, FTT LRT and FTT RPT + +## 1. Technical Approach + +All 10 new list types follow the identical pattern established by `libs/list-types/care-standards-tribunal-weekly-hearing-list`. Each list type becomes an independent workspace package under `libs/list-types/`. The page controller and Nunjucks template live in `apps/web/src/pages/(list-types)/`. + +### Reference module: `@hmcts/care-standards-tribunal-weekly-hearing-list` + +Every new module replicates this exact file structure: + +``` +libs/list-types// + package.json + tsconfig.json + src/ + config.ts # moduleRoot, assets, schemaPath + index.ts # re-exports: locales, model types, renderer, PDF generator, email summary + conversion/-config.ts # Excel field config, registerConverter(id), registerConverterByName(name) + email-summary/summary-builder.ts # extractCaseSummary + email-summary/summary-builder.test.ts + locales/en.ts + locales/cy.ts + models/types.ts # hearing interface + list type alias + pdf/pdf-generator.ts + pdf/pdf-generator.test.ts + pdf/pdf-template.njk + rendering/renderer.ts + rendering/renderer.test.ts + schemas/.json # JSON Schema (draft-07) + +apps/web/src/pages/(list-types)// + index.ts # GET controller (mirrors CST controller exactly) + index.test.ts + .njk # HTML template +``` + +Because all 10 lists share the same pattern, the only differences between modules are: +- Field names and headers (driving the model, schema, converter config, renderer, and template) +- Important-information accordion content (locale strings) +- Converter registration IDs and names +- `courtName` string passed to the renderer +- Email summary fields (always: Date, Time, Case Reference Number) + +### SIAC / POAC / PAAC — shared field set + +All three share the same 7 fields: + +| Excel header | Field name | Required | Validation | +|---|---|---|---| +| Date | `date` | yes | `DD_MM_YYYY_PATTERN` | +| Time | `time` | yes | no-HTML | +| Appellant | `appellant` | yes | no-HTML | +| Case Reference Number | `caseReferenceNumber` | yes | no-HTML | +| Hearing Type | `hearingType` | yes | no-HTML | +| Courtroom | `courtroom` | yes | no-HTML | +| Additional information | `additionalInformation` | yes | no-HTML | + +Because the field sets are identical across SIAC, POAC, and PAAC, a single Excel converter config object can be created and reused by all three converters (registered under different IDs and names). + +### FTT Tax Chamber — field set + +| Excel header | Field name | Required | Validation | +|---|---|---|---| +| Date | `date` | yes | `DD_MM_YYYY_PATTERN` | +| Hearing Time | `hearingTime` | yes | no-HTML | +| Case Name | `caseName` | yes | no-HTML | +| Case Reference Number | `caseReferenceNumber` | yes | no-HTML | +| Judge(s) | `judges` | yes | no-HTML | +| Member(s) | `members` | yes | no-HTML | +| Venue/Platform | `venuePlatform` | yes | no-HTML | + +### FTT LRT (Lands Registration Tribunal) — field set + +| Excel header | Field name | Required | Validation | +|---|---|---|---| +| Date | `date` | yes | `DD_MM_YYYY_PATTERN` | +| Hearing Time | `hearingTime` | yes | no-HTML | +| Case Name | `caseName` | yes | no-HTML | +| Case Reference Number | `caseReferenceNumber` | yes | no-HTML | +| Judge | `judge` | yes | no-HTML | +| Venue/Platform | `venuePlatform` | yes | no-HTML | + +### FTT RPT (Residential and Property Tribunal) — field set + +Identical across all 5 regional variants: + +| Excel header | Field name | Required | Validation | +|---|---|---|---| +| Date | `date` | yes | `DD_MM_YYYY_PATTERN` | +| Time | `time` | yes | no-HTML | +| Venue | `venue` | yes | no-HTML | +| Case Type | `caseType` | yes | no-HTML | +| Case Reference Number | `caseReferenceNumber` | yes | no-HTML | +| Judge(s) | `judges` | yes | no-HTML | +| Member(s) | `members` | yes | no-HTML | +| Hearing Method | `hearingMethod` | yes | no-HTML | +| Additional Information | `additionalInformation` | yes | no-HTML | + +The same Excel converter config can be reused across all 5 RPT regional variants (registered under different IDs and names). + +--- + +## 2. List Type Registry + +Ten new entries are appended to `libs/location/src/list-type-data.ts`. The current highest ID in that file is 27 (`SJP_DELTA_PUBLIC_LIST`). The canonical IDs for the new types are 28–37 (the IDs 24–33 in the ticket comment refer to the database auto-increment at seed time; the canonical registry-level IDs assigned here are what matters for `registerConverter`). + +**Note:** The ticket's spec comment (which was machine-generated) assigns IDs 24–33, but IDs 24–27 are already taken in `list-type-data.ts` by the SJP list types. The next available IDs in `list-type-data.ts` start at **28**. The `registerConverter(id, ...)` call in each `-config.ts` file must match the `id` field in `list-type-data.ts`. + +| id | name | englishFriendlyName | shortenedFriendlyName | urlPath | provenance | isNonStrategic | defaultSensitivity | subJurisdictionIds | +|----|------|---------------------|----------------------|---------|------------|----------------|-------------------|--------------------| +| 28 | `SIAC_WEEKLY_HEARING_LIST` | Special Immigration Appeals Commission Weekly Hearing List | SIAC Weekly Hearing List | `siac-weekly-hearing-list` | `MANUAL_UPLOAD` | true | TBD (see open questions) | [25] | +| 29 | `POAC_WEEKLY_HEARING_LIST` | Proscribed Organisations Appeal Commission Weekly Hearing List | POAC Weekly Hearing List | `poac-weekly-hearing-list` | `MANUAL_UPLOAD` | true | TBD | [23] | +| 30 | `PAAC_WEEKLY_HEARING_LIST` | Pathogens Access Appeal Commission Weekly Hearing List | PACC Weekly Hearing List | `paac-weekly-hearing-list` | `MANUAL_UPLOAD` | true | TBD | [21] | +| 31 | `FTT_TAX_CHAMBER_WEEKLY_HEARING_LIST` | First-tier Tribunal (Tax Chamber) Weekly Hearing List | FFT Tax Weekly Hearing List | `ftt-tax-chamber-weekly-hearing-list` | `MANUAL_UPLOAD` | true | `Public` | [16] | +| 32 | `FTT_LANDS_REGISTRATION_TRIBUNAL_WEEKLY_HEARING_LIST` | First-tier Tribunal (Lands Registration Tribunal) Weekly Hearing List | FFT (LR) Weekly Hearing List | `ftt-lands-registration-tribunal-weekly-hearing-list` | `MANUAL_UPLOAD` | true | `Public` | [15] | +| 33 | `FTT_RPT_EASTERN_WEEKLY_HEARING_LIST` | First-tier Tribunal (Residential and Property Tribunal) Eastern Region Weekly Hearing List | RPT Eastern Weekly Hearing List | `ftt-rpt-eastern-weekly-hearing-list` | `MANUAL_UPLOAD` | true | `Public` | [24] | +| 34 | `FTT_RPT_LONDON_WEEKLY_HEARING_LIST` | First-tier Tribunal (Residential and Property Tribunal) London Region Weekly Hearing List | RPT London Weekly Hearing List | `ftt-rpt-london-weekly-hearing-list` | `MANUAL_UPLOAD` | true | `Public` | [24] | +| 35 | `FTT_RPT_MIDLANDS_WEEKLY_HEARING_LIST` | First-tier Tribunal (Residential and Property Tribunal) Midlands Region Weekly Hearing List | RPT Midlands Weekly Hearing List | `ftt-rpt-midlands-weekly-hearing-list` | `MANUAL_UPLOAD` | true | `Public` | [24] | +| 36 | `FTT_RPT_NORTHERN_WEEKLY_HEARING_LIST` | First-tier Tribunal (Residential and Property Tribunal) Northern Region Weekly Hearing List | RPT Northern Weekly Hearing List | `ftt-rpt-northern-weekly-hearing-list` | `MANUAL_UPLOAD` | true | `Public` | [24] | +| 37 | `FTT_RPT_SOUTHERN_WEEKLY_HEARING_LIST` | First-tier Tribunal (Residential and Property Tribunal) Southern Region Weekly Hearing List | RPT Southern Weekly Hearing List | `ftt-rpt-southern-weekly-hearing-list` | `MANUAL_UPLOAD` | true | `Public` | [24] | + +SubJurisdiction IDs come from `libs/location/src/location-data.ts`: +- 25 = Special Immigration Appeals Commission +- 23 = Proscribed Organisations Appeal Commission +- 21 = Pathogens Access Appeal Commission +- 16 = First-Tier Tribunal (Tax Chamber) +- 15 = First-Tier Tribunal (Lands Registration Tribunal) +- 24 = Residential Property Tribunal (used for all 5 RPT regional variants) + +Welsh friendly names are placeholders (English text) pending translation — see open questions. + +--- + +## 3. New Module Packages + +### 3a. `@hmcts/siac-poac-paac-weekly-hearing-list` + +Because SIAC, POAC, and PAAC share identical field definitions and page layout, they can share a **single lib package**. The single converter config is registered three times (once per list type ID and name). Three separate page controllers and templates are created in `apps/web/src/pages/(list-types)/` to give each list type its own URL and locale content. + +**Module path:** `libs/list-types/siac-poac-paac-weekly-hearing-list/` + +Key files: +- `src/models/types.ts` — `SiacPoacPaacHearing` interface (7 fields), `SiacPoacPaacHearingList` type alias +- `src/conversion/siac-poac-paac-config.ts` — single `SIAC_POAC_PAAC_EXCEL_CONFIG`, three `registerConverter` / `registerConverterByName` calls +- `src/rendering/renderer.ts` — `renderSiacPoacPaacData(list, options): RenderedData` +- `src/email-summary/summary-builder.ts` — `extractCaseSummary` returns `[Date, Time, Case Reference Number]` +- `src/pdf/pdf-generator.ts` — `generateSiacPoacPaacWeeklyHearingListPdf(options)`; accepts a `courtName` option so the same generator serves all three courts +- `src/pdf/pdf-template.njk` — shared PDF template (7-column table) +- `src/locales/en.ts` — shared label strings (table headers, search label, common copy) +- `src/locales/cy.ts` — Welsh placeholders (English text until translations are provided) +- `src/schemas/siac-poac-paac-weekly-hearing-list.json` — JSON Schema for the shared field set + +Three locale variants for the important-information accordion are stored as named exports within the locales files, keyed by tribunal abbreviation (`siacImportantInfo`, `poacImportantInfo`, `paacImportantInfo`), so each page controller can select the correct text without importing from three separate modules. + +**Page controllers and templates in `apps/web/src/pages/(list-types)/`:** +- `siac-weekly-hearing-list/index.ts` + `siac-weekly-hearing-list.njk` +- `poac-weekly-hearing-list/index.ts` + `poac-weekly-hearing-list.njk` +- `paac-weekly-hearing-list/index.ts` + `paac-weekly-hearing-list.njk` + +### 3b. `@hmcts/ftt-tax-chamber-weekly-hearing-list` + +**Module path:** `libs/list-types/ftt-tax-chamber-weekly-hearing-list/` + +Key differences from CST: +- 7-column table (Date, Hearing Time, Case Name, Case Reference Number, Judge(s), Member(s), Venue/Platform) +- Multi-paragraph important-information accordion with external link +- `extractCaseSummary` returns `[Date, Hearing Time, Case Reference Number]` + +**Page:** `apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/` + +### 3c. `@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list` + +**Module path:** `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/` + +Key differences: +- 6-column table (Date, Hearing Time, Case Name, Case Reference Number, Judge, Venue/Platform) +- Important-information accordion contains a placeholder office email (`[insert office email]`) +- `extractCaseSummary` returns `[Date, Hearing Time, Case Reference Number]` + +**Page:** `apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/` + +### 3d. `@hmcts/ftt-rpt-weekly-hearing-list` + +Because all 5 RPT regional variants share the same field definitions, page layout, and important-information text, they share a **single lib package** — the same strategy used for SIAC/POAC/PAAC. + +**Module path:** `libs/list-types/ftt-rpt-weekly-hearing-list/` + +Key details: +- `src/conversion/ftt-rpt-config.ts` — single `FTT_RPT_EXCEL_CONFIG`, five `registerConverter` / `registerConverterByName` calls +- Important-information accordion is identical across all 5 regions (same placeholder email) +- `extractCaseSummary` returns `[Date, Time, Case Reference Number]` +- 9-column table (Date, Time, Venue, Case Type, Case Reference Number, Judge(s), Member(s), Hearing Method, Additional Information) + +**Page controllers and templates in `apps/web/src/pages/(list-types)/`:** +- `ftt-rpt-eastern-weekly-hearing-list/` +- `ftt-rpt-london-weekly-hearing-list/` +- `ftt-rpt-midlands-weekly-hearing-list/` +- `ftt-rpt-northern-weekly-hearing-list/` +- `ftt-rpt-southern-weekly-hearing-list/` + +--- + +## 4. Files to Create Per Module + +The table below lists the source files to create. Build output (`dist/`) is generated — not created manually. + +### `@hmcts/siac-poac-paac-weekly-hearing-list` + +| File | Notes | +|------|-------| +| `libs/list-types/siac-poac-paac-weekly-hearing-list/package.json` | Same scripts as CST; deps: `@hmcts/list-types-common`, `@hmcts/pdf-generation`, `@hmcts/postgres-prisma`, `luxon`, `nunjucks` | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/tsconfig.json` | Extends `../../../tsconfig.json`; `resolveJsonModule: true` | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.ts` | `moduleRoot`, `assets`, `schemaPath` | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/index.ts` | Re-exports from all sub-modules | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/conversion/siac-poac-paac-config.ts` | Field config + 3x register calls | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/models/types.ts` | 7-field interface + list type | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/en.ts` | Shared labels + 3 accordion text variants | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/cy.ts` | Welsh placeholders | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.ts` | `renderSiacPoacPaacData` | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.test.ts` | | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.ts` | Date + Time + Case Reference Number | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.test.ts` | | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.ts` | Accepts `courtName` param | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.test.ts` | | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-template.njk` | 7-column PDF table | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/schemas/siac-poac-paac-weekly-hearing-list.json` | JSON Schema | +| `libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.test.ts` | moduleRoot + assets checks | +| `apps/web/src/pages/(list-types)/siac-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/siac-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/siac-weekly-hearing-list/siac-weekly-hearing-list.njk` | | +| `apps/web/src/pages/(list-types)/poac-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/poac-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/poac-weekly-hearing-list/poac-weekly-hearing-list.njk` | | +| `apps/web/src/pages/(list-types)/paac-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/paac-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/paac-weekly-hearing-list/paac-weekly-hearing-list.njk` | | + +### `@hmcts/ftt-tax-chamber-weekly-hearing-list` + +| File | Notes | +|------|-------| +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/package.json` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/tsconfig.json` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/index.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/conversion/ftt-tax-config.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/models/types.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/en.ts` | Multi-paragraph accordion, external link | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/cy.ts` | Placeholder | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.test.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.test.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.test.ts` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-template.njk` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/schemas/ftt-tax-chamber-weekly-hearing-list.json` | | +| `libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-tax-chamber-weekly-hearing-list/ftt-tax-chamber-weekly-hearing-list.njk` | | + +### `@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list` + +| File | Notes | +|------|-------| +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/package.json` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/tsconfig.json` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/config.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/index.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/conversion/ftt-lrt-config.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/models/types.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/locales/en.ts` | Placeholder office email in accordion | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/locales/cy.ts` | Placeholder | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.test.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/email-summary/summary-builder.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/email-summary/summary-builder.test.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/pdf/pdf-generator.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/pdf/pdf-generator.test.ts` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/pdf/pdf-template.njk` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/schemas/ftt-lands-registration-tribunal-weekly-hearing-list.json` | | +| `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/config.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-lands-registration-tribunal-weekly-hearing-list/ftt-lands-registration-tribunal-weekly-hearing-list.njk` | | + +### `@hmcts/ftt-rpt-weekly-hearing-list` + +| File | Notes | +|------|-------| +| `libs/list-types/ftt-rpt-weekly-hearing-list/package.json` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/tsconfig.json` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/config.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/index.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/conversion/ftt-rpt-config.ts` | 5x register calls | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/models/types.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/en.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/cy.ts` | Placeholder | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.test.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.test.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.ts` | Accepts `regionName` + `courtName` | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.test.ts` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-template.njk` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/schemas/ftt-rpt-weekly-hearing-list.json` | | +| `libs/list-types/ftt-rpt-weekly-hearing-list/src/config.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-eastern-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-eastern-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-eastern-weekly-hearing-list/ftt-rpt-eastern-weekly-hearing-list.njk` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-london-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-london-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-london-weekly-hearing-list/ftt-rpt-london-weekly-hearing-list.njk` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-midlands-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-midlands-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-midlands-weekly-hearing-list/ftt-rpt-midlands-weekly-hearing-list.njk` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-northern-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-northern-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-northern-weekly-hearing-list/ftt-rpt-northern-weekly-hearing-list.njk` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-southern-weekly-hearing-list/index.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-southern-weekly-hearing-list/index.test.ts` | | +| `apps/web/src/pages/(list-types)/ftt-rpt-southern-weekly-hearing-list/ftt-rpt-southern-weekly-hearing-list.njk` | | + +--- + +## 5. Existing Files to Modify + +| File | Change | +|------|--------| +| `libs/location/src/list-type-data.ts` | Add 10 new entries (IDs 28–37) | +| `apps/web/src/app.ts` | Import `moduleRoot` from each of the 4 new packages' `/config` and add to `modulePaths` array | +| `tsconfig.json` (root) | Add `paths` entries for the 4 new packages (and their `/config` sub-paths) | + +The `vite.build.ts` in `apps/web` does not need a change — none of the new modules have frontend assets (same as CST). The seed mechanism automatically picks up new `listTypeData` entries via `seedListTypes()` — no seed file change is needed. + +--- + +## 6. Error Handling + +All page controllers follow the CST pattern: +- Missing `artefactId` query parameter → 400 with `errors/common` template +- Artefact not found in DB → 404 +- JSON file not found on disk → 404 +- JSON Schema validation failure → 400 +- Any uncaught exception → 500 + +This is handled by the try/catch structure in each controller's `GET` handler, identical to `apps/web/src/pages/(list-types)/care-standards-tribunal-weekly-hearing-list/index.ts`. + +--- + +## 7. Open Questions / Assumptions + +1. **ID collision with existing IDs 24–27.** The ticket comment assigns IDs 24–33 to the new list types, but `list-type-data.ts` already uses IDs 24–27 for the SJP lists. The plan uses IDs 28–37 (next available). If the DB has already had IDs 24–33 created in a non-local environment, the `registerConverter(id, ...)` calls will need to match whatever IDs exist there. Name-based registration (`registerConverterByName`) is the safer fallback and must always be included. + +2. **SIAC / POAC / PAAC access classification.** `defaultSensitivity` is marked as TBD. If these lists contain information about persons whose identities need protection (which the accordion text implies), `Classified` or `Private` may be more appropriate than `Public`. Awaiting confirmation. + +3. **Welsh translations.** No Welsh strings were supplied for any of the 10 new list types. All `cy.ts` files will use English text as placeholders, matching the same value as `en.ts`, until translations are provided. + +4. **FTT LRT and FTT RPT office email.** The accordion text contains `[insert office email]`. The placeholder string `[insert office email]` will be used verbatim in `en.ts` until real addresses are confirmed. + +5. **PAAC upload label — double-C.** The ticket specifies "PACC Weekly Hearing List" (double-C) as the upload form label. This is used as-is in `shortenedFriendlyName`. If this is a typo for "PAAC" it should be raised with the product owner before implementation. + +6. **PDF generation scope.** The CST reference implementation includes a fully working PDF generator. This plan includes PDF generators for all 10 new list types following the same pattern. If PDF generation is intended to be deferred, the `pdf-generator.ts` and `pdf-template.njk` files can be stubbed. + +7. **FTT RPT `caseType`.** The ticket does not specify whether `caseType` is free text or an enumerated value. The schema and converter will treat it as free text with no-HTML validation. If an enum is required, the schema and validator will need updating. + +8. **Shared lib packaging.** This plan groups SIAC/POAC/PAAC into one package and all 5 RPT variants into one package, following the principle of sharing code when field definitions are identical. If the product owner requires strict per-list isolation, each list type can instead be given its own package — at the cost of significant code duplication. diff --git a/docs/tickets/428/review.md b/docs/tickets/428/review.md new file mode 100644 index 000000000..af8b5e901 --- /dev/null +++ b/docs/tickets/428/review.md @@ -0,0 +1,195 @@ +# Code Review: Issue #428 + +## Summary + +This implementation adds 10 new non-strategic hearing list types across 4 new lib modules and 10 new page controllers, following the established pattern of `@hmcts/care-standards-tribunal-weekly-hearing-list`. The architecture is sound and the code quality is generally high. However, there are two bugs that will cause silent rendering failures in production — one in the SIAC/POAC/PAAC PDF template and one in the SIAC/POAC/PAAC web template — and a product decision about SIAC/POAC/PAAC sensitivity level that is currently resolved in the wrong direction. The verification tasks in Group 12 are not yet complete. + +--- + +## CRITICAL Issues + +### 1. SIAC/POAC/PAAC PDF template references an undefined key `t.importantInformation` + +**Files:** +- `libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-template.njk` line 21 +- `libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/en.ts` + +**Problem:** The PDF template renders `{{ t.importantInformation }}` but the `en.ts` and `cy.ts` locale objects have no `importantInformation` key. The locale only exports `siacImportantInformation`, `poacImportantInformation`, and `paacImportantInformation`. Nunjucks silently outputs an empty string for undefined variables, so all three PDF outputs will show a blank important information section with no error. + +**Impact:** Every generated PDF for SIAC, POAC, and PAAC will have an empty "Important information" box, silently dropping legally required content from the downloadable document. + +**Solution:** The PDF generator accepts a `courtName` parameter and a `listTitle` parameter, which identifies which tribunal is being rendered. The generator should resolve the correct accordion text and pass it explicitly to the template. Either add a `importantInformation` parameter to `PdfGenerationOptions` in `pdf-generator.ts` and require callers to supply it, or resolve it inside the generator using a map keyed on `courtName`. The template variable name can remain `importantInformation`. + +--- + +### 2. SIAC/POAC/PAAC web templates render the important information link as literal markdown text + +**Files:** +- `apps/web/src/pages/(list-types)/siac-weekly-hearing-list/siac-weekly-hearing-list.njk` line 32 +- `apps/web/src/pages/(list-types)/poac-weekly-hearing-list/poac-weekly-hearing-list.njk` line 32 +- `apps/web/src/pages/(list-types)/paac-weekly-hearing-list/paac-weekly-hearing-list.njk` line 32 + +**Problem:** The `siacImportantInformation` locale string is stored as a multi-paragraph string containing a Markdown-style hyperlink: `[Find out what to expect coming to a court or tribunal](https://www.gov.uk/guidance/what-to-expect-coming-to-a-court-or-tribunal)`. The template renders this via `{{ importantInformation }}` which outputs the raw text, so users see the literal markdown syntax rather than a working anchor link. + +Contrast this with the FTT Tax, FTT LRT, and FTT RPT modules, which correctly separate the link into distinct locale keys (`importantInformationText`, `importantInformationLinkText`, `importantInformationLinkUrl`) and render the anchor explicitly in the template. + +**Impact:** The external "Find out what to expect" link required by the ticket specification is broken for all three SIAC/POAC/PAAC pages. Users see raw text like `[Find out what to expect coming to a court or tribunal](https://...)` rather than a clickable link. This is an accessibility failure (WCAG 2.1 SC 4.1.3 — status messages and interactive content must be programmatically determinable). + +**Solution:** Refactor the `siacImportantInformation`, `poacImportantInformation`, and `paacImportantInformation` locale keys to follow the same pattern used in `ftt-tax-chamber-weekly-hearing-list`. Replace the single string with three keys per tribunal variant: `siacImportantInformationText`, `siacImportantInformationLinkText`, `siacImportantInformationLinkUrl`. Update the SIAC, POAC, and PAAC templates to render the link explicitly with ``. The cy.ts locale stubs must mirror the same key structure. + +--- + +### 3. SIAC/POAC/PAAC `defaultSensitivity` set to `Public` — unresolved open question + +**File:** `libs/location/src/list-type-data.ts` lines 327, 340, 352 + +**Problem:** The plan explicitly flagged this as an open question (plan.md Open Question 2): *"If these lists contain information about persons whose identities need protection (which the accordion text implies), `Classified` or `Private` may be more appropriate than `Public`."* The implementation has resolved this as `Public` without documented confirmation from the product owner. + +The important information text itself states *"The tribunal sometimes uses reference numbers or initials to protect the anonymity of those involved in the appeal"*, which strongly implies these are not straightforwardly public lists. + +**Impact:** Setting the wrong sensitivity level means access control may be incorrectly applied, potentially exposing hearing information about individuals under anonymity protection to users who should not have access. + +**Solution:** Obtain explicit written confirmation from the product owner about the intended sensitivity level for IDs 28, 29, and 30 before these entries go live. If `Public` is confirmed, document the confirmation in the ticket. + +--- + +## HIGH PRIORITY Issues + +### 4. `@hmcts/postgres-prisma` listed as a dependency but is never imported + +**Files:** +- `libs/list-types/siac-poac-paac-weekly-hearing-list/package.json` line 29 +- `libs/list-types/ftt-tax-chamber-weekly-hearing-list/package.json` line 29 +- `libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/package.json` line 29 +- `libs/list-types/ftt-rpt-weekly-hearing-list/package.json` line 29 + +**Problem:** All four new packages declare `"@hmcts/postgres-prisma": "workspace:*"` as a dependency. No file within any of these modules imports from `@hmcts/postgres-prisma`. This appears to be copied directly from the reference module (`care-standards-tribunal-weekly-hearing-list`), which also carries this unused dependency. None of the new modules query the database directly — all DB access goes through `@hmcts/publication` via `getArtefactById`. + +**Impact:** Unnecessary dependency increases bundle size and can trigger Prisma client initialisation in environments where a database connection is not available, adding latency and potential startup errors to unrelated test runs. + +**Solution:** Remove `@hmcts/postgres-prisma` from the `dependencies` section of all four new `package.json` files. + +### 5. Inline ` +``` +This pattern is directly copied from the CST reference module, which has the same issue. The CLAUDE.md frontend rules state: "Never use inline styles — use GOV.UK classes only." Injecting a ` + + +
+

{{ header.listTitle }}

+ +

{{ t.factLinkText }} {{ t.factAdditionalText }}

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+
+ +
+

{{ t.importantInformationTitle }}

+

{{ t.importantInformationText }}

+

{{ t.importantInformationLinkText }}

+
+ + {% if hearings.length > 0 %} + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.hearingTime }}{{ t.tableHeaders.caseName }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.judge }}{{ t.tableHeaders.venuePlatform }}
{{ hearing.date }}{{ hearing.hearingTime }}{{ hearing.caseName }}{{ hearing.caseReferenceNumber }}{{ hearing.judge }}{{ hearing.venuePlatform }}
+ {% else %} +

No hearings scheduled.

+ {% endif %} + + + + diff --git a/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.test.ts b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.test.ts new file mode 100644 index 000000000..0bd0d08b0 --- /dev/null +++ b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from "vitest"; +import type { FttLrtHearingList } from "../models/types.js"; +import { renderFttLrtData } from "./renderer.js"; + +describe("renderFttLrtData", () => { + it("should render hearing list with formatted dates", () => { + // Arrange + const hearingList: FttLrtHearingList = [ + { + date: "02/01/2025", + hearingTime: "10:00am", + caseName: "A Vs B", + caseReferenceNumber: "LRT/00001/2025", + judge: "Judge Smith", + venuePlatform: "London" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Land Registration Tribunal)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List" + }; + + // Act + const result = renderFttLrtData(hearingList, options); + + // Assert + expect(result.header.listTitle).toBe("First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List"); + expect(result.header.weekCommencingDate).toBe("02 January 2025"); + expect(result.header.lastUpdatedDate).toBe("01 January 2025"); + expect(result.header.lastUpdatedTime).toContain("am"); + + expect(result.hearings).toHaveLength(1); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[0].hearingTime).toBe("10:00am"); + expect(result.hearings[0].caseName).toBe("A Vs B"); + expect(result.hearings[0].caseReferenceNumber).toBe("LRT/00001/2025"); + expect(result.hearings[0].judge).toBe("Judge Smith"); + expect(result.hearings[0].venuePlatform).toBe("London"); + }); + + it("should render multiple hearings", () => { + // Arrange + const hearingList: FttLrtHearingList = [ + { + date: "02/01/2025", + hearingTime: "10:00am", + caseName: "A Vs B", + caseReferenceNumber: "LRT/00001/2025", + judge: "Judge Smith", + venuePlatform: "London" + }, + { + date: "03/01/2025", + hearingTime: "2:00pm", + caseName: "C Vs D", + caseReferenceNumber: "LRT/00002/2025", + judge: "Judge Brown", + venuePlatform: "Manchester" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Land Registration Tribunal)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List" + }; + + // Act + const result = renderFttLrtData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(2); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[1].date).toBe("03 January 2025"); + }); + + it("should handle empty hearing list", () => { + // Arrange + const hearingList: FttLrtHearingList = []; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Land Registration Tribunal)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List" + }; + + // Act + const result = renderFttLrtData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(0); + expect(result.header.listTitle).toBe("First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List"); + }); + + it("should format lastUpdated time correctly", () => { + // Arrange + const hearingList: FttLrtHearingList = [ + { + date: "02/01/2025", + hearingTime: "10:00am", + caseName: "A Vs B", + caseReferenceNumber: "LRT/00001/2025", + judge: "Judge Smith", + venuePlatform: "London" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Land Registration Tribunal)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T14:30:00Z", + listTitle: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List" + }; + + // Act + const result = renderFttLrtData(hearingList, options); + + // Assert + expect(result.header.lastUpdatedTime).toBe("2:30pm"); + }); +}); diff --git a/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.ts b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.ts new file mode 100644 index 000000000..234027575 --- /dev/null +++ b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/rendering/renderer.ts @@ -0,0 +1,44 @@ +import { formatDdMmYyyyDate, formatDisplayDate, formatLastUpdatedDateTime } from "@hmcts/list-types-common"; +import type { FttLrtHearing, FttLrtHearingList } from "../models/types.js"; + +export interface RenderOptions { + locale: string; + courtName: string; + contentDate: Date; + lastReceivedDate: string; + listTitle: string; +} + +export interface RenderedData { + header: { + listTitle: string; + weekCommencingDate: string; + lastUpdatedDate: string; + lastUpdatedTime: string; + }; + hearings: FttLrtHearing[]; +} + +export function renderFttLrtData(hearingList: FttLrtHearingList, options: RenderOptions): RenderedData { + const weekCommencingDate = formatDisplayDate(options.contentDate, options.locale); + const { date: lastUpdatedDate, time: lastUpdatedTime } = formatLastUpdatedDateTime(options.lastReceivedDate, options.locale); + + const renderedHearings = hearingList.map((hearing) => ({ + date: formatDdMmYyyyDate(hearing.date, options.locale), + hearingTime: hearing.hearingTime, + caseName: hearing.caseName, + caseReferenceNumber: hearing.caseReferenceNumber, + judge: hearing.judge, + venuePlatform: hearing.venuePlatform + })); + + return { + header: { + listTitle: options.listTitle, + weekCommencingDate, + lastUpdatedDate, + lastUpdatedTime + }, + hearings: renderedHearings + }; +} diff --git a/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/schemas/ftt-lands-registration-tribunal-weekly-hearing-list.json b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/schemas/ftt-lands-registration-tribunal-weekly-hearing-list.json new file mode 100644 index 000000000..a425dba9a --- /dev/null +++ b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/schemas/ftt-lands-registration-tribunal-weekly-hearing-list.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List", + "description": "Schema for First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List from pip-data-management", + "type": "array", + "items": { + "type": "object", + "required": ["date", "hearingTime", "caseName", "caseReferenceNumber", "judge", "venuePlatform"], + "properties": { + "date": { + "title": "Date", + "type": "string", + "pattern": "^\\d{2}/\\d{2}/\\d{4}$", + "examples": ["02/01/2025"] + }, + "hearingTime": { + "title": "Hearing Time", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["10:00am"] + }, + "caseName": { + "title": "Case Name", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["A Vs B"] + }, + "caseReferenceNumber": { + "title": "Case Reference Number", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["LRT/00001/2025"] + }, + "judge": { + "title": "Judge", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Judge Smith"] + }, + "venuePlatform": { + "title": "Venue/Platform", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["London"] + } + } + } +} diff --git a/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/tsconfig.json b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/tsconfig.json new file mode 100644 index 000000000..a029ee937 --- /dev/null +++ b/libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules", "src/assets/"] +} diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/package.json b/libs/list-types/ftt-rpt-weekly-hearing-list/package.json new file mode 100644 index 000000000..78314c781 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/package.json @@ -0,0 +1,42 @@ +{ + "name": "@hmcts/ftt-rpt-weekly-hearing-list", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "production": "./dist/index.js", + "default": "./src/index.ts" + }, + "./config": { + "production": "./dist/config.js", + "default": "./src/config.ts" + } + }, + "scripts": { + "build": "tsc && yarn build:nunjucks && yarn build:views && yarn build:schemas", + "build:nunjucks": "mkdir -p dist/pdf && cd src/pdf && find . -name '*.njk' -exec sh -c 'mkdir -p ../../dist/pdf/$(dirname {}) && cp {} ../../dist/pdf/{}' \\;", + "build:views": "mkdir -p dist/views && find src/views -name '*.njk' -exec cp {} dist/views/ \\;", + "build:schemas": "mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/", + "dev": "tsc --watch", + "test": "vitest run", + "test:watch": "vitest watch", + "format": "biome format --write .", + "lint": "biome check .", + "lint:fix": "biome check --write --unsafe ." + }, + "dependencies": { + "@hmcts/list-types-common": "workspace:*", + "@hmcts/pdf-generation": "workspace:*", + "luxon": "3.7.2", + "nunjucks": "3.2.4" + }, + "devDependencies": { + "@types/luxon": "3.7.1", + "@types/node": "24.10.4", + "typescript": "6.0.3", + "vitest": "4.1.9" + }, + "peerDependencies": { + "express": "^5.1.0" + } +} diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/config.test.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/config.test.ts new file mode 100644 index 000000000..ba5b2f5b3 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/config.test.ts @@ -0,0 +1,48 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { assets, moduleRoot } from "./config.js"; + +describe("ftt-rpt-weekly-hearing-list config", () => { + describe("moduleRoot", () => { + it("should be defined", () => { + expect(moduleRoot).toBeDefined(); + expect(typeof moduleRoot).toBe("string"); + }); + + it("should point to an existing directory", () => { + expect(existsSync(moduleRoot)).toBe(true); + }); + + it("should be an absolute path", () => { + expect(path.isAbsolute(moduleRoot)).toBe(true); + }); + }); + + describe("assets", () => { + it("should be defined", () => { + expect(assets).toBeDefined(); + expect(typeof assets).toBe("string"); + }); + + it("should point to assets directory", () => { + expect(assets).toContain("assets"); + }); + + it("should be an absolute path", () => { + expect(path.isAbsolute(assets)).toBe(true); + }); + + it("should have valid path structure", () => { + expect(assets).toBeTruthy(); + }); + + it("should end with trailing slash", () => { + expect(assets).toMatch(/\/$/); + }); + + it("should be subdirectory of moduleRoot", () => { + expect(assets.startsWith(moduleRoot)).toBe(true); + }); + }); +}); diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/config.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/config.ts new file mode 100644 index 000000000..90a77f452 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/config.ts @@ -0,0 +1,9 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const moduleRoot = __dirname; +export const assets = path.join(__dirname, "assets/"); +export const schemaPath = path.join(__dirname, "schemas/ftt-rpt-weekly-hearing-list.json"); diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/conversion/ftt-rpt-config.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/conversion/ftt-rpt-config.ts new file mode 100644 index 000000000..401f963ba --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/conversion/ftt-rpt-config.ts @@ -0,0 +1,89 @@ +import { + createConverter, + DD_MM_YYYY_PATTERN, + type ExcelConverterConfig, + registerConverter, + registerConverterByName, + validateDateFormat, + validateNoHtmlTags +} from "@hmcts/list-types-common"; + +// FTT RPT Weekly Hearing List — shared config for all 5 regional variants (listTypeIds: 33–37) +export const FTT_RPT_EXCEL_CONFIG: ExcelConverterConfig = { + fields: [ + { + header: "Date", + fieldName: "date", + required: true, + validators: [validateDateFormat(DD_MM_YYYY_PATTERN, "dd/MM/yyyy (e.g., 02/01/2025)")] + }, + { + header: "Time", + fieldName: "time", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Time", rowNumber)] + }, + { + header: "Venue", + fieldName: "venue", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Venue", rowNumber)] + }, + { + header: "Case Type", + fieldName: "caseType", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Case Type", rowNumber)] + }, + { + header: "Case Reference Number", + fieldName: "caseReferenceNumber", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Case Reference Number", rowNumber)] + }, + { + header: "Judge(s)", + fieldName: "judges", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Judge(s)", rowNumber)] + }, + { + header: "Member(s)", + fieldName: "members", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Member(s)", rowNumber)] + }, + { + header: "Hearing Method", + fieldName: "hearingMethod", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Hearing Method", rowNumber)] + }, + { + header: "Additional Information", + fieldName: "additionalInformation", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Additional Information", rowNumber)] + } + ], + minRows: 1 +}; + +// Register the same converter for all 5 RPT regional variants +// Name-based registration handles environments where the DB ID differs from the canonical seeded ID +const fttRptConverter = createConverter(FTT_RPT_EXCEL_CONFIG); + +registerConverter(33, fttRptConverter); +registerConverterByName("FTT_RPT_EASTERN_WEEKLY_HEARING_LIST", fttRptConverter); + +registerConverter(34, fttRptConverter); +registerConverterByName("FTT_RPT_LONDON_WEEKLY_HEARING_LIST", fttRptConverter); + +registerConverter(35, fttRptConverter); +registerConverterByName("FTT_RPT_MIDLANDS_WEEKLY_HEARING_LIST", fttRptConverter); + +registerConverter(36, fttRptConverter); +registerConverterByName("FTT_RPT_NORTHERN_WEEKLY_HEARING_LIST", fttRptConverter); + +registerConverter(37, fttRptConverter); +registerConverterByName("FTT_RPT_SOUTHERN_WEEKLY_HEARING_LIST", fttRptConverter); diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.test.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.test.ts new file mode 100644 index 000000000..a3b797d0d --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from "vitest"; +import type { FttRptHearingList } from "../models/types.js"; +import { extractCaseSummary, formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING } from "./summary-builder.js"; + +describe("SPECIAL_CATEGORY_DATA_WARNING", () => { + it("should contain required warning text", () => { + expect(SPECIAL_CATEGORY_DATA_WARNING).toContain("Special Category Data"); + expect(SPECIAL_CATEGORY_DATA_WARNING).toContain("Data Protection Act 2018"); + }); +}); + +describe("extractCaseSummary", () => { + it("should extract case summaries from hearing list", () => { + // Arrange + const hearingList: FttRptHearingList = [ + { + date: "01/01/2025", + time: "10:00am", + venue: "London", + caseType: "Leasehold", + caseReferenceNumber: "RPT/00001/2025", + judges: "Judge Smith", + members: "", + hearingMethod: "In person", + additionalInformation: "" + }, + { + date: "02/01/2025", + time: "2:00pm", + venue: "Manchester", + caseType: "Rent", + caseReferenceNumber: "RPT/00002/2025", + judges: "Judge Brown", + members: "Member Jones", + hearingMethod: "Video", + additionalInformation: "" + } + ]; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(2); + expect(result[0]).toEqual([ + { label: "Date", value: "01/01/2025" }, + { label: "Time", value: "10:00am" }, + { label: "Case reference number", value: "RPT/00001/2025" } + ]); + expect(result[1]).toEqual([ + { label: "Date", value: "02/01/2025" }, + { label: "Time", value: "2:00pm" }, + { label: "Case reference number", value: "RPT/00002/2025" } + ]); + }); + + it("should handle empty hearing list", () => { + // Arrange + const hearingList: FttRptHearingList = []; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(0); + }); + + it("should handle missing case details with empty string", () => { + // Arrange + const hearingList: FttRptHearingList = [ + { + date: "", + time: "", + venue: "", + caseType: "", + caseReferenceNumber: "", + judges: "", + members: "", + hearingMethod: "", + additionalInformation: "" + } + ]; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(1); + expect(result[0]).toEqual([ + { label: "Date", value: "" }, + { label: "Time", value: "" }, + { label: "Case reference number", value: "" } + ]); + }); +}); + +describe("formatCaseSummaryForEmail", () => { + it("should format single case summary correctly", () => { + // Arrange + const items = [ + [ + { label: "Date", value: "01/01/2025" }, + { label: "Time", value: "10:00am" }, + { label: "Case reference number", value: "RPT/00001/2025" } + ] + ]; + + // Act + const result = formatCaseSummaryForEmail(items); + + // Assert + expect(result).toContain("---"); + expect(result).toContain("Date - 01/01/2025"); + expect(result).toContain("Time - 10:00am"); + expect(result).toContain("Case reference number - RPT/00001/2025"); + }); + + it("should handle empty case list", () => { + // Act + const result = formatCaseSummaryForEmail([]); + + // Assert + expect(result).toBe("No cases scheduled."); + }); +}); diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.ts new file mode 100644 index 000000000..f8dd07d50 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/email-summary/summary-builder.ts @@ -0,0 +1,12 @@ +import { type CaseSummary, formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING } from "@hmcts/list-types-common"; +import type { FttRptHearingList } from "../models/types.js"; + +export { formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING }; + +export function extractCaseSummary(jsonData: FttRptHearingList): CaseSummary[] { + return jsonData.map((hearing) => [ + { label: "Date", value: hearing.date || "" }, + { label: "Time", value: hearing.time || "" }, + { label: "Case reference number", value: hearing.caseReferenceNumber || "" } + ]); +} diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/index.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/index.ts new file mode 100644 index 000000000..ff016d75f --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/index.ts @@ -0,0 +1,11 @@ +import "./conversion/ftt-rpt-config.js"; // Register converter on module load + +// Business logic exports +export type { ValidationResult } from "@hmcts/publication"; +export * from "./email-summary/summary-builder.js"; +export { cy as fttRptWeeklyHearingListCy } from "./locales/cy.js"; +// Locale exports +export { en as fttRptWeeklyHearingListEn } from "./locales/en.js"; +export * from "./models/types.js"; +export * from "./pdf/pdf-generator.js"; +export * from "./rendering/renderer.js"; diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/cy.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/cy.ts new file mode 100644 index 000000000..e0c1fa80d --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/cy.ts @@ -0,0 +1,45 @@ +import { provenanceLabelsCy as provenanceLabels } from "@hmcts/list-types-common"; + +export const cy = { + listForWeekCommencing: "List for week commencing", + lastUpdated: "Last updated", + at: "at", + factLinkText: "Find contact details and other information about courts and tribunals", + factLinkUrl: "https://www.find-court-tribunal.service.gov.uk/", + factAdditionalText: "in England and Wales, and some non-devolved tribunals in Scotland.", + importantInformationTitle: "Important information", + importantInformationText: + 'Members of the public wishing to observe a hearing or representatives of the media may, on their request, join any telephone or video hearing remotely while they are taking place by sending an email in advance to the tribunal at [insert office email] with the following details in the subject line "[OBSERVER/MEDIA] REQUEST – [case reference] – [hearing date]" and appropriate arrangements will be made to allow access where reasonably practicable.', + importantInformationLinkText: "Observe a court or tribunal hearing as a journalist, researcher or member of the public", + importantInformationLinkUrl: "https://www.gov.uk/guidance/observe-a-court-or-tribunal-hearing", + searchCasesTitle: "Search Cases", + searchCasesLabel: "Search by case reference number, date, venue, or other details", + tableHeaders: { + date: "Date", + time: "Time", + venue: "Venue", + caseType: "Case type", + caseReferenceNumber: "Case reference number", + judges: "Judge(s)", + members: "Member(s)", + hearingMethod: "Hearing method", + additionalInformation: "Additional information" + }, + dataSource: "Data source", + backToTop: "Back to top", + cautionNote: + "Note this document contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be handled appropriately.", + cautionReporting: + "This document contains information intended to assist the accurate reporting of court proceedings. It is vital you ensure that you safeguard the Special Category Data included and abide by reporting restrictions (for example on victims and children). HMCTS will stop sending the data if there is concern about how it will be used.", + provenanceLabels, + rptEasternCourtName: "First-tier Tribunal (Residential Property Tribunal): Eastern region", + rptLondonCourtName: "First-tier Tribunal (Residential Property Tribunal): London region", + rptMidlandsCourtName: "First-tier Tribunal (Residential Property Tribunal): Midlands region", + rptNorthernCourtName: "First-tier Tribunal (Residential Property Tribunal): Northern region", + rptSouthernCourtName: "First-tier Tribunal (Residential Property Tribunal): Southern region", + rptEasternPageTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List", + rptLondonPageTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List", + rptMidlandsPageTitle: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List", + rptNorthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List", + rptSouthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List" +}; diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/en.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/en.ts new file mode 100644 index 000000000..e672154a2 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/locales/en.ts @@ -0,0 +1,45 @@ +import { provenanceLabelsEn as provenanceLabels } from "@hmcts/list-types-common"; + +export const en = { + listForWeekCommencing: "List for week commencing", + lastUpdated: "Last updated", + at: "at", + factLinkText: "Find contact details and other information about courts and tribunals", + factLinkUrl: "https://www.find-court-tribunal.service.gov.uk/", + factAdditionalText: "in England and Wales, and some non-devolved tribunals in Scotland.", + importantInformationTitle: "Important information", + importantInformationText: + 'Members of the public wishing to observe a hearing or representatives of the media may, on their request, join any telephone or video hearing remotely while they are taking place by sending an email in advance to the tribunal at [insert office email] with the following details in the subject line "[OBSERVER/MEDIA] REQUEST – [case reference] – [hearing date]" and appropriate arrangements will be made to allow access where reasonably practicable.', + importantInformationLinkText: "Observe a court or tribunal hearing as a journalist, researcher or member of the public", + importantInformationLinkUrl: "https://www.gov.uk/guidance/observe-a-court-or-tribunal-hearing", + searchCasesTitle: "Search Cases", + searchCasesLabel: "Search by case reference number, date, venue, or other details", + tableHeaders: { + date: "Date", + time: "Time", + venue: "Venue", + caseType: "Case type", + caseReferenceNumber: "Case reference number", + judges: "Judge(s)", + members: "Member(s)", + hearingMethod: "Hearing method", + additionalInformation: "Additional information" + }, + dataSource: "Data source", + backToTop: "Back to top", + cautionNote: + "Note this document contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be handled appropriately.", + cautionReporting: + "This document contains information intended to assist the accurate reporting of court proceedings. It is vital you ensure that you safeguard the Special Category Data included and abide by reporting restrictions (for example on victims and children). HMCTS will stop sending the data if there is concern about how it will be used.", + provenanceLabels, + rptEasternCourtName: "First-tier Tribunal (Residential Property Tribunal): Eastern region", + rptLondonCourtName: "First-tier Tribunal (Residential Property Tribunal): London region", + rptMidlandsCourtName: "First-tier Tribunal (Residential Property Tribunal): Midlands region", + rptNorthernCourtName: "First-tier Tribunal (Residential Property Tribunal): Northern region", + rptSouthernCourtName: "First-tier Tribunal (Residential Property Tribunal): Southern region", + rptEasternPageTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List", + rptLondonPageTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List", + rptMidlandsPageTitle: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List", + rptNorthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List", + rptSouthernPageTitle: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List" +}; diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/models/types.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/models/types.ts new file mode 100644 index 000000000..4a5cafc7f --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/models/types.ts @@ -0,0 +1,13 @@ +export interface FttRptHearing { + date: string; + time: string; + venue: string; + caseType: string; + caseReferenceNumber: string; + judges: string; + members: string; + hearingMethod: string; + additionalInformation: string; +} + +export type FttRptHearingList = FttRptHearing[]; diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.test.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.test.ts new file mode 100644 index 000000000..774c071b7 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.test.ts @@ -0,0 +1,160 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockUploadBlob } = vi.hoisted(() => ({ + mockUploadBlob: vi.fn() +})); +vi.mock("@hmcts/azure-blob", () => ({ + uploadBlob: mockUploadBlob, + CONTAINER: { ARTEFACT: "artefact", PUBLICATIONS: "publications" } +})); + +vi.mock("@hmcts/pdf-generation", () => ({ + generatePdfFromHtml: vi.fn() +})); + +vi.mock("../rendering/renderer.js", () => ({ + renderFttRptData: vi.fn() +})); + +import { generatePdfFromHtml } from "@hmcts/pdf-generation"; +import { renderFttRptData } from "../rendering/renderer.js"; +import { generateFttRptWeeklyHearingListPdf } from "./pdf-generator.js"; + +const mockRenderedData = { + header: { + listTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List", + weekCommencingDate: "01 January 2025", + lastUpdatedDate: "12 November 2025", + lastUpdatedTime: "9am" + }, + hearings: [] +}; + +const mockHearingList = [ + { + date: "01/01/2025", + time: "10:00am", + venue: "London", + caseType: "Leasehold", + caseReferenceNumber: "RPT/00001/2025", + judges: "Judge Smith", + members: "", + hearingMethod: "In person", + additionalInformation: "" + } +]; + +describe("generateFttRptWeeklyHearingListPdf", () => { + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(renderFttRptData).mockReturnValue(mockRenderedData); + mockUploadBlob.mockResolvedValue(undefined); + }); + + it("should generate PDF successfully", async () => { + // Arrange + const pdfBuffer = Buffer.from("PDF content"); + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer, + sizeBytes: 1024 + }); + + // Act + const result = await generateFttRptWeeklyHearingListPdf({ + artefactId: "test-artefact-123", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal): Eastern region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List" + }); + + // Assert + expect(result.success).toBe(true); + expect(result.pdfPath).toContain("test-artefact-123.pdf"); + expect(result.sizeBytes).toBe(1024); + expect(result.exceedsMaxSize).toBe(false); + }); + + it("should return exceedsMaxSize true when PDF is over 2MB", async () => { + // Arrange + const largePdfBuffer = Buffer.alloc(3 * 1024 * 1024); + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer: largePdfBuffer, + sizeBytes: 3 * 1024 * 1024 + }); + + // Act + const result = await generateFttRptWeeklyHearingListPdf({ + artefactId: "large-pdf-123", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal): London region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List" + }); + + // Assert + expect(result.success).toBe(true); + expect(result.exceedsMaxSize).toBe(true); + }); + + it("should return error when PDF generation fails", async () => { + // Arrange + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: false, + error: "Puppeteer crashed" + }); + + // Act + const result = await generateFttRptWeeklyHearingListPdf({ + artefactId: "failed-pdf", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal): Midlands region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List" + }); + + // Assert + expect(result.success).toBe(false); + expect(result.error).toBe("Puppeteer crashed"); + }); + + it("should pass correct render options to renderer", async () => { + // Arrange + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer: Buffer.from("PDF"), + sizeBytes: 100 + }); + + const contentDate = new Date("2025-06-15"); + + // Act + await generateFttRptWeeklyHearingListPdf({ + artefactId: "test-render-options", + contentDate, + locale: "cy", + locationId: "999", + jsonData: mockHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal): Northern region", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List" + }); + + // Assert + expect(renderFttRptData).toHaveBeenCalledWith(mockHearingList, { + locale: "cy", + courtName: "First-tier Tribunal (Residential Property Tribunal): Northern region", + contentDate, + lastReceivedDate: expect.any(String), + listTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List" + }); + }); +}); diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.ts new file mode 100644 index 000000000..7cd60d8cc --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-generator.ts @@ -0,0 +1,28 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { type BasePdfGenerationOptions, generateFttSiacWeeklyHearingListPdf, type PdfGenerationResult } from "@hmcts/list-types-common"; +import { generatePdfFromHtml } from "@hmcts/pdf-generation"; +import { PROVENANCE_LABELS } from "@hmcts/publication"; +import type { FttRptHearingList } from "../models/types.js"; +import { renderFttRptData } from "../rendering/renderer.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +interface PdfGenerationOptions extends BasePdfGenerationOptions { + contentDate: Date; + courtName: string; + listTitle: string; +} + +export async function generateFttRptWeeklyHearingListPdf(options: PdfGenerationOptions): Promise { + return generateFttSiacWeeklyHearingListPdf({ + ...options, + moduleDir: __dirname, + provenanceLabel: options.provenance ? PROVENANCE_LABELS[options.provenance as keyof typeof PROVENANCE_LABELS] || options.provenance : "", + importEn: () => import("../locales/en.js"), + importCy: () => import("../locales/cy.js"), + generatePdf: generatePdfFromHtml, + renderData: renderFttRptData + }); +} diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-template.njk b/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-template.njk new file mode 100644 index 000000000..43e8b4e78 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/pdf/pdf-template.njk @@ -0,0 +1,70 @@ + + + + + + {{ header.listTitle }} + + + +
+

{{ header.listTitle }}

+ +

{{ t.factLinkText }} {{ t.factAdditionalText }}

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+
+ +
+

{{ t.importantInformationTitle }}

+

{{ t.importantInformationText }}

+

{{ t.importantInformationLinkText }}

+
+ + {% if hearings.length > 0 %} + + + + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.time }}{{ t.tableHeaders.venue }}{{ t.tableHeaders.caseType }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.judges }}{{ t.tableHeaders.members }}{{ t.tableHeaders.hearingMethod }}{{ t.tableHeaders.additionalInformation }}
{{ hearing.date }}{{ hearing.time }}{{ hearing.venue }}{{ hearing.caseType }}{{ hearing.caseReferenceNumber }}{{ hearing.judges }}{{ hearing.members }}{{ hearing.hearingMethod }}{{ hearing.additionalInformation }}
+ {% else %} +

No hearings scheduled.

+ {% endif %} + + + + diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.test.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.test.ts new file mode 100644 index 000000000..2fc436b9f --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from "vitest"; +import type { FttRptHearingList } from "../models/types.js"; +import { renderFttRptData } from "./renderer.js"; + +describe("renderFttRptData", () => { + it("should render hearing list with formatted dates", () => { + // Arrange + const hearingList: FttRptHearingList = [ + { + date: "02/01/2025", + time: "10:00am", + venue: "London", + caseType: "Leasehold", + caseReferenceNumber: "RPT/00001/2025", + judges: "Judge Smith", + members: "Member Jones", + hearingMethod: "In person", + additionalInformation: "" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Residential Property Tribunal): Eastern region", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List" + }; + + // Act + const result = renderFttRptData(hearingList, options); + + // Assert + expect(result.header.listTitle).toBe("First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List"); + expect(result.header.weekCommencingDate).toBe("02 January 2025"); + expect(result.header.lastUpdatedDate).toBe("01 January 2025"); + expect(result.header.lastUpdatedTime).toContain("am"); + + expect(result.hearings).toHaveLength(1); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[0].time).toBe("10:00am"); + expect(result.hearings[0].venue).toBe("London"); + expect(result.hearings[0].caseType).toBe("Leasehold"); + expect(result.hearings[0].caseReferenceNumber).toBe("RPT/00001/2025"); + expect(result.hearings[0].judges).toBe("Judge Smith"); + expect(result.hearings[0].members).toBe("Member Jones"); + expect(result.hearings[0].hearingMethod).toBe("In person"); + expect(result.hearings[0].additionalInformation).toBe(""); + }); + + it("should render multiple hearings", () => { + // Arrange + const hearingList: FttRptHearingList = [ + { + date: "02/01/2025", + time: "10:00am", + venue: "London", + caseType: "Leasehold", + caseReferenceNumber: "RPT/00001/2025", + judges: "Judge Smith", + members: "", + hearingMethod: "In person", + additionalInformation: "" + }, + { + date: "03/01/2025", + time: "2:00pm", + venue: "Manchester", + caseType: "Rent", + caseReferenceNumber: "RPT/00002/2025", + judges: "Judge Brown", + members: "Member Jones", + hearingMethod: "Video", + additionalInformation: "Remote" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Residential Property Tribunal): Northern region", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List" + }; + + // Act + const result = renderFttRptData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(2); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[1].date).toBe("03 January 2025"); + }); + + it("should handle empty hearing list", () => { + // Arrange + const hearingList: FttRptHearingList = []; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Residential Property Tribunal): London region", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List" + }; + + // Act + const result = renderFttRptData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(0); + expect(result.header.listTitle).toBe("First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List"); + }); + + it("should format lastUpdated time correctly", () => { + // Arrange + const hearingList: FttRptHearingList = [ + { + date: "02/01/2025", + time: "10:00am", + venue: "London", + caseType: "Leasehold", + caseReferenceNumber: "RPT/00001/2025", + judges: "Judge Smith", + members: "", + hearingMethod: "In person", + additionalInformation: "" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Residential Property Tribunal): Southern region", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T14:30:00Z", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List" + }; + + // Act + const result = renderFttRptData(hearingList, options); + + // Assert + expect(result.header.lastUpdatedTime).toBe("2:30pm"); + }); +}); diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.ts b/libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.ts new file mode 100644 index 000000000..13bb7dd58 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/rendering/renderer.ts @@ -0,0 +1,47 @@ +import { formatDdMmYyyyDate, formatDisplayDate, formatLastUpdatedDateTime } from "@hmcts/list-types-common"; +import type { FttRptHearing, FttRptHearingList } from "../models/types.js"; + +export interface RenderOptions { + locale: string; + courtName: string; + contentDate: Date; + lastReceivedDate: string; + listTitle: string; +} + +export interface RenderedData { + header: { + listTitle: string; + weekCommencingDate: string; + lastUpdatedDate: string; + lastUpdatedTime: string; + }; + hearings: FttRptHearing[]; +} + +export function renderFttRptData(hearingList: FttRptHearingList, options: RenderOptions): RenderedData { + const weekCommencingDate = formatDisplayDate(options.contentDate, options.locale); + const { date: lastUpdatedDate, time: lastUpdatedTime } = formatLastUpdatedDateTime(options.lastReceivedDate, options.locale); + + const renderedHearings = hearingList.map((hearing) => ({ + date: formatDdMmYyyyDate(hearing.date, options.locale), + time: hearing.time, + venue: hearing.venue, + caseType: hearing.caseType, + caseReferenceNumber: hearing.caseReferenceNumber, + judges: hearing.judges, + members: hearing.members, + hearingMethod: hearing.hearingMethod, + additionalInformation: hearing.additionalInformation + })); + + return { + header: { + listTitle: options.listTitle, + weekCommencingDate, + lastUpdatedDate, + lastUpdatedTime + }, + hearings: renderedHearings + }; +} diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/schemas/ftt-rpt-weekly-hearing-list.json b/libs/list-types/ftt-rpt-weekly-hearing-list/src/schemas/ftt-rpt-weekly-hearing-list.json new file mode 100644 index 000000000..d4ab398c1 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/schemas/ftt-rpt-weekly-hearing-list.json @@ -0,0 +1,66 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "First-tier Tribunal (Residential Property Tribunal) Weekly Hearing List", + "description": "Schema for First-tier Tribunal (Residential Property Tribunal) Weekly Hearing Lists from pip-data-management", + "type": "array", + "items": { + "type": "object", + "required": ["date", "time", "venue", "caseType", "caseReferenceNumber", "judges", "members", "hearingMethod", "additionalInformation"], + "properties": { + "date": { + "title": "Date", + "type": "string", + "pattern": "^\\d{2}/\\d{2}/\\d{4}$", + "examples": ["02/01/2025"] + }, + "time": { + "title": "Time", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["10:00am"] + }, + "venue": { + "title": "Venue", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["London"] + }, + "caseType": { + "title": "Case Type", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Leasehold"] + }, + "caseReferenceNumber": { + "title": "Case Reference Number", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["RPT/00001/2025"] + }, + "judges": { + "title": "Judge(s)", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Judge Smith"] + }, + "members": { + "title": "Member(s)", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Member Jones"] + }, + "hearingMethod": { + "title": "Hearing Method", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["In person"] + }, + "additionalInformation": { + "title": "Additional Information", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Remote hearing"] + } + } + } +} diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/src/views/ftt-rpt-weekly-hearing-list.njk b/libs/list-types/ftt-rpt-weekly-hearing-list/src/views/ftt-rpt-weekly-hearing-list.njk new file mode 100644 index 000000000..5da1848af --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/src/views/ftt-rpt-weekly-hearing-list.njk @@ -0,0 +1,82 @@ +{% extends "layouts/base-template.njk" %} + + +{% block page_content %} +
+
+ +

{{ header.listTitle }}

+ +

+ {{ t.factLinkText }} {{ t.factAdditionalText }} +

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+ +
+ + + {{ t.importantInformationTitle }} + + +
+

{{ t.importantInformationText }}

+

+ + {{ t.importantInformationLinkText }} + +

+
+
+ +
+

{{ t.searchCasesTitle }}

+ + +
+ +
+ + + + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.time }}{{ t.tableHeaders.venue }}{{ t.tableHeaders.caseType }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.judges }}{{ t.tableHeaders.members }}{{ t.tableHeaders.hearingMethod }}{{ t.tableHeaders.additionalInformation }}
{{ hearing.date }}{{ hearing.time }}{{ hearing.venue }}{{ hearing.caseType }}{{ hearing.caseReferenceNumber }}{{ hearing.judges }}{{ hearing.members }}{{ hearing.hearingMethod }}{{ hearing.additionalInformation }}
+
+ +

{{ t.dataSource }}: {{ dataSource }}

+ + + +
+
+{% endblock %} diff --git a/libs/list-types/ftt-rpt-weekly-hearing-list/tsconfig.json b/libs/list-types/ftt-rpt-weekly-hearing-list/tsconfig.json new file mode 100644 index 000000000..a029ee937 --- /dev/null +++ b/libs/list-types/ftt-rpt-weekly-hearing-list/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules", "src/assets/"] +} diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/package.json b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/package.json new file mode 100644 index 000000000..068745056 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/package.json @@ -0,0 +1,41 @@ +{ + "name": "@hmcts/ftt-tax-chamber-weekly-hearing-list", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "production": "./dist/index.js", + "default": "./src/index.ts" + }, + "./config": { + "production": "./dist/config.js", + "default": "./src/config.ts" + } + }, + "scripts": { + "build": "tsc && yarn build:nunjucks && yarn build:schemas", + "build:nunjucks": "mkdir -p dist/pdf && cd src/pdf && find . -name '*.njk' -exec sh -c 'mkdir -p ../../dist/pdf/$(dirname {}) && cp {} ../../dist/pdf/{}' \\;", + "build:schemas": "mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/", + "dev": "tsc --watch", + "test": "vitest run", + "test:watch": "vitest watch", + "format": "biome format --write .", + "lint": "biome check .", + "lint:fix": "biome check --write --unsafe ." + }, + "dependencies": { + "@hmcts/list-types-common": "workspace:*", + "@hmcts/pdf-generation": "workspace:*", + "luxon": "3.7.2", + "nunjucks": "3.2.4" + }, + "devDependencies": { + "@types/luxon": "3.7.1", + "@types/node": "24.10.4", + "typescript": "6.0.3", + "vitest": "4.1.9" + }, + "peerDependencies": { + "express": "^5.1.0" + } +} diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.test.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.test.ts new file mode 100644 index 000000000..dad18f7dd --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.test.ts @@ -0,0 +1,48 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { assets, moduleRoot } from "./config.js"; + +describe("ftt-tax-chamber-weekly-hearing-list config", () => { + describe("moduleRoot", () => { + it("should be defined", () => { + expect(moduleRoot).toBeDefined(); + expect(typeof moduleRoot).toBe("string"); + }); + + it("should point to an existing directory", () => { + expect(existsSync(moduleRoot)).toBe(true); + }); + + it("should be an absolute path", () => { + expect(path.isAbsolute(moduleRoot)).toBe(true); + }); + }); + + describe("assets", () => { + it("should be defined", () => { + expect(assets).toBeDefined(); + expect(typeof assets).toBe("string"); + }); + + it("should point to assets directory", () => { + expect(assets).toContain("assets"); + }); + + it("should be an absolute path", () => { + expect(path.isAbsolute(assets)).toBe(true); + }); + + it("should have valid path structure", () => { + expect(assets).toBeTruthy(); + }); + + it("should end with trailing slash", () => { + expect(assets).toMatch(/\/$/); + }); + + it("should be subdirectory of moduleRoot", () => { + expect(assets.startsWith(moduleRoot)).toBe(true); + }); + }); +}); diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.ts new file mode 100644 index 000000000..79bbf2632 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config.ts @@ -0,0 +1,9 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const moduleRoot = __dirname; +export const assets = path.join(__dirname, "assets/"); +export const schemaPath = path.join(__dirname, "schemas/ftt-tax-chamber-weekly-hearing-list.json"); diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/conversion/ftt-tax-config.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/conversion/ftt-tax-config.ts new file mode 100644 index 000000000..eb988d900 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/conversion/ftt-tax-config.ts @@ -0,0 +1,64 @@ +import { + createConverter, + DD_MM_YYYY_PATTERN, + type ExcelConverterConfig, + registerConverter, + registerConverterByName, + validateDateFormat, + validateNoHtmlTags +} from "@hmcts/list-types-common"; + +// First-tier Tribunal (Tax Chamber) Weekly Hearing List (listTypeId: 31) +export const FTT_TAX_EXCEL_CONFIG: ExcelConverterConfig = { + fields: [ + { + header: "Date", + fieldName: "date", + required: true, + validators: [validateDateFormat(DD_MM_YYYY_PATTERN, "dd/MM/yyyy (e.g., 02/01/2025)")] + }, + { + header: "Hearing Time", + fieldName: "hearingTime", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Hearing Time", rowNumber)] + }, + { + header: "Case Name", + fieldName: "caseName", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Case Name", rowNumber)] + }, + { + header: "Case Reference Number", + fieldName: "caseReferenceNumber", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Case Reference Number", rowNumber)] + }, + { + header: "Judge(s)", + fieldName: "judges", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Judge(s)", rowNumber)] + }, + { + header: "Member(s)", + fieldName: "members", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Member(s)", rowNumber)] + }, + { + header: "Venue/Platform", + fieldName: "venuePlatform", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Venue/Platform", rowNumber)] + } + ], + minRows: 1 +}; + +// Register the FTT Tax converter with listTypeId 31 and by name +// Name-based registration handles environments where the DB ID differs from the canonical seeded ID +const fttTaxConverter = createConverter(FTT_TAX_EXCEL_CONFIG); +registerConverter(31, fttTaxConverter); +registerConverterByName("FTT_TAX_CHAMBER_WEEKLY_HEARING_LIST", fttTaxConverter); diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.test.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.test.ts new file mode 100644 index 000000000..7e6faa0fc --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "vitest"; +import type { FttTaxChamberHearingList } from "../models/types.js"; +import { extractCaseSummary, formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING } from "./summary-builder.js"; + +describe("SPECIAL_CATEGORY_DATA_WARNING", () => { + it("should contain required warning text", () => { + expect(SPECIAL_CATEGORY_DATA_WARNING).toContain("Special Category Data"); + expect(SPECIAL_CATEGORY_DATA_WARNING).toContain("Data Protection Act 2018"); + }); +}); + +describe("extractCaseSummary", () => { + it("should extract case summaries from hearing list", () => { + // Arrange + const hearingList: FttTaxChamberHearingList = [ + { + date: "01/01/2025", + hearingTime: "10:00am", + caseName: "A Vs HMRC", + caseReferenceNumber: "TC/00001/2025", + judges: "Judge Smith", + members: "", + venuePlatform: "London" + }, + { + date: "02/01/2025", + hearingTime: "2:00pm", + caseName: "B Vs HMRC", + caseReferenceNumber: "TC/00002/2025", + judges: "Judge Brown", + members: "Member Jones", + venuePlatform: "Manchester" + } + ]; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(2); + expect(result[0]).toEqual([ + { label: "Date", value: "01/01/2025" }, + { label: "Hearing time", value: "10:00am" }, + { label: "Case reference number", value: "TC/00001/2025" } + ]); + expect(result[1]).toEqual([ + { label: "Date", value: "02/01/2025" }, + { label: "Hearing time", value: "2:00pm" }, + { label: "Case reference number", value: "TC/00002/2025" } + ]); + }); + + it("should handle empty hearing list", () => { + // Arrange + const hearingList: FttTaxChamberHearingList = []; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(0); + }); + + it("should handle missing case details with empty string", () => { + // Arrange + const hearingList: FttTaxChamberHearingList = [ + { + date: "", + hearingTime: "", + caseName: "", + caseReferenceNumber: "", + judges: "", + members: "", + venuePlatform: "" + } + ]; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(1); + expect(result[0]).toEqual([ + { label: "Date", value: "" }, + { label: "Hearing time", value: "" }, + { label: "Case reference number", value: "" } + ]); + }); +}); + +describe("formatCaseSummaryForEmail", () => { + it("should format single case summary correctly", () => { + // Arrange + const items = [ + [ + { label: "Date", value: "01/01/2025" }, + { label: "Hearing time", value: "10:00am" }, + { label: "Case reference number", value: "TC/00001/2025" } + ] + ]; + + // Act + const result = formatCaseSummaryForEmail(items); + + // Assert + expect(result).toContain("---"); + expect(result).toContain("Date - 01/01/2025"); + expect(result).toContain("Hearing time - 10:00am"); + expect(result).toContain("Case reference number - TC/00001/2025"); + }); + + it("should handle empty case list", () => { + // Act + const result = formatCaseSummaryForEmail([]); + + // Assert + expect(result).toBe("No cases scheduled."); + }); +}); diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.ts new file mode 100644 index 000000000..baf114a20 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/email-summary/summary-builder.ts @@ -0,0 +1,12 @@ +import { type CaseSummary, formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING } from "@hmcts/list-types-common"; +import type { FttTaxChamberHearingList } from "../models/types.js"; + +export { formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING }; + +export function extractCaseSummary(jsonData: FttTaxChamberHearingList): CaseSummary[] { + return jsonData.map((hearing) => [ + { label: "Date", value: hearing.date || "" }, + { label: "Hearing time", value: hearing.hearingTime || "" }, + { label: "Case reference number", value: hearing.caseReferenceNumber || "" } + ]); +} diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/index.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/index.ts new file mode 100644 index 000000000..031ef210c --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/index.ts @@ -0,0 +1,11 @@ +import "./conversion/ftt-tax-config.js"; // Register converter on module load + +// Business logic exports +export type { ValidationResult } from "@hmcts/publication"; +export * from "./email-summary/summary-builder.js"; +export { cy as fttTaxChamberWeeklyHearingListCy } from "./locales/cy.js"; +// Locale exports +export { en as fttTaxChamberWeeklyHearingListEn } from "./locales/en.js"; +export * from "./models/types.js"; +export * from "./pdf/pdf-generator.js"; +export * from "./rendering/renderer.js"; diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/cy.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/cy.ts new file mode 100644 index 000000000..8cd266568 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/cy.ts @@ -0,0 +1,38 @@ +import { provenanceLabelsCy as provenanceLabels } from "@hmcts/list-types-common"; + +export const cy = { + pageTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + listForWeekCommencing: "List for week commencing", + lastUpdated: "Last updated", + at: "at", + factLinkText: "Find contact details and other information about courts and tribunals", + factLinkUrl: "https://www.find-court-tribunal.service.gov.uk/", + factAdditionalText: "in England and Wales, and some non-devolved tribunals in Scotland.", + importantInformationTitle: "Important information", + importantInformationParagraphs: [ + "Open justice is a fundamental principle of our justice system. You can attend a public hearing in person, or you can apply for permission to observe remotely.", + "Members of the public and the media can ask to join any telephone or video hearing remotely. Contact the Tribunal before the hearing to ask for permission to attend by emailing taxappeals@justice.gov.uk.", + "The subject line for the email should contain the following wording: \"HEARING ACCESS REQUEST – [Appellant's name] v [Respondent's name, for example HMRC] – [case reference] – [hearing date]\". You will be sent instructions on how to join the hearing.", + "The judge may refuse a request and can also decide a hearing must be held in private, in such cases you will not be able to attend." + ], + importantInformationLinkText: "Observe a court or tribunal hearing as a journalist, researcher or member of the public", + importantInformationLinkUrl: "https://www.gov.uk/guidance/observe-a-court-or-tribunal-hearing", + searchCasesTitle: "Search Cases", + searchCasesLabel: "Search by case name, date, judge, or other details", + tableHeaders: { + date: "Date", + hearingTime: "Hearing time", + caseName: "Case name", + caseReferenceNumber: "Case reference number", + judges: "Judge(s)", + members: "Member(s)", + venuePlatform: "Venue/Platform" + }, + dataSource: "Data source", + backToTop: "Back to top", + cautionNote: + "Note this document contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be handled appropriately.", + cautionReporting: + "This document contains information intended to assist the accurate reporting of court proceedings. It is vital you ensure that you safeguard the Special Category Data included and abide by reporting restrictions (for example on victims and children). HMCTS will stop sending the data if there is concern about how it will be used.", + provenanceLabels +}; diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/en.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/en.ts new file mode 100644 index 000000000..806465103 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/locales/en.ts @@ -0,0 +1,38 @@ +import { provenanceLabelsEn as provenanceLabels } from "@hmcts/list-types-common"; + +export const en = { + pageTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + listForWeekCommencing: "List for week commencing", + lastUpdated: "Last updated", + at: "at", + factLinkText: "Find contact details and other information about courts and tribunals", + factLinkUrl: "https://www.find-court-tribunal.service.gov.uk/", + factAdditionalText: "in England and Wales, and some non-devolved tribunals in Scotland.", + importantInformationTitle: "Important information", + importantInformationParagraphs: [ + "Open justice is a fundamental principle of our justice system. You can attend a public hearing in person, or you can apply for permission to observe remotely.", + "Members of the public and the media can ask to join any telephone or video hearing remotely. Contact the Tribunal before the hearing to ask for permission to attend by emailing taxappeals@justice.gov.uk.", + "The subject line for the email should contain the following wording: \"HEARING ACCESS REQUEST – [Appellant's name] v [Respondent's name, for example HMRC] – [case reference] – [hearing date]\". You will be sent instructions on how to join the hearing.", + "The judge may refuse a request and can also decide a hearing must be held in private, in such cases you will not be able to attend." + ], + importantInformationLinkText: "Observe a court or tribunal hearing as a journalist, researcher or member of the public", + importantInformationLinkUrl: "https://www.gov.uk/guidance/observe-a-court-or-tribunal-hearing", + searchCasesTitle: "Search Cases", + searchCasesLabel: "Search by case name, date, judge, or other details", + tableHeaders: { + date: "Date", + hearingTime: "Hearing time", + caseName: "Case name", + caseReferenceNumber: "Case reference number", + judges: "Judge(s)", + members: "Member(s)", + venuePlatform: "Venue/Platform" + }, + dataSource: "Data source", + backToTop: "Back to top", + cautionNote: + "Note this document contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be handled appropriately.", + cautionReporting: + "This document contains information intended to assist the accurate reporting of court proceedings. It is vital you ensure that you safeguard the Special Category Data included and abide by reporting restrictions (for example on victims and children). HMCTS will stop sending the data if there is concern about how it will be used.", + provenanceLabels +}; diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/models/types.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/models/types.ts new file mode 100644 index 000000000..5e17059ab --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/models/types.ts @@ -0,0 +1,11 @@ +export interface FttTaxChamberHearing { + date: string; + hearingTime: string; + caseName: string; + caseReferenceNumber: string; + judges: string; + members: string; + venuePlatform: string; +} + +export type FttTaxChamberHearingList = FttTaxChamberHearing[]; diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.test.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.test.ts new file mode 100644 index 000000000..a63673ab9 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.test.ts @@ -0,0 +1,150 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockUploadBlob } = vi.hoisted(() => ({ + mockUploadBlob: vi.fn() +})); +vi.mock("@hmcts/azure-blob", () => ({ + uploadBlob: mockUploadBlob, + CONTAINER: { ARTEFACT: "artefact", PUBLICATIONS: "publications" } +})); + +vi.mock("@hmcts/pdf-generation", () => ({ + generatePdfFromHtml: vi.fn() +})); + +vi.mock("../rendering/renderer.js", () => ({ + renderFttTaxChamberData: vi.fn() +})); + +import { generatePdfFromHtml } from "@hmcts/pdf-generation"; +import { renderFttTaxChamberData } from "../rendering/renderer.js"; +import { generateFttTaxChamberWeeklyHearingListPdf } from "./pdf-generator.js"; + +const mockRenderedData = { + header: { + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + weekCommencingDate: "01 January 2025", + lastUpdatedDate: "12 November 2025", + lastUpdatedTime: "9am" + }, + hearings: [] +}; + +const mockHearingList = [ + { + date: "01/01/2025", + hearingTime: "10:00am", + caseName: "A Vs HMRC", + caseReferenceNumber: "TC/00001/2025", + judges: "Judge Smith", + members: "", + venuePlatform: "London" + } +]; + +describe("generateFttTaxChamberWeeklyHearingListPdf", () => { + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(renderFttTaxChamberData).mockReturnValue(mockRenderedData); + mockUploadBlob.mockResolvedValue(undefined); + }); + + it("should generate PDF successfully", async () => { + // Arrange + const pdfBuffer = Buffer.from("PDF content"); + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer, + sizeBytes: 1024 + }); + + // Act + const result = await generateFttTaxChamberWeeklyHearingListPdf({ + artefactId: "test-artefact-123", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList + }); + + // Assert + expect(result.success).toBe(true); + expect(result.pdfPath).toContain("test-artefact-123.pdf"); + expect(result.sizeBytes).toBe(1024); + expect(result.exceedsMaxSize).toBe(false); + }); + + it("should return exceedsMaxSize true when PDF is over 2MB", async () => { + // Arrange + const largePdfBuffer = Buffer.alloc(3 * 1024 * 1024); + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer: largePdfBuffer, + sizeBytes: 3 * 1024 * 1024 + }); + + // Act + const result = await generateFttTaxChamberWeeklyHearingListPdf({ + artefactId: "large-pdf-123", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList + }); + + // Assert + expect(result.success).toBe(true); + expect(result.exceedsMaxSize).toBe(true); + }); + + it("should return error when PDF generation fails", async () => { + // Arrange + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: false, + error: "Puppeteer crashed" + }); + + // Act + const result = await generateFttTaxChamberWeeklyHearingListPdf({ + artefactId: "failed-pdf", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList + }); + + // Assert + expect(result.success).toBe(false); + expect(result.error).toBe("Puppeteer crashed"); + }); + + it("should pass correct render options to renderer", async () => { + // Arrange + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer: Buffer.from("PDF"), + sizeBytes: 100 + }); + + const contentDate = new Date("2025-06-15"); + + // Act + await generateFttTaxChamberWeeklyHearingListPdf({ + artefactId: "test-render-options", + contentDate, + locale: "cy", + locationId: "999", + jsonData: mockHearingList + }); + + // Assert + expect(renderFttTaxChamberData).toHaveBeenCalledWith(mockHearingList, { + locale: "cy", + courtName: "First-tier Tribunal (Tax Chamber)", + contentDate, + lastReceivedDate: expect.any(String), + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List" + }); + }); +}); diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.ts new file mode 100644 index 000000000..3c9889027 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-generator.ts @@ -0,0 +1,28 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { type BasePdfGenerationOptions, generateFttSiacWeeklyHearingListPdf, type PdfGenerationResult } from "@hmcts/list-types-common"; +import { generatePdfFromHtml } from "@hmcts/pdf-generation"; +import { PROVENANCE_LABELS } from "@hmcts/publication"; +import type { FttTaxChamberHearingList } from "../models/types.js"; +import { renderFttTaxChamberData } from "../rendering/renderer.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +interface PdfGenerationOptions extends BasePdfGenerationOptions { + contentDate: Date; +} + +export async function generateFttTaxChamberWeeklyHearingListPdf(options: PdfGenerationOptions): Promise { + return generateFttSiacWeeklyHearingListPdf({ + ...options, + courtName: "First-tier Tribunal (Tax Chamber)", + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + moduleDir: __dirname, + provenanceLabel: options.provenance ? PROVENANCE_LABELS[options.provenance as keyof typeof PROVENANCE_LABELS] || options.provenance : "", + importEn: () => import("../locales/en.js"), + importCy: () => import("../locales/cy.js"), + generatePdf: generatePdfFromHtml, + renderData: renderFttTaxChamberData + }); +} diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-template.njk b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-template.njk new file mode 100644 index 000000000..617bf8967 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/pdf/pdf-template.njk @@ -0,0 +1,68 @@ + + + + + + {{ header.listTitle }} + + + +
+

{{ header.listTitle }}

+ +

{{ t.factLinkText }} {{ t.factAdditionalText }}

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+
+ +
+

{{ t.importantInformationTitle }}

+ {% for paragraph in t.importantInformationParagraphs %} +

{{ paragraph }}

+ {% endfor %} +

{{ t.importantInformationLinkText }}

+
+ + {% if hearings.length > 0 %} + + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.hearingTime }}{{ t.tableHeaders.caseName }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.judges }}{{ t.tableHeaders.members }}{{ t.tableHeaders.venuePlatform }}
{{ hearing.date }}{{ hearing.hearingTime }}{{ hearing.caseName }}{{ hearing.caseReferenceNumber }}{{ hearing.judges }}{{ hearing.members }}{{ hearing.venuePlatform }}
+ {% else %} +

No hearings scheduled.

+ {% endif %} + + + + diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.test.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.test.ts new file mode 100644 index 000000000..d4410adc9 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from "vitest"; +import type { FttTaxChamberHearingList } from "../models/types.js"; +import { renderFttTaxChamberData } from "./renderer.js"; + +describe("renderFttTaxChamberData", () => { + it("should render hearing list with formatted dates", () => { + // Arrange + const hearingList: FttTaxChamberHearingList = [ + { + date: "02/01/2025", + hearingTime: "10:00am", + caseName: "A Vs HMRC", + caseReferenceNumber: "TC/00001/2025", + judges: "Judge Smith", + members: "Member Jones", + venuePlatform: "London" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Tax Chamber)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List" + }; + + // Act + const result = renderFttTaxChamberData(hearingList, options); + + // Assert + expect(result.header.listTitle).toBe("First-tier Tribunal (Tax Chamber) Weekly Hearing List"); + expect(result.header.weekCommencingDate).toBe("02 January 2025"); + expect(result.header.lastUpdatedDate).toBe("01 January 2025"); + expect(result.header.lastUpdatedTime).toContain("am"); + + expect(result.hearings).toHaveLength(1); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[0].hearingTime).toBe("10:00am"); + expect(result.hearings[0].caseName).toBe("A Vs HMRC"); + expect(result.hearings[0].caseReferenceNumber).toBe("TC/00001/2025"); + expect(result.hearings[0].judges).toBe("Judge Smith"); + expect(result.hearings[0].members).toBe("Member Jones"); + expect(result.hearings[0].venuePlatform).toBe("London"); + }); + + it("should render multiple hearings", () => { + // Arrange + const hearingList: FttTaxChamberHearingList = [ + { + date: "02/01/2025", + hearingTime: "10:00am", + caseName: "A Vs HMRC", + caseReferenceNumber: "TC/00001/2025", + judges: "Judge Smith", + members: "", + venuePlatform: "London" + }, + { + date: "03/01/2025", + hearingTime: "2:00pm", + caseName: "B Vs HMRC", + caseReferenceNumber: "TC/00002/2025", + judges: "Judge Brown", + members: "Member Jones", + venuePlatform: "Manchester" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Tax Chamber)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List" + }; + + // Act + const result = renderFttTaxChamberData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(2); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[1].date).toBe("03 January 2025"); + }); + + it("should handle empty hearing list", () => { + // Arrange + const hearingList: FttTaxChamberHearingList = []; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Tax Chamber)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List" + }; + + // Act + const result = renderFttTaxChamberData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(0); + expect(result.header.listTitle).toBe("First-tier Tribunal (Tax Chamber) Weekly Hearing List"); + }); + + it("should format lastUpdated time correctly", () => { + // Arrange + const hearingList: FttTaxChamberHearingList = [ + { + date: "02/01/2025", + hearingTime: "10:00am", + caseName: "A Vs HMRC", + caseReferenceNumber: "TC/00001/2025", + judges: "Judge Smith", + members: "", + venuePlatform: "London" + } + ]; + + const options = { + locale: "en", + courtName: "First-tier Tribunal (Tax Chamber)", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T14:30:00Z", + listTitle: "First-tier Tribunal (Tax Chamber) Weekly Hearing List" + }; + + // Act + const result = renderFttTaxChamberData(hearingList, options); + + // Assert + expect(result.header.lastUpdatedTime).toBe("2:30pm"); + }); +}); diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.ts b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.ts new file mode 100644 index 000000000..0af31f956 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/rendering/renderer.ts @@ -0,0 +1,45 @@ +import { formatDdMmYyyyDate, formatDisplayDate, formatLastUpdatedDateTime } from "@hmcts/list-types-common"; +import type { FttTaxChamberHearing, FttTaxChamberHearingList } from "../models/types.js"; + +export interface RenderOptions { + locale: string; + courtName: string; + contentDate: Date; + lastReceivedDate: string; + listTitle: string; +} + +export interface RenderedData { + header: { + listTitle: string; + weekCommencingDate: string; + lastUpdatedDate: string; + lastUpdatedTime: string; + }; + hearings: FttTaxChamberHearing[]; +} + +export function renderFttTaxChamberData(hearingList: FttTaxChamberHearingList, options: RenderOptions): RenderedData { + const weekCommencingDate = formatDisplayDate(options.contentDate, options.locale); + const { date: lastUpdatedDate, time: lastUpdatedTime } = formatLastUpdatedDateTime(options.lastReceivedDate, options.locale); + + const renderedHearings = hearingList.map((hearing) => ({ + date: formatDdMmYyyyDate(hearing.date, options.locale), + hearingTime: hearing.hearingTime, + caseName: hearing.caseName, + caseReferenceNumber: hearing.caseReferenceNumber, + judges: hearing.judges, + members: hearing.members, + venuePlatform: hearing.venuePlatform + })); + + return { + header: { + listTitle: options.listTitle, + weekCommencingDate, + lastUpdatedDate, + lastUpdatedTime + }, + hearings: renderedHearings + }; +} diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/schemas/ftt-tax-chamber-weekly-hearing-list.json b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/schemas/ftt-tax-chamber-weekly-hearing-list.json new file mode 100644 index 000000000..0c84f39a5 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/schemas/ftt-tax-chamber-weekly-hearing-list.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + "description": "Schema for First-tier Tribunal (Tax Chamber) Weekly Hearing List from pip-data-management", + "type": "array", + "items": { + "type": "object", + "required": ["date", "hearingTime", "caseName", "caseReferenceNumber", "judges", "members", "venuePlatform"], + "properties": { + "date": { + "title": "Date", + "type": "string", + "pattern": "^\\d{2}/\\d{2}/\\d{4}$", + "examples": ["02/01/2025"] + }, + "hearingTime": { + "title": "Hearing Time", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["10:00am"] + }, + "caseName": { + "title": "Case Name", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["A Vs HMRC"] + }, + "caseReferenceNumber": { + "title": "Case Reference Number", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["TC/00001/2025"] + }, + "judges": { + "title": "Judge(s)", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Judge Smith"] + }, + "members": { + "title": "Member(s)", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Member Jones"] + }, + "venuePlatform": { + "title": "Venue/Platform", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["London"] + } + } + } +} diff --git a/libs/list-types/ftt-tax-chamber-weekly-hearing-list/tsconfig.json b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/tsconfig.json new file mode 100644 index 000000000..a029ee937 --- /dev/null +++ b/libs/list-types/ftt-tax-chamber-weekly-hearing-list/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules", "src/assets/"] +} diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/package.json b/libs/list-types/siac-poac-paac-weekly-hearing-list/package.json new file mode 100644 index 000000000..f068f6aa2 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/package.json @@ -0,0 +1,42 @@ +{ + "name": "@hmcts/siac-poac-paac-weekly-hearing-list", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "production": "./dist/index.js", + "default": "./src/index.ts" + }, + "./config": { + "production": "./dist/config.js", + "default": "./src/config.ts" + } + }, + "scripts": { + "build": "tsc && yarn build:nunjucks && yarn build:views && yarn build:schemas", + "build:nunjucks": "mkdir -p dist/pdf && cd src/pdf && find . -name '*.njk' -exec sh -c 'mkdir -p ../../dist/pdf/$(dirname {}) && cp {} ../../dist/pdf/{}' \\;", + "build:views": "mkdir -p dist/views && find src/views -name '*.njk' -exec cp {} dist/views/ \\;", + "build:schemas": "mkdir -p dist/schemas && cp src/schemas/*.json dist/schemas/", + "dev": "tsc --watch", + "test": "vitest run", + "test:watch": "vitest watch", + "format": "biome format --write .", + "lint": "biome check .", + "lint:fix": "biome check --write --unsafe ." + }, + "dependencies": { + "@hmcts/list-types-common": "workspace:*", + "@hmcts/pdf-generation": "workspace:*", + "luxon": "3.7.2", + "nunjucks": "3.2.4" + }, + "devDependencies": { + "@types/luxon": "3.7.1", + "@types/node": "24.10.4", + "typescript": "6.0.3", + "vitest": "4.1.9" + }, + "peerDependencies": { + "express": "^5.1.0" + } +} diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.test.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.test.ts new file mode 100644 index 000000000..10f207cca --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.test.ts @@ -0,0 +1,48 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { assets, moduleRoot } from "./config.js"; + +describe("siac-poac-paac-weekly-hearing-list config", () => { + describe("moduleRoot", () => { + it("should be defined", () => { + expect(moduleRoot).toBeDefined(); + expect(typeof moduleRoot).toBe("string"); + }); + + it("should point to an existing directory", () => { + expect(existsSync(moduleRoot)).toBe(true); + }); + + it("should be an absolute path", () => { + expect(path.isAbsolute(moduleRoot)).toBe(true); + }); + }); + + describe("assets", () => { + it("should be defined", () => { + expect(assets).toBeDefined(); + expect(typeof assets).toBe("string"); + }); + + it("should point to assets directory", () => { + expect(assets).toContain("assets"); + }); + + it("should be an absolute path", () => { + expect(path.isAbsolute(assets)).toBe(true); + }); + + it("should have valid path structure", () => { + expect(assets).toBeTruthy(); + }); + + it("should end with trailing slash", () => { + expect(assets).toMatch(/\/$/); + }); + + it("should be subdirectory of moduleRoot", () => { + expect(assets.startsWith(moduleRoot)).toBe(true); + }); + }); +}); diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.ts new file mode 100644 index 000000000..5a5bd9b11 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/config.ts @@ -0,0 +1,9 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const moduleRoot = __dirname; +export const assets = path.join(__dirname, "assets/"); +export const schemaPath = path.join(__dirname, "schemas/siac-poac-paac-weekly-hearing-list.json"); diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/conversion/siac-poac-paac-config.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/conversion/siac-poac-paac-config.ts new file mode 100644 index 000000000..8a8904a77 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/conversion/siac-poac-paac-config.ts @@ -0,0 +1,71 @@ +import { + createConverter, + DD_MM_YYYY_PATTERN, + type ExcelConverterConfig, + registerConverter, + registerConverterByName, + validateDateFormat, + validateNoHtmlTags +} from "@hmcts/list-types-common"; + +// SIAC / POAC / PAAC Weekly Hearing List (listTypeIds: 28, 29, 30) +export const SIAC_POAC_PAAC_EXCEL_CONFIG: ExcelConverterConfig = { + fields: [ + { + header: "Date", + fieldName: "date", + required: true, + validators: [validateDateFormat(DD_MM_YYYY_PATTERN, "dd/MM/yyyy (e.g., 02/01/2025)")] + }, + { + header: "Time", + fieldName: "time", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Time", rowNumber)] + }, + { + header: "Appellant", + fieldName: "appellant", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Appellant", rowNumber)] + }, + { + header: "Case Reference Number", + fieldName: "caseReferenceNumber", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Case Reference Number", rowNumber)] + }, + { + header: "Hearing Type", + fieldName: "hearingType", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Hearing Type", rowNumber)] + }, + { + header: "Courtroom", + fieldName: "courtroom", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Courtroom", rowNumber)] + }, + { + header: "Additional information", + fieldName: "additionalInformation", + required: true, + validators: [(value, rowNumber) => validateNoHtmlTags(value, "Additional information", rowNumber)] + } + ], + minRows: 1 +}; + +// Register the converter for all three list type IDs and names +// Name-based registration handles environments where the DB ID differs from the canonical seeded ID +const siacPoacPaacConverter = createConverter(SIAC_POAC_PAAC_EXCEL_CONFIG); + +registerConverter(28, siacPoacPaacConverter); +registerConverterByName("SIAC_WEEKLY_HEARING_LIST", siacPoacPaacConverter); + +registerConverter(29, siacPoacPaacConverter); +registerConverterByName("POAC_WEEKLY_HEARING_LIST", siacPoacPaacConverter); + +registerConverter(30, siacPoacPaacConverter); +registerConverterByName("PAAC_WEEKLY_HEARING_LIST", siacPoacPaacConverter); diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.test.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.test.ts new file mode 100644 index 000000000..3f7649a96 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "vitest"; +import type { SiacPoacPaacHearingList } from "../models/types.js"; +import { extractCaseSummary, formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING } from "./summary-builder.js"; + +describe("SPECIAL_CATEGORY_DATA_WARNING", () => { + it("should contain required warning text", () => { + expect(SPECIAL_CATEGORY_DATA_WARNING).toContain("Special Category Data"); + expect(SPECIAL_CATEGORY_DATA_WARNING).toContain("Data Protection Act 2018"); + }); +}); + +describe("extractCaseSummary", () => { + it("should extract case summaries from hearing list", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = [ + { + date: "01/01/2025", + time: "10:00am", + appellant: "Smith v Secretary of State", + caseReferenceNumber: "SC/00001/2025", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "" + }, + { + date: "02/01/2025", + time: "2:00pm", + appellant: "Brown v Secretary of State", + caseReferenceNumber: "SC/00002/2025", + hearingType: "Preliminary hearing", + courtroom: "Court 2", + additionalInformation: "" + } + ]; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(2); + expect(result[0]).toEqual([ + { label: "Date", value: "01/01/2025" }, + { label: "Time", value: "10:00am" }, + { label: "Case reference number", value: "SC/00001/2025" } + ]); + expect(result[1]).toEqual([ + { label: "Date", value: "02/01/2025" }, + { label: "Time", value: "2:00pm" }, + { label: "Case reference number", value: "SC/00002/2025" } + ]); + }); + + it("should handle empty hearing list", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = []; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(0); + }); + + it("should handle missing case details with empty string", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = [ + { + date: "", + time: "", + appellant: "", + caseReferenceNumber: "", + hearingType: "", + courtroom: "", + additionalInformation: "" + } + ]; + + // Act + const result = extractCaseSummary(hearingList); + + // Assert + expect(result).toHaveLength(1); + expect(result[0]).toEqual([ + { label: "Date", value: "" }, + { label: "Time", value: "" }, + { label: "Case reference number", value: "" } + ]); + }); +}); + +describe("formatCaseSummaryForEmail", () => { + it("should format single case summary correctly", () => { + // Arrange + const items = [ + [ + { label: "Date", value: "01/01/2025" }, + { label: "Time", value: "10:00am" }, + { label: "Case reference number", value: "SC/00001/2025" } + ] + ]; + + // Act + const result = formatCaseSummaryForEmail(items); + + // Assert + expect(result).toContain("---"); + expect(result).toContain("Date - 01/01/2025"); + expect(result).toContain("Time - 10:00am"); + expect(result).toContain("Case reference number - SC/00001/2025"); + }); + + it("should handle empty case list", () => { + // Act + const result = formatCaseSummaryForEmail([]); + + // Assert + expect(result).toBe("No cases scheduled."); + }); +}); diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.ts new file mode 100644 index 000000000..60642ae27 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/email-summary/summary-builder.ts @@ -0,0 +1,12 @@ +import { type CaseSummary, formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING } from "@hmcts/list-types-common"; +import type { SiacPoacPaacHearingList } from "../models/types.js"; + +export { formatCaseSummaryForEmail, SPECIAL_CATEGORY_DATA_WARNING }; + +export function extractCaseSummary(jsonData: SiacPoacPaacHearingList): CaseSummary[] { + return jsonData.map((hearing) => [ + { label: "Date", value: hearing.date || "" }, + { label: "Time", value: hearing.time || "" }, + { label: "Case reference number", value: hearing.caseReferenceNumber || "" } + ]); +} diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/index.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/index.ts new file mode 100644 index 000000000..a99ab6959 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/index.ts @@ -0,0 +1,11 @@ +import "./conversion/siac-poac-paac-config.js"; // Register converter on module load + +// Business logic exports +export type { ValidationResult } from "@hmcts/publication"; +export * from "./email-summary/summary-builder.js"; +export { cy as siacPoacPaacWeeklyHearingListCy } from "./locales/cy.js"; +// Locale exports +export { en as siacPoacPaacWeeklyHearingListEn } from "./locales/en.js"; +export * from "./models/types.js"; +export * from "./pdf/pdf-generator.js"; +export * from "./rendering/renderer.js"; diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/cy.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/cy.ts new file mode 100644 index 000000000..3b25453dd --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/cy.ts @@ -0,0 +1,36 @@ +import { provenanceLabelsCy as provenanceLabels } from "@hmcts/list-types-common"; + +export const cy = { + listForWeekCommencing: "List for week commencing", + lastUpdated: "Last updated", + at: "at", + factLinkText: "Find contact details and other information about courts and tribunals", + factLinkUrl: "https://www.find-court-tribunal.service.gov.uk/", + factAdditionalText: "in England and Wales, and some non-devolved tribunals in Scotland.", + importantInformationTitle: "Important information", + searchCasesTitle: "Search Cases", + searchCasesLabel: "Search by case reference number, date, appellant, or other details", + tableHeaders: { + date: "Date", + time: "Time", + appellant: "Appellant", + caseReferenceNumber: "Case reference number", + hearingType: "Hearing type", + courtroom: "Courtroom", + additionalInformation: "Additional information" + }, + dataSource: "Data source", + backToTop: "Back to top", + cautionNote: + "Note this document contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be handled appropriately.", + cautionReporting: + "This document contains information intended to assist the accurate reporting of court proceedings. It is vital you ensure that you safeguard the Special Category Data included and abide by reporting restrictions (for example on victims and children). HMCTS will stop sending the data if there is concern about how it will be used.", + provenanceLabels, + siacPageTitle: "Special Immigration Appeals Commission Weekly Hearing List", + poacPageTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List", + paacPageTitle: "Pathogens Access Appeal Commission Weekly Hearing List", + importantInformationText: "The tribunal sometimes uses reference numbers or initials to protect the anonymity of those involved in the appeal.", + importantInformationVenue: "All hearings take place at Field House, 15-25 Bream's Buildings, London EC4A 1DZ.", + importantInformationLinkText: "Find out what to expect coming to a court or tribunal", + importantInformationLinkUrl: "https://www.gov.uk/guidance/what-to-expect-coming-to-a-court-or-tribunal" +}; diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/en.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/en.ts new file mode 100644 index 000000000..4e1c82fb7 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/en.ts @@ -0,0 +1,36 @@ +import { provenanceLabelsEn as provenanceLabels } from "@hmcts/list-types-common"; + +export const en = { + listForWeekCommencing: "List for week commencing", + lastUpdated: "Last updated", + at: "at", + factLinkText: "Find contact details and other information about courts and tribunals", + factLinkUrl: "https://www.find-court-tribunal.service.gov.uk/", + factAdditionalText: "in England and Wales, and some non-devolved tribunals in Scotland.", + importantInformationTitle: "Important information", + searchCasesTitle: "Search Cases", + searchCasesLabel: "Search by case reference number, date, appellant, or other details", + tableHeaders: { + date: "Date", + time: "Time", + appellant: "Appellant", + caseReferenceNumber: "Case reference number", + hearingType: "Hearing type", + courtroom: "Courtroom", + additionalInformation: "Additional information" + }, + dataSource: "Data source", + backToTop: "Back to top", + cautionNote: + "Note this document contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be handled appropriately.", + cautionReporting: + "This document contains information intended to assist the accurate reporting of court proceedings. It is vital you ensure that you safeguard the Special Category Data included and abide by reporting restrictions (for example on victims and children). HMCTS will stop sending the data if there is concern about how it will be used.", + provenanceLabels, + siacPageTitle: "Special Immigration Appeals Commission Weekly Hearing List", + poacPageTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List", + paacPageTitle: "Pathogens Access Appeal Commission Weekly Hearing List", + importantInformationText: "The tribunal sometimes uses reference numbers or initials to protect the anonymity of those involved in the appeal.", + importantInformationVenue: "All hearings take place at Field House, 15-25 Bream's Buildings, London EC4A 1DZ.", + importantInformationLinkText: "Find out what to expect coming to a court or tribunal", + importantInformationLinkUrl: "https://www.gov.uk/guidance/what-to-expect-coming-to-a-court-or-tribunal" +}; diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/locales.test.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/locales.test.ts new file mode 100644 index 000000000..dbd3bda0e --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/locales/locales.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { cy } from "./cy.js"; +import { en } from "./en.js"; + +describe("siac-poac-paac locales", () => { + describe("en", () => { + it("should have importantInformationText as a separate field without the venue", () => { + expect(en.importantInformationText).toContain("anonymity"); + expect(en.importantInformationText).not.toContain("Field House"); + }); + + it("should have importantInformationVenue as a separate field on a new line", () => { + expect(en.importantInformationVenue).toBe("All hearings take place at Field House, 15-25 Bream's Buildings, London EC4A 1DZ."); + }); + }); + + describe("cy", () => { + it("should have importantInformationText as a separate field without the venue", () => { + expect(cy.importantInformationText).toContain("anonymity"); + expect(cy.importantInformationText).not.toContain("Field House"); + }); + + it("should have importantInformationVenue as a separate field on a new line", () => { + expect(cy.importantInformationVenue).toBe("All hearings take place at Field House, 15-25 Bream's Buildings, London EC4A 1DZ."); + }); + }); +}); diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/models/types.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/models/types.ts new file mode 100644 index 000000000..bbad0319e --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/models/types.ts @@ -0,0 +1,11 @@ +export interface SiacPoacPaacHearing { + date: string; + time: string; + appellant: string; + caseReferenceNumber: string; + hearingType: string; + courtroom: string; + additionalInformation: string; +} + +export type SiacPoacPaacHearingList = SiacPoacPaacHearing[]; diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.test.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.test.ts new file mode 100644 index 000000000..17547ed81 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.test.ts @@ -0,0 +1,158 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockUploadBlob } = vi.hoisted(() => ({ + mockUploadBlob: vi.fn() +})); +vi.mock("@hmcts/azure-blob", () => ({ + uploadBlob: mockUploadBlob, + CONTAINER: { ARTEFACT: "artefact", PUBLICATIONS: "publications" } +})); + +vi.mock("@hmcts/pdf-generation", () => ({ + generatePdfFromHtml: vi.fn() +})); + +vi.mock("../rendering/renderer.js", () => ({ + renderSiacPoacPaacData: vi.fn() +})); + +import { generatePdfFromHtml } from "@hmcts/pdf-generation"; +import { renderSiacPoacPaacData } from "../rendering/renderer.js"; +import { generateSiacPoacPaacWeeklyHearingListPdf } from "./pdf-generator.js"; + +const mockRenderedData = { + header: { + listTitle: "Special Immigration Appeals Commission Weekly Hearing List", + weekCommencingDate: "01 January 2025", + lastUpdatedDate: "12 November 2025", + lastUpdatedTime: "9am" + }, + hearings: [] +}; + +const mockHearingList = [ + { + date: "01/01/2025", + time: "10:00am", + appellant: "Smith v Secretary of State", + caseReferenceNumber: "SC/00001/2025", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "" + } +]; + +describe("generateSiacPoacPaacWeeklyHearingListPdf", () => { + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(renderSiacPoacPaacData).mockReturnValue(mockRenderedData); + mockUploadBlob.mockResolvedValue(undefined); + }); + + it("should generate PDF successfully", async () => { + // Arrange + const pdfBuffer = Buffer.from("PDF content"); + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer, + sizeBytes: 1024 + }); + + // Act + const result = await generateSiacPoacPaacWeeklyHearingListPdf({ + artefactId: "test-artefact-123", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList, + courtName: "Special Immigration Appeals Commission", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }); + + // Assert + expect(result.success).toBe(true); + expect(result.pdfPath).toContain("test-artefact-123.pdf"); + expect(result.sizeBytes).toBe(1024); + expect(result.exceedsMaxSize).toBe(false); + }); + + it("should return exceedsMaxSize true when PDF is over 2MB", async () => { + // Arrange + const largePdfBuffer = Buffer.alloc(3 * 1024 * 1024); + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer: largePdfBuffer, + sizeBytes: 3 * 1024 * 1024 + }); + + // Act + const result = await generateSiacPoacPaacWeeklyHearingListPdf({ + artefactId: "large-pdf-123", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList, + courtName: "Special Immigration Appeals Commission", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }); + + // Assert + expect(result.success).toBe(true); + expect(result.exceedsMaxSize).toBe(true); + }); + + it("should return error when PDF generation fails", async () => { + // Arrange + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: false, + error: "Puppeteer crashed" + }); + + // Act + const result = await generateSiacPoacPaacWeeklyHearingListPdf({ + artefactId: "failed-pdf", + contentDate: new Date("2025-01-01"), + locale: "en", + locationId: "240", + jsonData: mockHearingList, + courtName: "Special Immigration Appeals Commission", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }); + + // Assert + expect(result.success).toBe(false); + expect(result.error).toBe("Puppeteer crashed"); + }); + + it("should pass correct render options to renderer", async () => { + // Arrange + vi.mocked(generatePdfFromHtml).mockResolvedValue({ + success: true, + pdfBuffer: Buffer.from("PDF"), + sizeBytes: 100 + }); + + const contentDate = new Date("2025-06-15"); + + // Act + await generateSiacPoacPaacWeeklyHearingListPdf({ + artefactId: "test-render-options", + contentDate, + locale: "cy", + locationId: "999", + jsonData: mockHearingList, + courtName: "Proscribed Organisations Appeal Commission", + listTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List" + }); + + // Assert + expect(renderSiacPoacPaacData).toHaveBeenCalledWith(mockHearingList, { + locale: "cy", + courtName: "Proscribed Organisations Appeal Commission", + contentDate, + lastReceivedDate: expect.any(String), + listTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List" + }); + }); +}); diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.ts new file mode 100644 index 000000000..cd818790e --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-generator.ts @@ -0,0 +1,28 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { type BasePdfGenerationOptions, generateFttSiacWeeklyHearingListPdf, type PdfGenerationResult } from "@hmcts/list-types-common"; +import { generatePdfFromHtml } from "@hmcts/pdf-generation"; +import { PROVENANCE_LABELS } from "@hmcts/publication"; +import type { SiacPoacPaacHearingList } from "../models/types.js"; +import { renderSiacPoacPaacData } from "../rendering/renderer.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +interface PdfGenerationOptions extends BasePdfGenerationOptions { + contentDate: Date; + courtName: string; + listTitle: string; +} + +export async function generateSiacPoacPaacWeeklyHearingListPdf(options: PdfGenerationOptions): Promise { + return generateFttSiacWeeklyHearingListPdf({ + ...options, + moduleDir: __dirname, + provenanceLabel: options.provenance ? PROVENANCE_LABELS[options.provenance as keyof typeof PROVENANCE_LABELS] || options.provenance : "", + importEn: () => import("../locales/en.js"), + importCy: () => import("../locales/cy.js"), + generatePdf: generatePdfFromHtml, + renderData: renderSiacPoacPaacData + }); +} diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-template.njk b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-template.njk new file mode 100644 index 000000000..3bae0d2e3 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/pdf/pdf-template.njk @@ -0,0 +1,67 @@ + + + + + + {{ header.listTitle }} + + + +
+

{{ header.listTitle }}

+ +

{{ t.factLinkText }} {{ t.factAdditionalText }}

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+
+ +
+

{{ t.importantInformationTitle }}

+

{{ t.importantInformationText }}

+

{{ t.importantInformationVenue }}

+

{{ t.importantInformationLinkText }}

+
+ + {% if hearings.length > 0 %} + + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.time }}{{ t.tableHeaders.appellant }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.hearingType }}{{ t.tableHeaders.courtroom }}{{ t.tableHeaders.additionalInformation }}
{{ hearing.date }}{{ hearing.time }}{{ hearing.appellant }}{{ hearing.caseReferenceNumber }}{{ hearing.hearingType }}{{ hearing.courtroom }}{{ hearing.additionalInformation }}
+ {% else %} +

No hearings scheduled.

+ {% endif %} + + + + diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.test.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.test.ts new file mode 100644 index 000000000..c6178320f --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, it } from "vitest"; +import type { SiacPoacPaacHearingList } from "../models/types.js"; +import { renderSiacPoacPaacData } from "./renderer.js"; + +describe("renderSiacPoacPaacData", () => { + it("should render hearing list with formatted dates", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = [ + { + date: "02/01/2025", + time: "10:00am", + appellant: "A Vs B", + caseReferenceNumber: "SC/00001/2025", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "Remote hearing" + } + ]; + + const options = { + locale: "en", + courtName: "Special Immigration Appeals Commission", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }; + + // Act + const result = renderSiacPoacPaacData(hearingList, options); + + // Assert + expect(result.header.listTitle).toBe("Special Immigration Appeals Commission Weekly Hearing List"); + expect(result.header.weekCommencingDate).toBe("02 January 2025"); + expect(result.header.lastUpdatedDate).toBe("01 January 2025"); + expect(result.header.lastUpdatedTime).toContain("am"); + + expect(result.hearings).toHaveLength(1); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[0].time).toBe("10:00am"); + expect(result.hearings[0].appellant).toBe("A Vs B"); + expect(result.hearings[0].caseReferenceNumber).toBe("SC/00001/2025"); + expect(result.hearings[0].hearingType).toBe("Substantive hearing"); + expect(result.hearings[0].courtroom).toBe("Court 1"); + expect(result.hearings[0].additionalInformation).toBe("Remote hearing"); + }); + + it("should render multiple hearings", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = [ + { + date: "02/01/2025", + time: "10:00am", + appellant: "A Vs B", + caseReferenceNumber: "SC/00001/2025", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "" + }, + { + date: "03/01/2025", + time: "2:00pm", + appellant: "C Vs D", + caseReferenceNumber: "SC/00002/2025", + hearingType: "Preliminary hearing", + courtroom: "Court 2", + additionalInformation: "In person" + } + ]; + + const options = { + locale: "en", + courtName: "Special Immigration Appeals Commission", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }; + + // Act + const result = renderSiacPoacPaacData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(2); + expect(result.hearings[0].date).toBe("02 January 2025"); + expect(result.hearings[1].date).toBe("03 January 2025"); + }); + + it("should format date with zero-padded day correctly", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = [ + { + date: "01/01/2025", + time: "10:00am", + appellant: "A Vs B", + caseReferenceNumber: "SC/00001/2025", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "" + } + ]; + + const options = { + locale: "en", + courtName: "Special Immigration Appeals Commission", + contentDate: new Date(2025, 0, 1), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }; + + // Act + const result = renderSiacPoacPaacData(hearingList, options); + + // Assert + expect(result.hearings[0].date).toBe("01 January 2025"); + }); + + it("should format lastUpdated with time", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = [ + { + date: "02/01/2025", + time: "10:00am", + appellant: "A Vs B", + caseReferenceNumber: "SC/00001/2025", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "" + } + ]; + + const options = { + locale: "en", + courtName: "Special Immigration Appeals Commission", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }; + + // Act + const result = renderSiacPoacPaacData(hearingList, options); + + // Assert + expect(result.header.lastUpdatedTime).toBe("9:55am"); + }); + + it("should handle empty hearing list", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = []; + + const options = { + locale: "en", + courtName: "Special Immigration Appeals Commission", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }; + + // Act + const result = renderSiacPoacPaacData(hearingList, options); + + // Assert + expect(result.hearings).toHaveLength(0); + expect(result.header.listTitle).toBe("Special Immigration Appeals Commission Weekly Hearing List"); + }); + + it("should use the provided listTitle from translations", () => { + // Arrange + const hearingList: SiacPoacPaacHearingList = [ + { + date: "02/01/2025", + time: "10:00am", + appellant: "A Vs B", + caseReferenceNumber: "SC/00001/2025", + hearingType: "Substantive hearing", + courtroom: "Court 1", + additionalInformation: "" + } + ]; + + const optionsWelsh = { + locale: "cy", + courtName: "Special Immigration Appeals Commission", + contentDate: new Date(2025, 0, 2), + lastReceivedDate: "2025-01-01T09:55:00Z", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }; + + // Act + const result = renderSiacPoacPaacData(hearingList, optionsWelsh); + + // Assert + expect(result.header.listTitle).toBe("Special Immigration Appeals Commission Weekly Hearing List"); + }); +}); diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.ts b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.ts new file mode 100644 index 000000000..c55475ef7 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/rendering/renderer.ts @@ -0,0 +1,45 @@ +import { formatDdMmYyyyDate, formatDisplayDate, formatLastUpdatedDateTime } from "@hmcts/list-types-common"; +import type { SiacPoacPaacHearing, SiacPoacPaacHearingList } from "../models/types.js"; + +export interface RenderOptions { + locale: string; + courtName: string; + contentDate: Date; + lastReceivedDate: string; + listTitle: string; +} + +export interface RenderedData { + header: { + listTitle: string; + weekCommencingDate: string; + lastUpdatedDate: string; + lastUpdatedTime: string; + }; + hearings: SiacPoacPaacHearing[]; +} + +export function renderSiacPoacPaacData(hearingList: SiacPoacPaacHearingList, options: RenderOptions): RenderedData { + const weekCommencingDate = formatDisplayDate(options.contentDate, options.locale); + const { date: lastUpdatedDate, time: lastUpdatedTime } = formatLastUpdatedDateTime(options.lastReceivedDate, options.locale); + + const renderedHearings = hearingList.map((hearing) => ({ + date: formatDdMmYyyyDate(hearing.date, options.locale), + time: hearing.time, + appellant: hearing.appellant, + caseReferenceNumber: hearing.caseReferenceNumber, + hearingType: hearing.hearingType, + courtroom: hearing.courtroom, + additionalInformation: hearing.additionalInformation + })); + + return { + header: { + listTitle: options.listTitle, + weekCommencingDate, + lastUpdatedDate, + lastUpdatedTime + }, + hearings: renderedHearings + }; +} diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/schemas/siac-poac-paac-weekly-hearing-list.json b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/schemas/siac-poac-paac-weekly-hearing-list.json new file mode 100644 index 000000000..645ad8cdd --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/schemas/siac-poac-paac-weekly-hearing-list.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SIAC / POAC / PAAC Weekly Hearing List", + "description": "Schema for SIAC, POAC and PAAC Weekly Hearing Lists from pip-data-management", + "type": "array", + "items": { + "type": "object", + "required": ["date", "time", "appellant", "caseReferenceNumber", "hearingType", "courtroom", "additionalInformation"], + "properties": { + "date": { + "title": "Date", + "type": "string", + "pattern": "^\\d{2}/\\d{2}/\\d{4}$", + "examples": ["02/01/2025"] + }, + "time": { + "title": "Time", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["10:00am"] + }, + "appellant": { + "title": "Appellant", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["A Vs B"] + }, + "caseReferenceNumber": { + "title": "Case Reference Number", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["SC/00001/2025"] + }, + "hearingType": { + "title": "Hearing Type", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Substantive hearing"] + }, + "courtroom": { + "title": "Courtroom", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Court 1"] + }, + "additionalInformation": { + "title": "Additional information", + "type": "string", + "pattern": "^(?!(.|\r|\n)*<[^>]+>)(.|\r|\n)*$", + "examples": ["Remote hearing"] + } + } + } +} diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/src/views/siac-poac-paac-weekly-hearing-list.njk b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/views/siac-poac-paac-weekly-hearing-list.njk new file mode 100644 index 000000000..dae0c884d --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/src/views/siac-poac-paac-weekly-hearing-list.njk @@ -0,0 +1,76 @@ +{% extends "layouts/base-template.njk" %} + +{% block page_content %} +
+
+ +

{{ header.listTitle }}

+ +

+ {{ t.factLinkText }} {{ t.factAdditionalText }} +

+ +

{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}

+

{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}

+ +
+ + + {{ t.importantInformationTitle }} + + +
+

{{ t.importantInformationText }}

+

{{ t.importantInformationVenue }}

+

+ {{ t.importantInformationLinkText }} +

+
+
+ +
+

{{ t.searchCasesTitle }}

+ + +
+ +
+ + + + + + + + + + + + + + {% for hearing in hearings %} + + + + + + + + + + {% endfor %} + +
{{ t.tableHeaders.date }}{{ t.tableHeaders.time }}{{ t.tableHeaders.appellant }}{{ t.tableHeaders.caseReferenceNumber }}{{ t.tableHeaders.hearingType }}{{ t.tableHeaders.courtroom }}{{ t.tableHeaders.additionalInformation }}
{{ hearing.date }}{{ hearing.time }}{{ hearing.appellant }}{{ hearing.caseReferenceNumber }}{{ hearing.hearingType }}{{ hearing.courtroom }}{{ hearing.additionalInformation }}
+
+ +

{{ t.dataSource }}: {{ dataSource }}

+ + + +
+
+{% endblock %} diff --git a/libs/list-types/siac-poac-paac-weekly-hearing-list/tsconfig.json b/libs/list-types/siac-poac-paac-weekly-hearing-list/tsconfig.json new file mode 100644 index 000000000..a029ee937 --- /dev/null +++ b/libs/list-types/siac-poac-paac-weekly-hearing-list/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules", "src/assets/"] +} diff --git a/libs/location/src/list-type-data.ts b/libs/location/src/list-type-data.ts index c7a07bcfc..cff325f34 100644 --- a/libs/location/src/list-type-data.ts +++ b/libs/location/src/list-type-data.ts @@ -319,6 +319,126 @@ export const listTypeData: ListTypeData[] = [ }, { id: 28, + name: "SIAC_WEEKLY_HEARING_LIST", + englishFriendlyName: "Special Immigration Appeals Commission Weekly Hearing List", + welshFriendlyName: "Special Immigration Appeals Commission Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "siac-poac-paac-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "SIAC Weekly Hearing List", + subJurisdictionIds: [25] + }, + { + id: 29, + name: "POAC_WEEKLY_HEARING_LIST", + englishFriendlyName: "Proscribed Organisations Appeal Commission Weekly Hearing List", + welshFriendlyName: "Proscribed Organisations Appeal Commission Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "siac-poac-paac-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "POAC Weekly Hearing List", + subJurisdictionIds: [23] + }, + { + id: 30, + name: "PAAC_WEEKLY_HEARING_LIST", + englishFriendlyName: "Pathogens Access Appeal Commission Weekly Hearing List", + welshFriendlyName: "Pathogens Access Appeal Commission Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "siac-poac-paac-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "PACC Weekly Hearing List", + subJurisdictionIds: [21] + }, + { + id: 31, + name: "FTT_TAX_CHAMBER_WEEKLY_HEARING_LIST", + englishFriendlyName: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + welshFriendlyName: "First-tier Tribunal (Tax Chamber) Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "ftt-tax-chamber-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "FFT Tax Weekly Hearing List", + subJurisdictionIds: [16] + }, + { + id: 32, + name: "FTT_LANDS_REGISTRATION_TRIBUNAL_WEEKLY_HEARING_LIST", + englishFriendlyName: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List", + welshFriendlyName: "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "ftt-lands-registration-tribunal-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "FFT (LR) Weekly Hearing List", + subJurisdictionIds: [15] + }, + { + id: 33, + name: "FTT_RPT_EASTERN_WEEKLY_HEARING_LIST", + englishFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List", + welshFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "ftt-rpt-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "FTT (RPT) Eastern Weekly Hearing List", + subJurisdictionIds: [24] + }, + { + id: 34, + name: "FTT_RPT_LONDON_WEEKLY_HEARING_LIST", + englishFriendlyName: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List", + welshFriendlyName: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "ftt-rpt-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "FTT (RPT) London Weekly Hearing List", + subJurisdictionIds: [24] + }, + { + id: 35, + name: "FTT_RPT_MIDLANDS_WEEKLY_HEARING_LIST", + englishFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List", + welshFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "ftt-rpt-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "FTT (RPT) Midlands Weekly Hearing List", + subJurisdictionIds: [24] + }, + { + id: 36, + name: "FTT_RPT_NORTHERN_WEEKLY_HEARING_LIST", + englishFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List", + welshFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "ftt-rpt-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "FTT (RPT) Northern Weekly Hearing List", + subJurisdictionIds: [24] + }, + { + id: 37, + name: "FTT_RPT_SOUTHERN_WEEKLY_HEARING_LIST", + englishFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List", + welshFriendlyName: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List", + provenance: "MANUAL_UPLOAD", + urlPath: "ftt-rpt-weekly-hearing-list", + isNonStrategic: true, + defaultSensitivity: "Public", + shortenedFriendlyName: "FTT (RPT) Southern Weekly Hearing List", + subJurisdictionIds: [24] + }, + { + id: 38, name: "SEND_DAILY_HEARING_LIST", englishFriendlyName: "First-tier Tribunal (Special Educational Needs and Disability) Daily Hearing List", welshFriendlyName: "Rhestr Gwrandawiadau Dyddiol y Tribiwnlys Haen Gyntaf (Anghenion Addysgol Arbennig ac Anabledd)", @@ -330,7 +450,7 @@ export const listTypeData: ListTypeData[] = [ subJurisdictionIds: [18] }, { - id: 29, + id: 39, name: "CIC_WEEKLY_HEARING_LIST", englishFriendlyName: "Criminal Injuries Compensation Weekly Hearing List", welshFriendlyName: "Rhestr Gwrandawiadau Wythnosol yr Iawndal am Anafiadau Troseddol", @@ -342,7 +462,7 @@ export const listTypeData: ListTypeData[] = [ subJurisdictionIds: [14] }, { - id: 30, + id: 40, name: "AST_DAILY_HEARING_LIST", englishFriendlyName: "Asylum Support Tribunal Daily Hearing List", welshFriendlyName: "Rhestr Gwrandawiadau Dyddiol y Tribiwnlys Cymorth Lloches", diff --git a/libs/location/src/location-data.ts b/libs/location/src/location-data.ts index 15d5bd03a..ffb80ee2e 100644 --- a/libs/location/src/location-data.ts +++ b/libs/location/src/location-data.ts @@ -297,7 +297,7 @@ export const locationData: { }, { subJurisdictionId: 15, - name: "First-Tier Tribunal (Lands Registration Tribunal)", + name: "First-Tier Tribunal (Land Registration Tribunal)", welshName: "Tribiwnlys Haen Gyntaf (Tribiwnlys Cofrestru Tir)", jurisdictionId: 4 }, diff --git a/libs/notifications/package.json b/libs/notifications/package.json index d075c1547..127d1e434 100644 --- a/libs/notifications/package.json +++ b/libs/notifications/package.json @@ -25,11 +25,15 @@ "@hmcts/cic-weekly-hearing-list": "workspace:*", "@hmcts/civil-and-family-daily-cause-list": "workspace:*", "@hmcts/court-of-appeal-civil-daily-cause-list": "workspace:*", + "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list": "workspace:*", + "@hmcts/ftt-rpt-weekly-hearing-list": "workspace:*", + "@hmcts/ftt-tax-chamber-weekly-hearing-list": "workspace:*", "@hmcts/list-types-common": "workspace:*", "@hmcts/london-administrative-court-daily-cause-list": "workspace:*", "@hmcts/postgres-prisma": "workspace:*", "@hmcts/rcj-standard-daily-cause-list": "workspace:*", "@hmcts/send-daily-hearing-list": "workspace:*", + "@hmcts/siac-poac-paac-weekly-hearing-list": "workspace:*", "notifications-node-client": "8.4.0" }, "peerDependencies": { diff --git a/libs/notifications/src/notification/notification-service.ts b/libs/notifications/src/notification/notification-service.ts index 195f2e248..3f742cd46 100644 --- a/libs/notifications/src/notification/notification-service.ts +++ b/libs/notifications/src/notification/notification-service.ts @@ -19,6 +19,15 @@ import { formatCaseSummaryForEmail as formatCourtOfAppealSummaryForEmail } from "@hmcts/court-of-appeal-civil-daily-cause-list"; import { extractCaseSummary as extractFamilySummary, formatCaseSummaryForEmail as formatFamilySummaryForEmail } from "@hmcts/family-daily-cause-list"; +import { + extractCaseSummary as extractFttLrtSummary, + formatCaseSummaryForEmail as formatFttLrtSummaryForEmail +} from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list"; +import { extractCaseSummary as extractFttRptSummary, formatCaseSummaryForEmail as formatFttRptSummaryForEmail } from "@hmcts/ftt-rpt-weekly-hearing-list"; +import { + extractCaseSummary as extractFttTaxSummary, + formatCaseSummaryForEmail as formatFttTaxSummaryForEmail +} from "@hmcts/ftt-tax-chamber-weekly-hearing-list"; import type { CaseSummary } from "@hmcts/list-types-common"; import { extractCaseSummary as extractLondonAdminSummary, @@ -27,6 +36,10 @@ import { import { prisma } from "@hmcts/postgres-prisma"; import { extractCaseSummary as extractRcjSummary, formatCaseSummaryForEmail as formatRcjSummaryForEmail } from "@hmcts/rcj-standard-daily-cause-list"; import { extractCaseSummary as extractSendSummary, formatCaseSummaryForEmail as formatSendSummaryForEmail } from "@hmcts/send-daily-hearing-list"; +import { + extractCaseSummary as extractSiacPoacPaacSummary, + formatCaseSummaryForEmail as formatSiacPoacPaacSummaryForEmail +} from "@hmcts/siac-poac-paac-weekly-hearing-list"; import { sendEmail } from "../govnotify/govnotify-client.js"; import { buildEnhancedTemplateParameters, @@ -108,7 +121,47 @@ const EMAIL_BUILDER_REGISTRY: Partial> = { BIRMINGHAM_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtConfig, LEEDS_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtConfig, BRISTOL_CARDIFF_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtConfig, - MANCHESTER_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtConfig + MANCHESTER_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtConfig, + SIAC_WEEKLY_HEARING_LIST: { + extract: extractSiacPoacPaacSummary as SummaryExtractor, + format: formatSiacPoacPaacSummaryForEmail + }, + POAC_WEEKLY_HEARING_LIST: { + extract: extractSiacPoacPaacSummary as SummaryExtractor, + format: formatSiacPoacPaacSummaryForEmail + }, + PAAC_WEEKLY_HEARING_LIST: { + extract: extractSiacPoacPaacSummary as SummaryExtractor, + format: formatSiacPoacPaacSummaryForEmail + }, + FTT_TAX_CHAMBER_WEEKLY_HEARING_LIST: { + extract: extractFttTaxSummary as SummaryExtractor, + format: formatFttTaxSummaryForEmail + }, + FTT_LANDS_REGISTRATION_TRIBUNAL_WEEKLY_HEARING_LIST: { + extract: extractFttLrtSummary as SummaryExtractor, + format: formatFttLrtSummaryForEmail + }, + FTT_RPT_EASTERN_WEEKLY_HEARING_LIST: { + extract: extractFttRptSummary as SummaryExtractor, + format: formatFttRptSummaryForEmail + }, + FTT_RPT_LONDON_WEEKLY_HEARING_LIST: { + extract: extractFttRptSummary as SummaryExtractor, + format: formatFttRptSummaryForEmail + }, + FTT_RPT_MIDLANDS_WEEKLY_HEARING_LIST: { + extract: extractFttRptSummary as SummaryExtractor, + format: formatFttRptSummaryForEmail + }, + FTT_RPT_NORTHERN_WEEKLY_HEARING_LIST: { + extract: extractFttRptSummary as SummaryExtractor, + format: formatFttRptSummaryForEmail + }, + FTT_RPT_SOUTHERN_WEEKLY_HEARING_LIST: { + extract: extractFttRptSummary as SummaryExtractor, + format: formatFttRptSummaryForEmail + } }; export interface NotificationResult { diff --git a/libs/publication/package.json b/libs/publication/package.json index 9521c9e6c..1cfa5188b 100644 --- a/libs/publication/package.json +++ b/libs/publication/package.json @@ -29,6 +29,9 @@ "@hmcts/civil-daily-cause-list": "workspace:*", "@hmcts/court-of-appeal-civil-daily-cause-list": "workspace:*", "@hmcts/family-daily-cause-list": "workspace:*", + "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list": "workspace:*", + "@hmcts/ftt-rpt-weekly-hearing-list": "workspace:*", + "@hmcts/ftt-tax-chamber-weekly-hearing-list": "workspace:*", "@hmcts/legacy-third-party-fulfilment": "workspace:*", "@hmcts/list-search-config": "workspace:*", "@hmcts/list-types-common": "workspace:*", @@ -38,6 +41,7 @@ "@hmcts/postgres-prisma": "workspace:*", "@hmcts/rcj-standard-daily-cause-list": "workspace:*", "@hmcts/send-daily-hearing-list": "workspace:*", + "@hmcts/siac-poac-paac-weekly-hearing-list": "workspace:*", "@hmcts/web-core": "workspace:*", "ajv": "8.20.0" }, diff --git a/libs/publication/src/processing/service.ts b/libs/publication/src/processing/service.ts index 26803a17f..9a1f97c42 100644 --- a/libs/publication/src/processing/service.ts +++ b/libs/publication/src/processing/service.ts @@ -6,6 +6,9 @@ import { type CauseListData, generateCauseListPdf } from "@hmcts/civil-and-famil import { type CauseListData as CivilCauseListData, generateCivilDailyCauseListPdf } from "@hmcts/civil-daily-cause-list"; import { type CourtOfAppealCivilData, generateCourtOfAppealCivilDailyCauseListPdf } from "@hmcts/court-of-appeal-civil-daily-cause-list"; import { type CauseListData as FamilyCauseListData, generateFamilyDailyCauseListPdf } from "@hmcts/family-daily-cause-list"; +import { type FttLrtHearingList, generateFttLrtWeeklyHearingListPdf } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list"; +import { type FttRptHearingList, generateFttRptWeeklyHearingListPdf } from "@hmcts/ftt-rpt-weekly-hearing-list"; +import { type FttTaxChamberHearingList, generateFttTaxChamberWeeklyHearingListPdf } from "@hmcts/ftt-tax-chamber-weekly-hearing-list"; import { sendThirdPartyPublications } from "@hmcts/legacy-third-party-fulfilment"; import { getLocationById } from "@hmcts/location"; import { generateLondonAdministrativeCourtDailyCauseListPdf, type LondonAdminCourtData } from "@hmcts/london-administrative-court-daily-cause-list"; @@ -13,6 +16,7 @@ import { sendListTypePublicationNotifications, sendLocationAndCaseSubscriptionNo import { prisma } from "@hmcts/postgres-prisma"; import { generateRcjStandardDailyCauseListPdf, type StandardHearingList } from "@hmcts/rcj-standard-daily-cause-list"; import { generateSendDailyHearingListPdf, type SendDailyHearingList } from "@hmcts/send-daily-hearing-list"; +import { generateSiacPoacPaacWeeklyHearingListPdf, type SiacPoacPaacHearingList } from "@hmcts/siac-poac-paac-weekly-hearing-list"; import { extractAndStoreArtefactSearch } from "../artefact-search-extractor.js"; const LOCALE_TO_LANGUAGE: Record = { @@ -95,7 +99,65 @@ const PDF_GENERATOR_REGISTRY: Partial> = { BIRMINGHAM_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtGenerator, LEEDS_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtGenerator, BRISTOL_CARDIFF_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtGenerator, - MANCHESTER_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtGenerator + MANCHESTER_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST: adminCourtGenerator, + SIAC_WEEKLY_HEARING_LIST: (p) => + generateSiacPoacPaacWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as SiacPoacPaacHearingList, + courtName: "Special Immigration Appeals Commission", + listTitle: "Special Immigration Appeals Commission Weekly Hearing List" + }), + POAC_WEEKLY_HEARING_LIST: (p) => + generateSiacPoacPaacWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as SiacPoacPaacHearingList, + courtName: "Proscribed Organisations Appeal Commission", + listTitle: "Proscribed Organisations Appeal Commission Weekly Hearing List" + }), + PAAC_WEEKLY_HEARING_LIST: (p) => + generateSiacPoacPaacWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as SiacPoacPaacHearingList, + courtName: "Pathogens Access Appeal Commission", + listTitle: "Pathogens Access Appeal Commission Weekly Hearing List" + }), + FTT_TAX_CHAMBER_WEEKLY_HEARING_LIST: (p) => generateFttTaxChamberWeeklyHearingListPdf({ ...p, jsonData: p.jsonData as FttTaxChamberHearingList }), + FTT_LANDS_REGISTRATION_TRIBUNAL_WEEKLY_HEARING_LIST: (p) => generateFttLrtWeeklyHearingListPdf({ ...p, jsonData: p.jsonData as FttLrtHearingList }), + FTT_RPT_EASTERN_WEEKLY_HEARING_LIST: (p) => + generateFttRptWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as FttRptHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal)", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Eastern region Weekly Hearing List" + }), + FTT_RPT_LONDON_WEEKLY_HEARING_LIST: (p) => + generateFttRptWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as FttRptHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal)", + listTitle: "First-tier Tribunal (Residential Property Tribunal): London region Weekly Hearing List" + }), + FTT_RPT_MIDLANDS_WEEKLY_HEARING_LIST: (p) => + generateFttRptWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as FttRptHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal)", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Midlands region Weekly Hearing List" + }), + FTT_RPT_NORTHERN_WEEKLY_HEARING_LIST: (p) => + generateFttRptWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as FttRptHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal)", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Northern region Weekly Hearing List" + }), + FTT_RPT_SOUTHERN_WEEKLY_HEARING_LIST: (p) => + generateFttRptWeeklyHearingListPdf({ + ...p, + jsonData: p.jsonData as FttRptHearingList, + courtName: "First-tier Tribunal (Residential Property Tribunal)", + listTitle: "First-tier Tribunal (Residential Property Tribunal): Southern region Weekly Hearing List" + }) }; export async function generatePublicationPdf(params: GeneratePdfParams): Promise { diff --git a/libs/publication/src/repository/model.ts b/libs/publication/src/repository/model.ts index 8b29e8dc7..ae810b31b 100644 --- a/libs/publication/src/repository/model.ts +++ b/libs/publication/src/repository/model.ts @@ -3,6 +3,7 @@ export interface Artefact { type: string; locationId: string; listTypeId: number; + listTypeName?: string; contentDate: Date; sensitivity: string; language: string; diff --git a/libs/publication/src/repository/queries.test.ts b/libs/publication/src/repository/queries.test.ts index eafb07d1e..1521822be 100644 --- a/libs/publication/src/repository/queries.test.ts +++ b/libs/publication/src/repository/queries.test.ts @@ -661,7 +661,7 @@ describe("getArtefactById", () => { noMatch: false } as any; - vi.mocked(prisma.artefact.findUnique).mockResolvedValue(mockArtefact); + vi.mocked(prisma.artefact.findUnique).mockResolvedValue({ ...mockArtefact, listType: { name: "CIVIL_DAILY_CAUSE_LIST" } } as any); const result = await getArtefactById("550e8400-e29b-41d4-a716-446655440000"); @@ -681,13 +681,15 @@ describe("getArtefactById", () => { isFlatFile: true, provenance: true, supersededCount: true, - noMatch: true + noMatch: true, + listType: { select: { name: true } } } }); expect(result).toEqual({ artefactId: "550e8400-e29b-41d4-a716-446655440000", locationId: "123", listTypeId: 1, + listTypeName: "CIVIL_DAILY_CAUSE_LIST", contentDate: mockArtefact.contentDate, sensitivity: "PUBLIC", language: "ENGLISH", @@ -722,7 +724,8 @@ describe("getArtefactById", () => { isFlatFile: true, provenance: true, supersededCount: true, - noMatch: true + noMatch: true, + listType: { select: { name: true } } } }); expect(result).toBeNull(); @@ -745,7 +748,7 @@ describe("getArtefactById", () => { noMatch: true } as any; - vi.mocked(prisma.artefact.findUnique).mockResolvedValue(mockArtefact); + vi.mocked(prisma.artefact.findUnique).mockResolvedValue({ ...mockArtefact, listType: { name: "FAMILY_DAILY_CAUSE_LIST" } } as any); const result = await getArtefactById("550e8400-e29b-41d4-a716-446655440001"); @@ -753,6 +756,7 @@ describe("getArtefactById", () => { artefactId: "550e8400-e29b-41d4-a716-446655440001", locationId: "456", listTypeId: 2, + listTypeName: "FAMILY_DAILY_CAUSE_LIST", contentDate: mockArtefact.contentDate, sensitivity: "PRIVATE", language: "WELSH", diff --git a/libs/publication/src/repository/queries.ts b/libs/publication/src/repository/queries.ts index e54da8187..e6c40d98d 100644 --- a/libs/publication/src/repository/queries.ts +++ b/libs/publication/src/repository/queries.ts @@ -89,7 +89,7 @@ export async function createArtefact(data: Artefact): Promise<{ artefactId: stri } export async function getArtefactById(artefactId: string): Promise { - return prisma.artefact.findUnique({ + const artefact = await prisma.artefact.findUnique({ where: { artefactId }, select: { artefactId: true, @@ -105,9 +105,13 @@ export async function getArtefactById(artefactId: string): Promise { diff --git a/templates/tech-spec-references/welsh-translations-catalogue.json b/templates/tech-spec-references/welsh-translations-catalogue.json index 77e08346b..cb9b85f92 100644 --- a/templates/tech-spec-references/welsh-translations-catalogue.json +++ b/templates/tech-spec-references/welsh-translations-catalogue.json @@ -630,7 +630,7 @@ "Proscribed Organisations Appeal Commission": "Comisiwn Apeliadau Sefydliadau Gwaharddedig", "Pathogens Access Appeal Commission": "Comisiwn Apeliadau Mynediad Pathogenau", "First-tier Tribunal (Tax Chamber)": "Tribiwnlys Haen Gyntaf (Siambr Treth)", - "First-tier Tribunal (Lands Registration Tribunal)": "Tribiwnlys Haen Gyntaf (Tribiwnlys Cofrestru Tir)", + "First-tier Tribunal (Land Registration Tribunal)": "Tribiwnlys Haen Gyntaf (Tribiwnlys Cofrestru Tir)", "First-tier Tribunal (Property Chamber) (Residential Property)": "Tribiwnlys Haen Gyntaf (Siambr Eiddo) (Eiddo Preswyl)", "SIAC Daily Weekly Hearing List": "Rhestr o Wrandawiadau Dyddiol Wythnosol SIAC", "POAC Daily Weekly Hearing List": "Rhestr o Wrandawiadau Dyddiol Wythnosol POAC", @@ -646,7 +646,7 @@ "Proscribed Organisations Appeal Commission Weekly Hearing List": "Rhestr o Wrandawiadau Wythnosol y Comisiwn Apeliadau Sefydliadau Gwaharddedig", "Pathogens Access Appeal Commission Weekly Hearing List": "Rhestr o Wrandawiadau Wythnosol y Comisiwn Apeliadau Mynediad Pathogenau", "First-tier Tribunal (Tax Chamber) Weekly Hearing list": "Rhestr o Wrandawiadau Wythnosol Tribiwnlys Haen Gyntaf (Siambr Treth)", - "First-tier Tribunal (Lands Registration Tribunal) Weekly Hearing list": "Rhestr o Wrandawiadau Wythnosol Tribiwnlys Haen Gyntaf (Tribiwnlys Cofrestru Tir)", + "First-tier Tribunal (Land Registration Tribunal) Weekly Hearing list": "Rhestr o Wrandawiadau Wythnosol Tribiwnlys Haen Gyntaf (Tribiwnlys Cofrestru Tir)", "Residential Property Tribunal: Eastern region Weekly Hearing List": "Tribiwnlys Eiddo Preswyl: Rhestr o Wrandawiadau Wythnosol rhanbarth Dwyrain Lloegr", "Residential Property Tribunal: London region Weekly Hearing List": "Tribiwnlys Eiddo Preswyl: Rhestr o Wrandawiadau Wythnosol rhanbarth Llundain", "Residential Property Tribunal: Midlands region Weekly Hearing List": "Tribiwnlys Eiddo Preswyl: Rhestr o Wrandawiadau Wythnosol rhanbarth Canolbarth Lloegr", diff --git a/tsconfig.json b/tsconfig.json index 151bfbad8..1c0f31ac7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,14 @@ "@hmcts/civil-daily-cause-list": ["libs/list-types/civil-daily-cause-list/src"], "@hmcts/family-daily-cause-list": ["libs/list-types/family-daily-cause-list/src"], "@hmcts/care-standards-tribunal-weekly-hearing-list": ["libs/list-types/care-standards-tribunal-weekly-hearing-list/src"], + "@hmcts/siac-poac-paac-weekly-hearing-list": ["libs/list-types/siac-poac-paac-weekly-hearing-list/src"], + "@hmcts/siac-poac-paac-weekly-hearing-list/config": ["libs/list-types/siac-poac-paac-weekly-hearing-list/src/config"], + "@hmcts/ftt-tax-chamber-weekly-hearing-list": ["libs/list-types/ftt-tax-chamber-weekly-hearing-list/src"], + "@hmcts/ftt-tax-chamber-weekly-hearing-list/config": ["libs/list-types/ftt-tax-chamber-weekly-hearing-list/src/config"], + "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list": ["libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src"], + "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list/config": ["libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list/src/config"], + "@hmcts/ftt-rpt-weekly-hearing-list": ["libs/list-types/ftt-rpt-weekly-hearing-list/src"], + "@hmcts/ftt-rpt-weekly-hearing-list/config": ["libs/list-types/ftt-rpt-weekly-hearing-list/src/config"], "@hmcts/send-daily-hearing-list": ["libs/list-types/send-daily-hearing-list/src"], "@hmcts/send-daily-hearing-list/config": ["libs/list-types/send-daily-hearing-list/src/config"], "@hmcts/cic-weekly-hearing-list": ["libs/list-types/cic-weekly-hearing-list/src"], diff --git a/yarn.lock b/yarn.lock index 9d200a2ea..978a3f03d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2452,6 +2452,57 @@ __metadata: languageName: unknown linkType: soft +"@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list@workspace:*, @hmcts/ftt-lands-registration-tribunal-weekly-hearing-list@workspace:libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list": + version: 0.0.0-use.local + resolution: "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list@workspace:libs/list-types/ftt-lands-registration-tribunal-weekly-hearing-list" + dependencies: + "@hmcts/list-types-common": "workspace:*" + "@hmcts/pdf-generation": "workspace:*" + "@types/luxon": "npm:3.7.1" + "@types/node": "npm:24.10.4" + luxon: "npm:3.7.2" + nunjucks: "npm:3.2.4" + typescript: "npm:6.0.3" + vitest: "npm:4.1.9" + peerDependencies: + express: ^5.1.0 + languageName: unknown + linkType: soft + +"@hmcts/ftt-rpt-weekly-hearing-list@workspace:*, @hmcts/ftt-rpt-weekly-hearing-list@workspace:libs/list-types/ftt-rpt-weekly-hearing-list": + version: 0.0.0-use.local + resolution: "@hmcts/ftt-rpt-weekly-hearing-list@workspace:libs/list-types/ftt-rpt-weekly-hearing-list" + dependencies: + "@hmcts/list-types-common": "workspace:*" + "@hmcts/pdf-generation": "workspace:*" + "@types/luxon": "npm:3.7.1" + "@types/node": "npm:24.10.4" + luxon: "npm:3.7.2" + nunjucks: "npm:3.2.4" + typescript: "npm:6.0.3" + vitest: "npm:4.1.9" + peerDependencies: + express: ^5.1.0 + languageName: unknown + linkType: soft + +"@hmcts/ftt-tax-chamber-weekly-hearing-list@workspace:*, @hmcts/ftt-tax-chamber-weekly-hearing-list@workspace:libs/list-types/ftt-tax-chamber-weekly-hearing-list": + version: 0.0.0-use.local + resolution: "@hmcts/ftt-tax-chamber-weekly-hearing-list@workspace:libs/list-types/ftt-tax-chamber-weekly-hearing-list" + dependencies: + "@hmcts/list-types-common": "workspace:*" + "@hmcts/pdf-generation": "workspace:*" + "@types/luxon": "npm:3.7.1" + "@types/node": "npm:24.10.4" + luxon: "npm:3.7.2" + nunjucks: "npm:3.2.4" + typescript: "npm:6.0.3" + vitest: "npm:4.1.9" + peerDependencies: + express: ^5.1.0 + languageName: unknown + linkType: soft + "@hmcts/legacy-third-party-fulfilment@workspace:*, @hmcts/legacy-third-party-fulfilment@workspace:libs/legacy-third-party-fulfilment": version: 0.0.0-use.local resolution: "@hmcts/legacy-third-party-fulfilment@workspace:libs/legacy-third-party-fulfilment" @@ -2544,11 +2595,15 @@ __metadata: "@hmcts/cic-weekly-hearing-list": "workspace:*" "@hmcts/civil-and-family-daily-cause-list": "workspace:*" "@hmcts/court-of-appeal-civil-daily-cause-list": "workspace:*" + "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list": "workspace:*" + "@hmcts/ftt-rpt-weekly-hearing-list": "workspace:*" + "@hmcts/ftt-tax-chamber-weekly-hearing-list": "workspace:*" "@hmcts/list-types-common": "workspace:*" "@hmcts/london-administrative-court-daily-cause-list": "workspace:*" "@hmcts/postgres-prisma": "workspace:*" "@hmcts/rcj-standard-daily-cause-list": "workspace:*" "@hmcts/send-daily-hearing-list": "workspace:*" + "@hmcts/siac-poac-paac-weekly-hearing-list": "workspace:*" notifications-node-client: "npm:8.4.0" typescript: "npm:6.0.3" peerDependencies: @@ -2638,6 +2693,9 @@ __metadata: "@hmcts/civil-daily-cause-list": "workspace:*" "@hmcts/court-of-appeal-civil-daily-cause-list": "workspace:*" "@hmcts/family-daily-cause-list": "workspace:*" + "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list": "workspace:*" + "@hmcts/ftt-rpt-weekly-hearing-list": "workspace:*" + "@hmcts/ftt-tax-chamber-weekly-hearing-list": "workspace:*" "@hmcts/legacy-third-party-fulfilment": "workspace:*" "@hmcts/list-search-config": "workspace:*" "@hmcts/list-types-common": "workspace:*" @@ -2647,6 +2705,7 @@ __metadata: "@hmcts/postgres-prisma": "workspace:*" "@hmcts/rcj-standard-daily-cause-list": "workspace:*" "@hmcts/send-daily-hearing-list": "workspace:*" + "@hmcts/siac-poac-paac-weekly-hearing-list": "workspace:*" "@hmcts/web-core": "workspace:*" ajv: "npm:8.20.0" typescript: "npm:6.0.3" @@ -2700,6 +2759,23 @@ __metadata: languageName: unknown linkType: soft +"@hmcts/siac-poac-paac-weekly-hearing-list@workspace:*, @hmcts/siac-poac-paac-weekly-hearing-list@workspace:libs/list-types/siac-poac-paac-weekly-hearing-list": + version: 0.0.0-use.local + resolution: "@hmcts/siac-poac-paac-weekly-hearing-list@workspace:libs/list-types/siac-poac-paac-weekly-hearing-list" + dependencies: + "@hmcts/list-types-common": "workspace:*" + "@hmcts/pdf-generation": "workspace:*" + "@types/luxon": "npm:3.7.1" + "@types/node": "npm:24.10.4" + luxon: "npm:3.7.2" + nunjucks: "npm:3.2.4" + typescript: "npm:6.0.3" + vitest: "npm:4.1.9" + peerDependencies: + express: ^5.1.0 + languageName: unknown + linkType: soft + "@hmcts/sjp-press-list@workspace:*, @hmcts/sjp-press-list@workspace:libs/list-types/sjp-press-list": version: 0.0.0-use.local resolution: "@hmcts/sjp-press-list@workspace:libs/list-types/sjp-press-list"