Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
78bace2
feat(#428): Add non-strategic hearing list types for SIAC, POAC, PAAC…
junaidiqbalmoj Jun 18, 2026
1a0ae8e
fix(#428): Address review feedback for non-strategic publishing
junaidiqbalmoj Jun 19, 2026
4ed8e88
refactor(#428): Consolidate duplicate FTT RPT NJK templates into a si…
junaidiqbalmoj Jun 19, 2026
cd845fd
refactor(#428): Move hardcoded FTT RPT court names into locale files
junaidiqbalmoj Jun 19, 2026
fac2b76
fix(#428): Fix build:views script for ftt-rpt lib
junaidiqbalmoj Jun 19, 2026
29a52bf
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 25, 2026
87316c8
code review refactoring
junaidiqbalmoj Jun 26, 2026
bcf5755
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 26, 2026
b253e48
Fix code review comments
junaidiqbalmoj Jun 26, 2026
f9ae375
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 26, 2026
c92f656
Merge branch 'master' into feature/428-tribunal-non-strategic-publishing
junaidiqbalmoj Jun 29, 2026
adc4874
Merge remote-tracking branch into feature/428-tribunal-non-strategic-…
junaidiqbalmoj Jun 29, 2026
b035fe1
refactor(#428): Consolidate duplicate SIAC/POAC/PAAC NJK templates in…
junaidiqbalmoj Jun 29, 2026
f84461f
remove script not required
junaidiqbalmoj Jun 29, 2026
3ffed41
fix(#428): Update SIAC/POAC/PAAC urlPath to point to consolidated page
junaidiqbalmoj Jun 29, 2026
293d290
fix(#428): Mock @hmcts/azure-blob in pdf-generator tests to prevent u…
junaidiqbalmoj Jun 29, 2026
329a30e
fix(#428): Mock @hmcts/azure-blob in ftt-tax-chamber pdf-generator te…
junaidiqbalmoj Jun 29, 2026
6d5131b
fix(#428): Mock @hmcts/azure-blob in ftt-lrt pdf-generator tests to p…
junaidiqbalmoj Jun 29, 2026
2202932
refactor(#428): Extract shared PDF generation logic into generateFttS…
junaidiqbalmoj Jun 29, 2026
28ed713
fix bugs
junaidiqbalmoj Jul 2, 2026
05aa2ee
merge(#428): Resolve conflicts between feature/428 and master branches
junaidiqbalmoj Jul 3, 2026
416db8f
fix(#428): Update SIAC/FTT renderer tests to match zero-padded date f…
junaidiqbalmoj Jul 3, 2026
fc272c4
chore: sort package.json dependencies alphabetically
junaidiqbalmoj Jul 3, 2026
f03d8d6
fix: restore missing migration SQL files and remove orphan migration …
junaidiqbalmoj Jul 3, 2026
5bee15d
Revert "fix: restore missing migration SQL files and remove orphan mi…
junaidiqbalmoj Jul 3, 2026
f1b27ab
fix bug (add new line for RR text)
junaidiqbalmoj Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- No-op: changes in this migration are applied by other in-progress tickets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this change is needed. If not required we should remove it

8 changes: 8 additions & 0 deletions apps/web/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -92,6 +96,10 @@ export async function createApp(): Promise<Express> {
authModuleRoot,
listTypesCommonModuleRoot,
careStandardsTribunalModuleRoot,
siacPoacPaacModuleRoot,
fttTaxChamberModuleRoot,
fttLrtModuleRoot,
fttRptModuleRoot,
civilFamilyCauseListModuleRoot,
civilDailyCauseListModuleRoot,
familyDailyCauseListModuleRoot,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{% extends "layouts/base-template.njk" %}


{% block page_content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">

<h1 class="govuk-heading-l" id="top">{{ header.listTitle }}</h1>

<p class="govuk-body">
<a href="{{ t.factLinkUrl }}" class="govuk-link">{{ t.factLinkText }}</a> {{ t.factAdditionalText }}
</p>

<p class="govuk-body govuk-!-font-weight-bold govuk-!-margin-bottom-1">{{ t.listForWeekCommencing }} {{ header.weekCommencingDate }}</p>
<p class="govuk-body">{{ t.lastUpdated }} {{ header.lastUpdatedDate }} {{ t.at }} {{ header.lastUpdatedTime }}</p>

<details class="govuk-details govuk-!-margin-top-6" data-module="govuk-details" open>
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
{{ t.importantInformationTitle }}
</span>
</summary>
<div class="govuk-details__text">
<p class="govuk-body">{{ t.importantInformationText }}</p>
<p class="govuk-body">
<a href="{{ t.importantInformationLinkUrl }}" class="govuk-link" target="_blank" rel="noopener noreferrer">
{{ t.importantInformationLinkText }}
</a>
</p>
</div>
</details>

<div class="govuk-form-group govuk-!-margin-top-6">
<h2 class="govuk-heading-s">{{ t.searchCasesTitle }}</h2>
<label class="govuk-label govuk-visually-hidden" for="case-search-input">
{{ t.searchCasesLabel }}
</label>
<input class="govuk-input govuk-!-width-one-half" id="case-search-input" name="search" type="text" aria-label="{{ t.searchCasesLabel }}">
</div>

<div id="hearings-table-container">
<table class="govuk-table" id="hearings-table" role="table" aria-label="{{ header.listTitle }}">
<thead class="govuk-table__head">
<tr class="govuk-table__row">
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.date }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.hearingTime }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.caseName }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.caseReferenceNumber }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.judge }}</th>
<th scope="col" class="govuk-table__header">{{ t.tableHeaders.venuePlatform }}</th>
</tr>
</thead>
<tbody class="govuk-table__body">
{% for hearing in hearings %}
<tr class="govuk-table__row">
<td class="govuk-table__cell">{{ hearing.date }}</td>
<td class="govuk-table__cell">{{ hearing.hearingTime }}</td>
<td class="govuk-table__cell">{{ hearing.caseName }}</td>
<td class="govuk-table__cell">{{ hearing.caseReferenceNumber }}</td>
<td class="govuk-table__cell">{{ hearing.judge }}</td>
<td class="govuk-table__cell">{{ hearing.venuePlatform }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

<p class="govuk-body-s govuk-!-margin-top-6">{{ t.dataSource }}: {{ dataSource }}</p>

<div class="govuk-!-margin-top-6">
<a href="#top" class="govuk-link">{{ t.backToTop }}</a>
</div>

</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { readFile } from "node:fs/promises";
import type { Request, Response } from "express";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mockValidate = vi.hoisted(() => vi.fn());

vi.mock("node:fs/promises", () => ({
readFile: vi.fn()
}));

vi.mock("@hmcts/list-types-common", async (importOriginal) => {
const actual = await importOriginal<typeof import("@hmcts/list-types-common")>();
return {
...actual,
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(),
PROVENANCE_LABELS: { MANUAL_UPLOAD: "Manual Upload", LIST_ASSIST: "List Assist" }
}));

vi.mock("@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list", async (importOriginal) => {
const actual = await importOriginal<typeof import("@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list")>();
return { ...actual, renderFttLrtData: vi.fn() };
});

import { renderFttLrtData } from "@hmcts/ftt-lands-registration-tribunal-weekly-hearing-list";
import { getArtefactById } from "@hmcts/publication";
import { GET } from "./index.js";

describe("FTT Lands Registration Tribunal Weekly Hearing List page controller", () => {
let req: Partial<Request>;
let res: Partial<Response>;

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 mockArtefact = {
artefactId: "test-artefact-123",
contentDate: new Date("2026-01-01"),
lastReceivedDate: new Date("2026-01-01T12:00:00Z"),
provenance: "MANUAL_UPLOAD"
};
const mockJsonData = [
{
date: "01/01/2026",
hearingTime: "10:00am",
caseName: "A Vs B",
caseReferenceNumber: "LRT/00001/2026",
judge: "Judge Smith",
venuePlatform: "London"
}
];
const mockRenderedData = {
header: {
listTitle: "First-tier Tribunal (Lands 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(mockArtefact as any);
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockJsonData));
mockValidate.mockReturnValue({ isValid: true, errors: [] });
vi.mocked(renderFttLrtData).mockReturnValue(mockRenderedData);

// Act
await GET(req as Request, res as Response);

// Assert
expect(renderFttLrtData).toHaveBeenCalledWith(mockJsonData, expect.objectContaining({ courtName: "First-tier Tribunal (Lands Registration Tribunal)" }));
const renderCall = vi.mocked(res.render).mock.calls[0];
expect(renderCall[0]).toBe("ftt-lands-registration-tribunal-weekly-hearing-list");
expect(renderCall[1]).toMatchObject({ 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);
});

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 JSON file is not found", async () => {
req.query = { artefactId: "test-artefact-123" };
vi.mocked(getArtefactById).mockResolvedValue({
artefactId: "test-artefact-123",
contentDate: new Date("2026-01-01"),
lastReceivedDate: new Date("2026-01-01T12:00:00Z"),
provenance: "MANUAL_UPLOAD"
} as any);
vi.mocked(readFile).mockRejectedValue(new Error("File not found"));
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);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
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 { getArtefactById } from "@hmcts/publication";
import type { Request, Response } from "express";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const MONOREPO_ROOT = path.join(__dirname, "..", "..", "..", "..", "..", "..");
const TEMP_UPLOAD_DIR = path.join(MONOREPO_ROOT, "storage", "temp", "uploads");
const validate = createJsonValidator(schemaPath);

export const GET = async (req: Request, res: Response) => {
const locale = res.locals.locale || "en";
const t = locale === "cy" ? cy : en;

const artefactId = req.query.artefactId as string;

if (!artefactId) {
return res.status(400).render("errors/common", {
en,
cy,
errorTitle: "Bad Request",
errorMessage: "Missing artefactId parameter"
});
}

try {
const artefact = await getArtefactById(artefactId);

if (!artefact) {
return res.status(404).render("errors/common", {
en,
cy,
errorTitle: "Not Found",
errorMessage: "The requested list could not be found"
});
}

const jsonFilePath = path.join(TEMP_UPLOAD_DIR, `${artefactId}.json`);

let jsonContent: string;
try {
jsonContent = await readFile(jsonFilePath, "utf-8");
} catch (error) {
console.error(`Error reading JSON file at ${jsonFilePath}:`, error);
return res.status(404).render("errors/common", {
en,
cy,
errorTitle: "Not Found",
errorMessage: "The requested list could not be found"
});
}

const jsonData: FttLrtHearingList = JSON.parse(jsonContent);

const validationResult = validate(jsonData);
if (!validationResult.isValid) {
console.error("Validation errors:", validationResult.errors);
return res.status(400).render("errors/common", {
en,
cy,
errorTitle: "Invalid Data",
errorMessage: "The list data is invalid"
});
}

const courtName = "First-tier Tribunal (Lands Registration Tribunal)";

const { header, hearings } = renderFttLrtData(jsonData, {
locale,
courtName,
contentDate: artefact.contentDate,
lastReceivedDate: artefact.lastReceivedDate.toISOString(),
listTitle: t.pageTitle
});

const dataSource = t.provenanceLabels[artefact.provenance as keyof typeof t.provenanceLabels] || artefact.provenance;

res.render("ftt-lands-registration-tribunal-weekly-hearing-list", {
en,
cy,
t,
title: header.listTitle,
header,
hearings,
dataSource
});
} catch (error) {
console.error("Error rendering FTT Lands Registration Tribunal Weekly Hearing List:", error);
return res.status(500).render("errors/common", {
en,
cy,
errorTitle: "Server Error",
errorMessage: "An error occurred while loading the list"
});
}
};
Loading
Loading