Skip to content

VIBE-339 Add PDF and email summary for RCJ and Care Standards List - #325

Merged
ChrisS1512 merged 46 commits into
masterfrom
feature/VIBE-339
Feb 24, 2026
Merged

ChrisS1512 merged 46 commits into
masterfrom
feature/VIBE-339

Conversation

@KianKwa

@KianKwa KianKwa commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

Jira link

https://tools.hmcts.net/jira/browse/VIBE-339

Change description

Add PDF and email summary for RCJ and Care Standards List

Checklist

  • commit messages are meaningful and follow good commit message guidelines
  • README and other documentation has been updated / added (if needed)
  • tests have been updated / new tests has been added (if needed)
  • Does this PR introduce a breaking change

Summary by CodeRabbit

  • New Features

    • Subscription emails can include generated PDFs or a download link, with case summaries included in the message.
    • Subscriptions now cover additional court and tribunal list types (RCJ, Court of Appeal civil, Administrative Court variants, Care Standards Tribunal).
    • New environment settings enable configurable notification templates and service URL for links.
  • Localization

    • Expanded English and Welsh content with cautionary guidance on sensitive data and reporting.
  • Tests

    • New end-to-end and unit tests for notifications, PDF generation, template selection and size handling.

@coderabbitai

coderabbitai Bot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds PDF generation (Puppeteer), per-list PDF and email-summary builders, a publication processing service that generates PDFs and triggers subscription notifications, GOV.UK Notify integration supporting PDF uploads and template selection (PDF+summary vs summary-only), and wiring from uploads/blob-ingestion into the new flow.

Changes

Cohort / File(s) Summary
Environment
apps/web/.env.example
Added GOVUK_NOTIFY_TEMPLATE_ID_SUBSCRIPTION, GOVUK_NOTIFY_TEMPLATE_ID_SUBSCRIPTION_PDF_AND_SUMMARY, GOVUK_NOTIFY_TEMPLATE_ID_SUBSCRIPTION_SUMMARY_ONLY and CATH_SERVICE_URL.
PDF generation core
libs/pdf-generation/src/*, libs/pdf-generation/package.json, libs/pdf-generation/tsconfig.json
New package adding Puppeteer-based HTML→PDF generator, tests and exported API (generatePdfFromHtml).
Common PDF & email utilities
libs/list-types/common/src/pdf/*, libs/list-types/common/src/email-summary/*, libs/list-types/common/src/index.ts, libs/list-types/common/src/list-type-ids.ts
Added PDF utilities (styles, storage, size limits), case-summary formatter and list-type id helpers; exported for list-type consumers.
Per-list PDF & email modules
libs/list-types/*/src/pdf/*, libs/list-types/*/src/email-summary/*, libs/list-types/*/src/pages/*, libs/list-types/*/package.json
Added per-list PDF generators, Nunjucks pdf templates, email-summary extractors, locale caution strings and updated build scripts to include pdf templates for multiple list-type packages.
Publication orchestration
libs/publication/src/processing/service.ts, libs/publication/src/processing/service.test.ts, libs/publication/src/index.ts
New publication service: selects generators, produces PDFs, and exposes generatePublicationPdf, sendPublicationNotificationsForArtefact and processPublication.
Notification system
libs/notifications/src/govnotify/*, libs/notifications/src/notification/*, libs/notifications/package.json
GovNotify client accepts optional pdfBuffer and templateId; template-config supports distinct subscription template env vars and selection; notification-service refactored to build enhanced template parameters, attach PDFs when under size limit, aggregate/skip results.
Upload & ingestion wiring
libs/admin-pages/src/pages/*, libs/api/src/blob-ingestion/*, e2e-tests/tests/subscription-notifications.spec.ts
Admin upload and blob-ingestion now call processPublication; publication payloads may include listTypeId, jsonData, pdfFilePath. New E2E tests for subscription notification flows.
List-type index exports
libs/list-types/*/src/index.ts
Indexes updated to re-export new pdf and email-summary modules and expose ValidationResult where applicable.
Tests & docs
libs/*/*.test.ts, docs/tickets/VIBE-339/*, docs/tickets/VIBE-341/*
Extensive unit/integration/e2e tests added/updated for PDF generation, email summary formatting and notification flows; comprehensive design/plan/task docs for VIBE-339/VIBE-341.
Tooling & config
package.json, tsconfig.json
Added puppeteer dependency, new workspace package @hmcts/pdf-generation and TS path alias; workspace wiring for list-type packages updated.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Upload as Admin UI / Blob Ingest
  participant Publication as Publication Service
  participant PDF as PDF Generator
  participant Storage as Temp Storage / Document Service
  participant Notify as GOV.UK Notify
  Upload->>Publication: processPublication(artefactId, listTypeId, jsonData, provenance, ...)
  activate Publication
  Publication->>PDF: generate...Pdf(artefactId, jsonData, locale, contentDate, provenance, ...)
  activate PDF
  PDF->>Storage: savePdfToStorage(artefactId, pdfBuffer)
  Storage-->>PDF: pdfPath, sizeBytes, exceedsMaxSize
  deactivate PDF
  Publication->>Notify: sendPublicationNotificationsForArtefact(locationId, pdfPath?, caseSummary?, ...)
  activate Notify
  Notify->>Notify: selectTemplate(hasPdf, sizeUnder2MB)
  alt attach PDF
    Notify->>Storage: prepareUpload(pdfBuffer)
    Storage-->>Notify: link_to_file
    Notify->>Notify: sendEmail(templateId, personalisation including link_to_file)
  else no PDF attachment
    Notify->>Notify: sendEmail(templateId, personalisation)
  end
  Notify-->>Publication: per-subscriber results (sent/failed/skipped)
  deactivate Notify
  Publication-->>Upload: aggregated results
  deactivate Publication
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: adding PDF and email summary functionality for RCJ and Care Standards list types, which aligns with the substantial implementation across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/VIBE-339

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright E2E Test Results

238 tests   238 ✅  21m 20s ⏱️
 33 suites    0 💤
  1 files      0 ❌

Results for commit d823a4b.

♻️ This comment has been updated with latest results.

@KianKwa
KianKwa changed the base branch from master to feature/VIBE-341 January 30, 2026 16:04
@KianKwa
KianKwa changed the base branch from master to feature/VIBE-341 February 13, 2026 15:16
@KianKwa
KianKwa changed the base branch from feature/VIBE-341 to master February 13, 2026 16:31

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/notifications/src/notification/notification-service.ts (1)

127-154: ⚠️ Potential issue | 🟠 Major

Audit log left in "Pending" if an exception is thrown after creation.

If buildEmailTemplateData (line 135) or sendEmail (line 137) throws, the catch block on line 151 returns { status: "failed" } but never calls updateNotificationStatus for the notification created at line 127. That record will remain "Pending" indefinitely.

Suggested fix
   } catch (error) {
     const errorMessage = error instanceof Error ? error.message : String(error);
+    if (notification) {
+      await updateNotificationStatus(notification.notificationId, "Failed", undefined, errorMessage).catch(() => {});
+    }
     return { status: "failed", error: `User ${subscription.userId}: ${errorMessage}` };
   }

This requires hoisting notification above the try or restructuring slightly so it's in scope. An alternative is to wrap the post-audit-log logic in its own try/catch.

🟡 Minor comments (21)
apps/web/.env.example-44-45 (1)

44-45: ⚠️ Potential issue | 🟡 Minor

Default CATH_SERVICE_URL points to production.

The .env.example uses the live production URL as the default. Other entries use placeholder values (e.g. template-uuid-here, your-api-key-here). Consider using a local or placeholder value to avoid accidental production-pointing in non-production environments.

Suggested change
 # Service URL for generating links in notifications
-CATH_SERVICE_URL=https://www.court-tribunal-hearings.service.gov.uk
+CATH_SERVICE_URL=https://localhost:8080
libs/list-types/rcj-standard-daily-cause-list/src/pages/en.ts-131-134 (1)

131-134: ⚠️ Potential issue | 🟡 Minor

Fix typo: "formally" should be "formerly".

"Note this document contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data…" uses the wrong word. "Formerly" means "in the past"; "formally" means "in a formal manner". This typo appears across 7 locale files. Consider fixing the shared constant in libs/list-types/common/src/email-summary/case-summary-formatter.ts to resolve it in multiple list types at once.

libs/list-types/civil-and-family-daily-cause-list/src/pages/cy.ts-40-43 (1)

40-43: ⚠️ Potential issue | 🟡 Minor

Inconsistent Welsh translations for caution notice fields across list types.

The cautionNote and cautionReporting fields use two distinct Welsh variants: this file uses "Noder … Neddf Gwarchod Data 2018 … ei drin yn y ffordd briodol" and "Mae'r ddogfen hon yn cynnwys gwybodaeth a fwriedir …", whilst administrative-court, rcj-standard, court-of-appeal-civil, and london-administrative-court use "Sylwer … Ddeddf Diogelu Data 2018 … ei thrin yn briodol" and "Mae'r ddogfen hon yn cynnwys gwybodaeth sydd â'r bwriad …" respectively. Both translations are valid Welsh, but these notices should use consistent wording across all list types.

libs/list-types/civil-and-family-daily-cause-list/src/pdf/pdf-template.njk-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Hardcoded lang="en" — should reflect the locale.

The PDF supports Welsh (cy) translations but the HTML lang attribute is always en. This affects accessibility metadata in the generated PDF.

Proposed fix
-<html lang="en">
+<html lang="{{ locale | default('en') }}">
docs/tickets/VIBE-341/plan.md-415-423 (1)

415-423: ⚠️ Potential issue | 🟡 Minor

Typo: "formally known as" should be "formerly known as".

Line 417 in the SPECIAL_CATEGORY_DATA_WARNING text uses "formally" (meaning "in a formal manner") instead of "formerly" (meaning "in the past"). This is a legal notice that may be copy-pasted into production code.

e2e-tests/tests/subscription-notifications.spec.ts-296-302 (1)

296-302: ⚠️ Potential issue | 🟡 Minor

Hardcoded sleep before asserting zero notifications is fragile.

Line 298 uses a raw 2-second setTimeout, then waitForNotifications with only 3 retries × 500ms. If the system is under load, a notification could arrive after this window, causing a false pass—or the test could be needlessly slow in normal conditions. Consider increasing the wait or documenting the timing rationale.

docs/tickets/VIBE-339/plan.md-237-239 (1)

237-239: ⚠️ Potential issue | 🟡 Minor

Typo: "formally known as" → "formerly known as".

This constant appears in the email summary builder template. Same issue flagged in the VIBE-341 docs—ensure the production SPECIAL_CATEGORY_DATA_WARNING constant uses the correct word.

docs/tickets/VIBE-341/ticket.md-52-52 (1)

52-52: ⚠️ Potential issue | 🟡 Minor

Typo: "formally known as" → "formerly known as".

Same issue as in plan.md. This text is a legal notice that will likely be used verbatim in email templates.

libs/list-types/civil-and-family-daily-cause-list/package.json-18-18 (1)

18-18: ⚠️ Potential issue | 🟡 Minor

build:pdf-templates will fail if src/pdf/*.njk matches nothing.

The cp src/pdf/*.njk dist/pdf/ command will error if the glob matches no files (shell default behaviour). The build:nunjucks script uses find which handles empty results gracefully. Consider aligning the approach or adding a glob guard.

Proposed fix
-    "build:pdf-templates": "mkdir -p dist/pdf && cp src/pdf/*.njk dist/pdf/",
+    "build:pdf-templates": "mkdir -p dist/pdf && find src/pdf -name '*.njk' -exec cp {} dist/pdf/ \\;",
e2e-tests/tests/subscription-notifications.spec.ts-235-236 (1)

235-236: ⚠️ Potential issue | 🟡 Minor

Consider using unique email addresses for each test user to avoid potential GOV.UK Notify deduplication.

Whilst testUser1 and testUser2 have distinct user IDs and separate subscriptions, they share the same email address. If GOV.UK Notify deduplicates notifications by email, this test may not properly validate multi-subscriber behaviour. Generate unique emails per user (e.g., by appending a timestamp or random identifier to the email address) to ensure each subscriber receives independent notifications.

libs/list-types/rcj-standard-daily-cause-list/src/pdf/pdf-template.njk-136-138 (1)

136-138: ⚠️ Potential issue | 🟡 Minor

Hardcoded English string "No hearings scheduled." — should use a translation key.

This string won't be translated for Welsh locale.

Proposed fix
-    <p>No hearings scheduled.</p>
+    <p>{{ t.common.noHearingsScheduled }}</p>
libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/en.ts-40-41 (1)

40-41: ⚠️ Potential issue | 🟡 Minor

Fix typo: "formally" should be "formerly" across multiple list-type files.

The phrase "formally known as" is incorrect; the intended phrase is "formerly known as" (previously). This user-facing legal text appears in at least six list-type locale files and one shared common file, suggesting the text was copied with the same typo. All instances require correction:

  • libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/en.ts:41
  • libs/list-types/civil-and-family-daily-cause-list/src/pages/en.ts:41
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/en.ts:26
  • libs/list-types/london-administrative-court-daily-cause-list/src/pages/en.ts:35
  • libs/list-types/administrative-court-daily-cause-list/src/pages/en.ts:56
  • libs/list-types/rcj-standard-daily-cause-list/src/pages/en.ts:132
  • libs/list-types/common/src/email-summary/case-summary-formatter.ts:1
libs/admin-pages/src/pages/manual-upload-summary/index.ts-111-119 (1)

111-119: ⚠️ Potential issue | 🟡 Minor

Silent swallow of JSON parse errors may mask corrupted uploads.

When a file has a .json extension (Line 90: isFlatFile = false) but fails to parse, the error is silently ignored and jsonData stays undefined. The upload proceeds as if it were a flat file, but the database record still has isFlatFile: false.

This mismatch between the stored metadata and actual processing could lead to confusion during debugging. Consider logging a warning so administrators have visibility.

Suggested improvement
       try {
         jsonData = JSON.parse(uploadData.file.toString("utf8"));
       } catch {
-        // Not valid JSON, treat as flat file
+        console.warn("[Manual Upload] File has .json extension but failed to parse as JSON, treating as flat file:", {
+          artefactId,
+          fileName: uploadData.fileName
+        });
       }
libs/notifications/src/notification/notification-service.test.ts-360-410 (1)

360-410: ⚠️ Potential issue | 🟡 Minor

Test name "summary-only" contradicts assertion expecting no summary.

Line 403 asserts getSubscriptionTemplateIdForListType is called with (8, false, false), indicating no summary and no PDF. However, the test is named "should use summary-only template for Civil and Family list without PDF", which suggests a summary should be present.

Comparing with related tests: when a PDF is provided (with or without size restrictions), the assertion passes true for hasSummary. Only when no PDF is provided does the current test expect false. The test name should either be corrected to reflect "standard template without PDF" or the assertion should be updated to (8, true, false) to match the stated intent.

libs/notifications/src/govnotify/template-config.test.ts-111-136 (1)

111-136: ⚠️ Potential issue | 🟡 Minor

Potential timezone-dependent test flakiness.

new Date("2025-01-15") is parsed as UTC midnight. The formatPublicationDate implementation uses date.getDate() (local time), so in timezones behind UTC (e.g. US-based CI runners), getDate() may return 14 instead of 15, causing failures.

Use explicit UTC dates with times to avoid ambiguity, or construct dates that are timezone-safe:

Suggested fix
-      const date = new Date("2025-01-15");
-      expect(formatPublicationDate(date)).toBe("15 January 2025");
+      const date = new Date(2025, 0, 15); // local date, no UTC ambiguity
+      expect(formatPublicationDate(date)).toBe("15 January 2025");

Apply the same approach to other date constructions in this block (lines 120, 127–129).

libs/list-types/administrative-court-daily-cause-list/src/pdf/pdf-generator.ts-39-42 (1)

39-42: ⚠️ Potential issue | 🟡 Minor

Misleading default for unrecognised listTypeId.

Line 42 falls back to "BIRMINGHAM_ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST" for any unknown ID. This silently produces a PDF labelled as Birmingham, which could be confusing. A generic fallback like "ADMINISTRATIVE_COURT_DAILY_CAUSE_LIST" or an early error would be more appropriate.

The same concern applies to the title fallback on line 41, though that one is more generic.

libs/admin-pages/src/pages/non-strategic-upload-summary/index.ts-131-138 (1)

131-138: ⚠️ Potential issue | 🟡 Minor

Silent JSON parse failure may hide upstream issues.

If a .json file is uploaded but contains malformed JSON, jsonData silently remains undefined and processPublication proceeds without it. This could lead to a successful upload response with no PDF or email summary generated, confusing the user.

Consider logging a warning so operators can diagnose why a JSON upload produced no summary.

Proposed fix
     } else {
       // Parse JSON data for JSON files
       try {
         jsonData = JSON.parse(uploadData.file.toString("utf8"));
-      } catch {
-        // Not valid JSON
+      } catch (parseError) {
+        console.warn("[Non-Strategic Upload] Failed to parse uploaded file as JSON:", parseError);
       }
     }
libs/list-types/administrative-court-daily-cause-list/src/pdf/pdf-generator.ts-82-82 (1)

82-82: ⚠️ Potential issue | 🟡 Minor

Non-null assertion on sizeBytes is risky.

pdfResult.sizeBytes! assumes the value is always defined when success is true and pdfBuffer exists. If the contract isn't enforced upstream, this silently passes undefined to savePdfToStorage.

Consider defaulting: pdfResult.sizeBytes ?? pdfResult.pdfBuffer.length.

Proposed fix
-    return await savePdfToStorage(options.artefactId, pdfResult.pdfBuffer, pdfResult.sizeBytes!);
+    return await savePdfToStorage(options.artefactId, pdfResult.pdfBuffer, pdfResult.sizeBytes ?? pdfResult.pdfBuffer.length);
libs/list-types/court-of-appeal-civil-daily-cause-list/src/pdf/pdf-generator.ts-51-60 (1)

51-60: ⚠️ Potential issue | 🟡 Minor

Non-null assertion on pdfResult.sizeBytes! is unguarded.

Line 53 checks pdfResult.success and pdfResult.pdfBuffer, but sizeBytes could still be undefined. If savePdfToStorage expects a number, this silently passes undefined.

Proposed fix
-    return await savePdfToStorage(options.artefactId, pdfResult.pdfBuffer, pdfResult.sizeBytes!);
+    return await savePdfToStorage(options.artefactId, pdfResult.pdfBuffer, pdfResult.sizeBytes ?? 0);
libs/publication/src/processing/service.ts-49-55 (1)

49-55: ⚠️ Potential issue | 🟡 Minor

Non-null assertions on displayFrom! and displayTo! are unsafe.

These fields are optional in GeneratePdfParams (lines 20-21). If processPublication is called for a Care Standards list without displayFrom/displayTo, this silently passes undefined despite the assertion.

Proposed fix — guard before invoking
   CARE_STANDARDS_TRIBUNAL_WEEKLY_HEARING_LIST: (p) =>
+    p.displayFrom && p.displayTo
+    ?
     generateCareStandardsTribunalWeeklyHearingListPdf({
       ...p,
       jsonData: p.jsonData as CareStandardsTribunalHearingList,
-      displayFrom: p.displayFrom!,
-      displayTo: p.displayTo!
-    }),
+      displayFrom: p.displayFrom,
+      displayTo: p.displayTo
+    })
+    : Promise.resolve({ success: false, error: "displayFrom and displayTo are required for Care Standards" }),
libs/list-types/rcj-standard-daily-cause-list/src/pdf/pdf-generator.ts-69-69 (1)

69-69: ⚠️ Potential issue | 🟡 Minor

Unsafe cast on translations lookup.

listTypeKey may not exist as a key in translations, making courtTranslations silently undefined. This would surface as a template rendering error at runtime.

Proposed fix
-    const courtTranslations = translations[listTypeKey as keyof typeof translations] as Record<string, string>;
+    const courtTranslations = translations[listTypeKey as keyof typeof translations] as Record<string, string> | undefined;
+    if (!courtTranslations) {
+      return createPdfErrorResult(new Error(`No translations found for list type key: ${listTypeKey}`));
+    }
🧹 Nitpick comments (30)
libs/list-types/rcj-standard-daily-cause-list/src/pages/en.ts (1)

131-134: Consider extracting shared caution strings to @hmcts/list-types-common.

The cautionNote and cautionReporting texts are duplicated verbatim across multiple list-type locale files (RCJ, London Admin, etc.). The AI summary mentions a shared SPECIAL_CATEGORY_DATA_WARNING constant already exists in the common module — these strings could live there too, reducing copy-paste drift.

libs/list-types/administrative-court-daily-cause-list/package.json (1)

17-17: build:nunjucks script is becoming unwieldy.

This one-liner now performs two separate find-and-copy passes (pages + pdf). As more template directories are added, this will keep growing. Consider extracting it into a small shell script (e.g., scripts/copy-templates.sh) that all list-type packages can share.

libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/en.ts (1)

25-28: Consider centralising shared caution strings.

The cautionNote and cautionReporting text is identical across six en.ts locale files (care-standards, administrative-court, civil-and-family, london-administrative-court, court-of-appeal, and rcj-standard). Extract these to @hmcts/list-types-common and re-export from each module to maintain a single source of truth for future updates.

docs/tickets/VIBE-339/ticket.md (1)

50-54: Add language identifiers to fenced code blocks.

The static analysis tool flags these code blocks as missing language specifiers. Adding text or plaintext would satisfy the linter.

-```
+```text
 Note this email contains Special Category Data...
libs/list-types/london-administrative-court-daily-cause-list/package.json (1)

17-17: Long build:nunjucks script duplicated across packages.

This shell one-liner is repeated identically in multiple package.json files. Consider extracting it into a shared script (e.g., a root-level scripts/build-nunjucks.sh) to reduce maintenance burden.

e2e-tests/tests/subscription-notifications.spec.ts (1)

153-303: Consider consolidating into a single journey test.

The coding guidelines state E2E tests should minimise test count with one test per complete user journey. These three tests (single subscriber, multiple subscribers, no-subscription location) could be combined into one sequential journey, with assertions inline.

This would also improve efficiency since the afterEach teardown and re-setup overhead is repeated for each test.

As per coding guidelines: "E2E tests in Playwright should minimize test count with one test per complete user journey, including validations, Welsh translations, and accessibility checks inline rather than in separate tests."

libs/list-types/civil-and-family-daily-cause-list/src/rendering/renderer.ts (1)

85-104: Pervasive as any mutation pattern degrades type safety.

Functions like calculateDuration, formatHearingChannel, processParties, and formatReportingRestrictions all mutate their input objects via (x as any).field = value. This defeats TypeScript's type system and makes the data flow hard to reason about. Consider extending the types or returning new objects instead.

This is pre-existing rather than introduced by this PR, so flagging as optional for a future refactor.

libs/list-types/common/src/pdf/pdf-utilities.ts (1)

9-12: Fragile monorepo root resolution via relative path traversal.

Five levels of .. from libs/list-types/common/src/pdf/ assumes a fixed directory depth. If the file is ever moved or the structure changes, this silently breaks. Consider deriving the root from a workspace marker (e.g., searching upward for package.json with workspaces) or using an environment variable.

libs/pdf-generation/src/generator.ts (1)

25-28: --no-sandbox disables Chrome's sandbox — document the justification.

This is typically required in containerised environments but is a security trade-off. A brief comment explaining why would help future maintainers.

libs/list-types/care-standards-tribunal-weekly-hearing-list/package.json (1)

17-17: Consider extracting the duplicated build:nunjucks script into a shared shell script or workspace-level task.

This exact command is duplicated across multiple list-type package.json files (e.g., RCJ, civil-and-family). A single shared script would reduce maintenance burden and the risk of divergence.

libs/list-types/civil-and-family-daily-cause-list/src/pdf/pdf-generator.ts (1)

62-62: Non-null assertion on pdfResult.sizeBytes! relies on an implicit contract.

The guard on line 55 checks success and pdfBuffer, but sizeBytes could theoretically be undefined even when both are truthy. Consider including sizeBytes in the guard or defaulting to pdfBuffer.length.

Proposed fix
-    return await savePdfToStorage(options.artefactId, pdfResult.pdfBuffer, pdfResult.sizeBytes!);
+    return await savePdfToStorage(options.artefactId, pdfResult.pdfBuffer, pdfResult.sizeBytes ?? pdfResult.pdfBuffer.length);
libs/list-types/rcj-standard-daily-cause-list/src/pdf/pdf-template.njk (1)

84-91: Duplicate <h4> for court.bundlesTitle when both bundleFilingText and bundlesText exist.

If both court.bundleFilingText and court.bundlesText are present, the heading renders twice. Consider combining them under a single conditional block.

Proposed fix
-    {% if court.bundlesTitle and court.bundleFilingText %}
-      <h4>{{ court.bundlesTitle }}</h4>
-      <p>{{ court.bundleFilingText }}</p>
-    {% endif %}
-    {% if court.bundlesTitle and court.bundlesText %}
-      <h4>{{ court.bundlesTitle }}</h4>
-      <p>{{ court.bundlesText }}</p>
-    {% endif %}
+    {% if court.bundlesTitle and (court.bundleFilingText or court.bundlesText) %}
+      <h4>{{ court.bundlesTitle }}</h4>
+      {% if court.bundleFilingText %}<p>{{ court.bundleFilingText }}</p>{% endif %}
+      {% if court.bundlesText %}<p>{{ court.bundlesText }}</p>{% endif %}
+    {% endif %}
libs/list-types/london-administrative-court-daily-cause-list/src/pdf/pdf-template.njk (2)

2-2: Hardcoded lang="en" won't reflect Welsh locale.

If this template is rendered with Welsh translations, the lang attribute should match. Consider making it dynamic, e.g. {{ language | default("en") }}.

Proposed fix
-<html lang="en">
+<html lang="{{ language | default('en') }}">

32-93: Duplicate table markup for mainHearings and planningCourt.

Both sections share identical column definitions and row templates. A Nunjucks macro could reduce duplication, but this is a minor template concern and fine to defer.

libs/notifications/src/notification/notification-service.test.ts (1)

53-82: Verbose mock re-initialisation in beforeEach.

The top-level vi.mock() blocks (Lines 4–41) already define default implementations. Since vi.clearAllMocks() resets them, every mock is duplicated inside beforeEach. This is functionally correct but adds maintenance overhead — if you change a default, you must update two places.

Consider extracting shared mock-setup helpers, or removing the inline return values from the top-level vi.mock() blocks and relying solely on beforeEach.

libs/list-types/administrative-court-daily-cause-list/src/email-summary/summary-builder.test.ts (2)

5-9: Inconsistent assertion depth for SPECIAL_CATEGORY_DATA_WARNING across list types.

This test only checks for "Special Category Data" (Line 7), while the equivalent test in court-of-appeal-civil-daily-cause-list also asserts the presence of "Data Protection Act 2018". Consider aligning the assertions for consistency.


11-55: Solid coverage of extraction and formatting.

Tests correctly validate the label/value mapping for Administrative Court hearings (including the Hearing type field, which is specific to this list type) and the email formatting output.

Consider adding an edge-case test for an empty hearing list, matching the pattern in the Court of Appeal Civil test suite.

libs/list-types/london-administrative-court-daily-cause-list/src/email-summary/summary-builder.test.ts (1)

11-51: Consider adding edge-case tests for empty data.

The sibling test in court-of-appeal-civil-daily-cause-list covers empty hearing lists and empty formatCaseSummaryForEmail input. Adding similar cases here would ensure consistent coverage across list types.

libs/notifications/src/govnotify/govnotify-client.ts (1)

63-64: Spreading templateParameters into a generic Record<string, unknown> loses type safety.

The TemplateParameters type is widened to Record<string, unknown>, which means any accidental key collision (e.g. a template parameter named link_to_file) would be silently overwritten on line 72. This is a minor concern but worth noting.

libs/list-types/london-administrative-court-daily-cause-list/src/pdf/pdf-generator.ts (1)

53-60: Non-null assertion on pdfResult.sizeBytes! trusts an implicit contract.

The guard checks !pdfResult.pdfBuffer but not sizeBytes. If generatePdfFromHtml ever returns a buffer without sizeBytes, this silently passes undefined to savePdfToStorage.

That said, this matches the pattern in the other PDF generators (e.g. civil-and-family-daily-cause-list, care-standards-tribunal), so it's consistent. If you do tighten it, consider doing so across all generators.

libs/list-types/administrative-court-daily-cause-list/src/pdf/pdf-generator.test.ts (1)

46-103: Test coverage is limited to the happy path.

Only success and render-option verification are tested. Missing coverage for:

  • PDF generation failure (success: false)
  • Exception thrown by renderAdminCourt or generatePdfFromHtml
  • Provenance label resolution

Other list-type PDF generator tests in this PR may have the same gap. Consider adding at least an error-path test.

libs/api/src/blob-ingestion/repository/service.test.ts (1)

305-306: setTimeout(50) for async assertions is fragile.

This pattern (repeated at lines 306, 333, 358, 383) relies on a timing assumption for fire-and-forget async work. It works because mocks resolve instantly, but it's brittle and slow. Consider a flushPromises utility or vi.waitFor() for more deterministic async test control.

Example flushPromises helper
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));

Then replace:

-    await new Promise((resolve) => setTimeout(resolve, 50));
+    await flushPromises();
libs/notifications/package.json (1)

24-32: Growing list of per-list-type dependencies in the notifications package.

Each new list type will require adding another dependency here. Consider whether a registry pattern or dynamic import approach (similar to the converter registry in @hmcts/list-types-common) could decouple notifications from individual list-type packages, avoiding this linear growth.

Not blocking — the current approach works and is explicit.

libs/api/src/blob-ingestion/repository/service.ts (2)

103-103: Unnecessary cast to CauseListData.

processPublication accepts jsonData?: unknown. Casting to CauseListData here is misleading — the data could be any list type's shape. Pass it as-is.

Proposed fix
-        jsonData: request.hearing_list as CauseListData,
+        jsonData: request.hearing_list,

77-77: Duplicated provenance resolution.

PROVENANCE_MAP[request.provenance] || request.provenance appears on both Line 77 and Line 104. Extract to a local variable once.

Proposed fix
+    const resolvedProvenance = PROVENANCE_MAP[request.provenance] || request.provenance;
+
     const artefactId = await createArtefact({
       ...
-      provenance: PROVENANCE_MAP[request.provenance] || request.provenance,
+      provenance: resolvedProvenance,
       ...
     });
     ...
-        provenance: PROVENANCE_MAP[request.provenance] || request.provenance,
+        provenance: resolvedProvenance,

Also applies to: 104-104

libs/notifications/src/govnotify/template-config.ts (2)

48-59: Index signature string | unknown | undefined reduces to unknown.

This effectively disables type checking for any dynamic key access on TemplateParameters. If the intent is to allow link_to_file (added by govnotify-client), consider typing it explicitly as an optional field instead.

Proposed fix
 export interface TemplateParameters {
   locations: string;
   ListType: string;
   content_date: string;
   start_page_link: string;
   subscription_page_link: string;
   display_summary?: string;
   summary_of_cases?: string;
-  [key: string]: string | unknown | undefined;
+  link_to_file?: unknown;
 }

14-14: Boolean parameter pdfUnder2MB should use is prefix per guidelines.

The coding guidelines specify booleans should use is/has/can prefixes. Consider isPdfUnder2MB.

Also, _listTypeId is currently unused. If it's not needed yet, consider removing it to align with YAGNI. Adding it later is a non-breaking change (callers can pass an extra arg).

libs/list-types/civil-and-family-daily-cause-list/src/email-summary/summary-builder.ts (1)

55-84: Six levels of nesting is hard to follow.

The deep nesting mirrors the data model, so it's structurally driven. Consider extracting a flatMapCases helper to flatten the traversal and reduce cognitive load.

libs/publication/src/processing/service.ts (1)

10-10: Rename mockListTypes to reflect its actual purpose.

The name is misleading: mockListTypes is legitimate production reference data containing list type definitions (e.g., CIVIL_DAILY_CAUSE_LIST), not mock/test data. It is used correctly in service.ts for list type lookups.

Per coding guidelines, constants should use SCREAMING_SNAKE_CASE. Consider renaming to LIST_TYPES or LIST_TYPE_REGISTRY in the shared library (@hmcts/list-types-common). This would require coordinated updates across packages that import from this library.

libs/notifications/src/notification/notification-service.ts (1)

165-176: Skip reasons are collected into the errors array.

skipNotification returns { status: "skipped", error: "User X: No email address" }, and aggregateResults (line 256–258) pushes that into result.errors. Downstream consumers may not expect skip reasons mixed in with actual errors. Consider a separate warnings or skippedReasons field, or only push errors for "failed" status.

Also applies to: 252-262

expect(govNotifyEmail.body).toContain("Unsubscribe");

// Verify email delivery status
expect(govNotifyEmail.status).toMatch(/delivered|sending|pending|created|permanent-failure/);

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.

⚠️ Potential issue | 🟠 Major

permanent-failure should not be an acceptable delivery status.

Including permanent-failure in the expected status regex means the test will pass even when the email permanently failed to deliver. This undermines the purpose of verifying delivery.

Proposed fix
-      expect(govNotifyEmail.status).toMatch(/delivered|sending|pending|created|permanent-failure/);
+      expect(govNotifyEmail.status).toMatch(/delivered|sending|pending|created/);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(govNotifyEmail.status).toMatch(/delivered|sending|pending|created|permanent-failure/);
expect(govNotifyEmail.status).toMatch(/delivered|sending|pending|created/);

</tbody>
</table>
{% else %}
<p>No hearings scheduled.</p>

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.

⚠️ Potential issue | 🟠 Major

Hardcoded English string bypasses translations.

"No hearings scheduled." should use a translation key (e.g. {{ t.common.noHearingsMessage }}) to support Welsh rendering, consistent with how other text in this template is handled.

Proposed fix
-    <p>No hearings scheduled.</p>
+    <p>{{ t.common.noHearingsMessage }}</p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p>No hearings scheduled.</p>
<p>{{ t.common.noHearingsMessage }}</p>

courtName: "Care Standards Tribunal",
displayFrom: options.displayFrom,
displayTo: options.displayTo,
lastReceivedDate: new Date().toISOString(),

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.

⚠️ Potential issue | 🟠 Major

lastReceivedDate: new Date().toISOString() captures PDF generation time, not actual data receipt time.

This means the "last updated" timestamp in the PDF will be whenever the PDF happens to be generated, which is misleading. Consider passing the actual last-received date through PdfGenerationOptions instead.

</tbody>
</table>
{% else %}
<p>No hearings scheduled.</p>

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.

⚠️ Potential issue | 🟠 Major

Hardcoded English string — use a translation key.

"No hearings scheduled." won't render in Welsh. Use a translation key such as {{ t.noHearingsScheduled }}.

Proposed fix
-    <p>No hearings scheduled.</p>
+    <p>{{ t.noHearingsScheduled }}</p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p>No hearings scheduled.</p>
<p>{{ t.noHearingsScheduled }}</p>

Comment on lines +6 to +15
function convertPartyRole(role: string): string {
const roleMap: Record<string, string> = {
APPLICANT_PETITIONER: "APPLICANT_PETITIONER",
APPLICANT_PETITIONER_REPRESENTATIVE: "APPLICANT_PETITIONER_REPRESENTATIVE",
RESPONDENT: "RESPONDENT",
RESPONDENT_REPRESENTATIVE: "RESPONDENT_REPRESENTATIVE"
};

return roleMap[role] || role;
}

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.

🛠️ Refactor suggestion | 🟠 Major

convertPartyRole is an identity function — every key maps to itself.

The roleMap maps each role string to the same string, and the fallback || role returns the input unchanged. This function always returns its argument unmodified.

Remove it and use party.partyRole directly, or implement the actual transformation if one is planned. As per coding guidelines: "Follow YAGNI principle: Don't add speculative functionality."

Proposed fix
-function convertPartyRole(role: string): string {
-  const roleMap: Record<string, string> = {
-    APPLICANT_PETITIONER: "APPLICANT_PETITIONER",
-    APPLICANT_PETITIONER_REPRESENTATIVE: "APPLICANT_PETITIONER_REPRESENTATIVE",
-    RESPONDENT: "RESPONDENT",
-    RESPONDENT_REPRESENTATIVE: "RESPONDENT_REPRESENTATIVE"
-  };
-
-  return roleMap[role] || role;
-}
-
 function createPartyDetails(party: Party): string {

Then on line 41:

-    const role = convertPartyRole(party.partyRole);
+    const role = party.partyRole;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function convertPartyRole(role: string): string {
const roleMap: Record<string, string> = {
APPLICANT_PETITIONER: "APPLICANT_PETITIONER",
APPLICANT_PETITIONER_REPRESENTATIVE: "APPLICANT_PETITIONER_REPRESENTATIVE",
RESPONDENT: "RESPONDENT",
RESPONDENT_REPRESENTATIVE: "RESPONDENT_REPRESENTATIVE"
};
return roleMap[role] || role;
}

Comment on lines +76 to +93
{% set durationText = '' %}
{% if sitting.durationAsHours > 0 %}
{% if sitting.durationAsHours > 1 %}
{% set durationText = sitting.durationAsHours ~ ' hours' %}
{% else %}
{% set durationText = sitting.durationAsHours ~ ' hour' %}
{% endif %}
{% endif %}
{% if sitting.durationAsMinutes > 0 %}
{% if durationText | length %}
{% set durationText = durationText ~ ' ' %}
{% endif %}
{% if sitting.durationAsMinutes > 1 %}
{% set durationText = durationText ~ sitting.durationAsMinutes ~ ' mins' %}
{% else %}
{% set durationText = durationText ~ sitting.durationAsMinutes ~ ' min' %}
{% endif %}
{% endif %}

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.

⚠️ Potential issue | 🟠 Major

Duration text is hardcoded in English.

Lines 79–92 use hardcoded strings (hours, hour, mins, min). These won't render correctly for Welsh locale PDFs. Use translation keys instead (e.g., {{ t.hours }}, {{ t.mins }}).

Comment on lines +38 to +51
export async function savePdfToStorage(artefactId: string, pdfBuffer: Buffer, sizeBytes: number): Promise<PdfGenerationResult> {
const exceedsMaxSize = sizeBytes > MAX_PDF_SIZE_BYTES;

await fs.mkdir(TEMP_STORAGE_BASE, { recursive: true });
const pdfPath = path.join(TEMP_STORAGE_BASE, `${artefactId}.pdf`);
await fs.writeFile(pdfPath, pdfBuffer);

return {
success: true,
pdfPath,
sizeBytes,
exceedsMaxSize
};
}

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.

⚠️ Potential issue | 🟠 Major

artefactId is used unsanitised in a file path — path traversal risk.

If artefactId ever contains path separators (e.g., ../../malicious), path.join(TEMP_STORAGE_BASE, ${artefactId}.pdf) could write outside the intended directory. Even if the value is currently system-generated, defensive validation is warranted.

Proposed fix
 export async function savePdfToStorage(artefactId: string, pdfBuffer: Buffer, sizeBytes: number): Promise<PdfGenerationResult> {
   const exceedsMaxSize = sizeBytes > MAX_PDF_SIZE_BYTES;
 
   await fs.mkdir(TEMP_STORAGE_BASE, { recursive: true });
-  const pdfPath = path.join(TEMP_STORAGE_BASE, `${artefactId}.pdf`);
+  const sanitisedId = path.basename(artefactId);
+  const pdfPath = path.join(TEMP_STORAGE_BASE, `${sanitisedId}.pdf`);
   await fs.writeFile(pdfPath, pdfBuffer);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function savePdfToStorage(artefactId: string, pdfBuffer: Buffer, sizeBytes: number): Promise<PdfGenerationResult> {
const exceedsMaxSize = sizeBytes > MAX_PDF_SIZE_BYTES;
await fs.mkdir(TEMP_STORAGE_BASE, { recursive: true });
const pdfPath = path.join(TEMP_STORAGE_BASE, `${artefactId}.pdf`);
await fs.writeFile(pdfPath, pdfBuffer);
return {
success: true,
pdfPath,
sizeBytes,
exceedsMaxSize
};
}
export async function savePdfToStorage(artefactId: string, pdfBuffer: Buffer, sizeBytes: number): Promise<PdfGenerationResult> {
const exceedsMaxSize = sizeBytes > MAX_PDF_SIZE_BYTES;
await fs.mkdir(TEMP_STORAGE_BASE, { recursive: true });
const sanitisedId = path.basename(artefactId);
const pdfPath = path.join(TEMP_STORAGE_BASE, `${sanitisedId}.pdf`);
await fs.writeFile(pdfPath, pdfBuffer);
return {
success: true,
pdfPath,
sizeBytes,
exceedsMaxSize
};
}

import { isValidEmail, type PublicationEvent, validatePublicationEvent } from "./validation.js";
import { type PublicationEvent, validatePublicationEvent } from "./validation.js";

const MAX_PDF_SIZE_BYTES = 2 * 1024 * 1024;

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.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n 'MAX_PDF_SIZE_BYTES' --type=ts

Repository: hmcts/cath-service

Length of output: 1202


🏁 Script executed:

# Check the specific file and lines mentioned
cat -n libs/list-types/common/src/index.ts | grep -A 2 -B 2 'MAX_PDF_SIZE_BYTES'

Repository: hmcts/cath-service

Length of output: 210


🏁 Script executed:

# Verify the value in notification-service.ts
cat -n libs/notifications/src/notification/notification-service.ts | sed -n '35p'

Repository: hmcts/cath-service

Length of output: 112


🏁 Script executed:

# Verify the current imports from `@hmcts/list-types-common` in notification-service.ts
sed -n '18p' libs/notifications/src/notification/notification-service.ts

Repository: hmcts/cath-service

Length of output: 158


Remove duplicate constant and import from common package.

MAX_PDF_SIZE_BYTES is already exported from @hmcts/list-types-common. Remove the local definition at line 35 and add it to the import statement at line 18 to avoid maintaining the same value in two locations.

Suggested fix

Remove line 35:

-const MAX_PDF_SIZE_BYTES = 2 * 1024 * 1024;

Update line 18:

-import { type CaseSummary, getListTypeName, type ListTypeName } from "@hmcts/list-types-common";
+import { type CaseSummary, getListTypeName, type ListTypeName, MAX_PDF_SIZE_BYTES } from "@hmcts/list-types-common";

Comment on lines +1 to +6
interface PdfGenerationResult {
success: boolean;
pdfBuffer?: Buffer;
sizeBytes?: number;
error?: string;
}

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.

🛠️ Refactor suggestion | 🟠 Major

PdfGenerationResult is not exported and is duplicated in pdf-utilities.ts.

This interface is defined here but not exported, so consumers cannot reference the return type. Additionally, a separate PdfGenerationResult interface exists in libs/list-types/common/src/pdf/pdf-utilities.ts with different fields (pdfPath vs pdfBuffer). Consider exporting this one and having pdf-utilities.ts import or extend it to avoid drift.

Comment on lines +30 to +34
const page = await browser.newPage();

await page.setContent(html, {
waitUntil: "networkidle0"
});

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.

⚠️ Potential issue | 🔴 Critical

No timeout on setContent with networkidle0 — potential hang.

If the provided HTML references external resources that never resolve, networkidle0 will wait indefinitely. Add a timeout to prevent the process from hanging.

Proposed fix
     await page.setContent(html, {
-      waitUntil: "networkidle0"
+      waitUntil: "networkidle0",
+      timeout: 30_000
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const page = await browser.newPage();
await page.setContent(html, {
waitUntil: "networkidle0"
});
const page = await browser.newPage();
await page.setContent(html, {
waitUntil: "networkidle0",
timeout: 30_000
});

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/list-types/court-of-appeal-civil-daily-cause-list/package.json (1)

37-42: ⚠️ Potential issue | 🟡 Minor

Missing @types/nunjucks in devDependencies.

nunjucks is used in TypeScript source files (e.g. pdf-generator.ts), but @types/nunjucks is absent from devDependencies. Without it, tsc will either error or silently resolve the import as any.

🛠️ Proposed fix
  "devDependencies": {
    "@types/luxon": "3.7.1",
    "@types/node": "24.10.4",
+   "@types/nunjucks": "3.2.6",
    "typescript": "5.9.3",
    "vitest": "4.0.18"
  },
🧹 Nitpick comments (3)
libs/list-types/civil-and-family-daily-cause-list/package.json (1)

18-18: build:pdf-templates glob may break on empty src/pdf/

cp src/pdf/*.njk dist/pdf/ will error if the glob matches nothing (shell passes the literal string to cp, producing "No such file or directory"). The existing build:nunjucks script uses find … -exec precisely to avoid this. Aligning the new script with that pattern would make it equally resilient.

♻️ Proposed fix
-"build:pdf-templates": "mkdir -p dist/pdf && cp src/pdf/*.njk dist/pdf/",
+"build:pdf-templates": "mkdir -p dist/pdf && find src/pdf -name '*.njk' -exec sh -c 'cp {} dist/pdf/$(basename {})' \\;",
libs/list-types/court-of-appeal-civil-daily-cause-list/package.json (1)

17-17: Fragile cd chaining and unsafe {} substitution in build:nunjucks.

Two issues:

  1. Fragile cd chain — the sequence cd src/pages && find ... && cd ../.. && ... && cd src/pdf runs in a single shell process. If the find -exec block exits non-zero, cd ../.. is skipped and the subsequent cd src/pdf resolves against the wrong working directory, silently copying nothing (or crashing).

  2. sh -c '... {} ...' anti-pattern — embedding {} directly inside the shell string rather than passing it as a positional argument breaks on filenames with spaces or special characters, and is flagged as a shell injection risk by most linters.

Use subshells to isolate cd and pass {} as a positional argument:

♻️ Proposed fix
-"build:nunjucks": "mkdir -p dist/pages && cd src/pages && find . -name '*.njk' -exec sh -c 'mkdir -p ../../dist/pages/$(dirname {}) && cp {} ../../dist/pages/{}' \\; && cd ../.. && mkdir -p dist/pdf && cd src/pdf && find . -name '*.njk' -exec sh -c 'mkdir -p ../../dist/pdf/$(dirname {}) && cp {} ../../dist/pdf/{}' \\;",
+"build:nunjucks": "mkdir -p dist/pages && (cd src/pages && find . -name '*.njk' -exec sh -c 'mkdir -p \"../../dist/pages/$(dirname \"$1\")\" && cp \"$1\" \"../../dist/pages/$1\"' _ {} \\;) && mkdir -p dist/pdf && (cd src/pdf && find . -name '*.njk' -exec sh -c 'mkdir -p \"../../dist/pdf/$(dirname \"$1\")\" && cp \"$1\" \"../../dist/pdf/$1\"' _ {} \\;)",
libs/list-types/london-administrative-court-daily-cause-list/package.json (1)

17-17: LGTM — PDF template copy mirrors the existing pages pattern correctly.

The cd ../.. navigation back to the package root before entering src/pdf is correct, and ../../dist/pdf/ from within src/pdf resolves as expected.

Optionally, the two find-based copy blocks could be extracted into a small helper shell script (e.g., scripts/copy-njk.sh <src> <dest>) to reduce the one-liner length and make future additions trivial — but this is purely a readability improvement.


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b09e037 and d823a4b.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (7)
  • libs/list-types/administrative-court-daily-cause-list/package.json
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/package.json
  • libs/list-types/civil-and-family-daily-cause-list/package.json
  • libs/list-types/court-of-appeal-civil-daily-cause-list/package.json
  • libs/list-types/london-administrative-court-daily-cause-list/package.json
  • libs/list-types/rcj-standard-daily-cause-list/package.json
  • package.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • libs/list-types/administrative-court-daily-cause-list/package.json
  • package.json
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/package.json
  • libs/list-types/rcj-standard-daily-cause-list/package.json

"exceljs": "4.4.0",
"luxon": "3.7.2"
"luxon": "3.7.2",
"nunjucks": "3.2.4"

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual package.json file
cat -n libs/list-types/london-administrative-court-daily-cause-list/package.json

Repository: hmcts/cath-service

Length of output: 1925


🏁 Script executed:

# Check if `@types/nunjucks` is declared anywhere in the workspace
rg "@types/nunjucks" libs/

Repository: hmcts/cath-service

Length of output: 215


🏁 Script executed:

# Check if nunjucks is imported directly in source files
fd "\.ts$" libs/list-types/london-administrative-court-daily-cause-list/src --exec grep -l "nunjucks" {} \;

Repository: hmcts/cath-service

Length of output: 44


🏁 Script executed:

# Verify `@hmcts/pdf-generation` package dependencies
cat -n libs/pdf-generation/package.json | grep -A 50 "devDependencies\|dependencies"

Repository: hmcts/cath-service

Length of output: 254


Add @types/nunjucks to devDependencies for consistency with workspace pattern.

nunjucks is declared as a dependency but @types/nunjucks is missing from devDependencies. Other similar packages in the workspace (e.g., civil-and-family-daily-cause-list, web-core) include it. Although no direct nunjucks imports are currently present in source files, including the types aligns with project standards and protects against any-type fallback if nunjucks is imported.

Proposed addition
   "devDependencies": {
+    "@types/nunjucks": "3.2.6",
     "@types/luxon": "3.7.1",

@ChrisS1512
ChrisS1512 merged commit 7e6e88b into master Feb 24, 2026
24 of 25 checks passed
@KianKwa
KianKwa deleted the feature/VIBE-339 branch April 13, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants