Skip to content

VIBE-341 Add PDF and email summary generation for Civil & Family Daily Cause List - #320

Merged
ChrisS1512 merged 21 commits into
masterfrom
feature/VIBE-341
Feb 24, 2026
Merged

VIBE-341 Add PDF and email summary generation for Civil & Family Daily Cause List#320
ChrisS1512 merged 21 commits into
masterfrom
feature/VIBE-341

Conversation

@KianKwa

@KianKwa KianKwa commented Jan 28, 2026

Copy link
Copy Markdown
Contributor

Jira link

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

Change description

Add PDF and email summary generation for Civil & Family Daily Cause 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

Release Notes

New Features

  • Added email summary functionality for Civil and Family Daily Cause List subscriptions, including case details and formatting.
  • Implemented PDF generation and delivery for subscription emails with three template variants (PDF + summary, summary-only, and original).
  • Enhanced email templates to include case summaries and PDF links with automatic size validation.

Tests

  • Added comprehensive end-to-end tests for subscription notification delivery and email content validation.
  • Extended test coverage for PDF generation and email delivery workflows.

Documentation

  • Added detailed technical planning and implementation guidance for the subscription fulfilment workflow.

@coderabbitai

coderabbitai Bot commented Jan 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements the VIBE-341 subscription fulfilment feature, adding PDF generation from Civil and Family Daily Cause List HTML, email summary extraction, GOV.UK Notify template selection (PDF+summary, summary-only, legacy), and centralized publication processing orchestration.

Changes

Cohort / File(s) Summary
Configuration & Dependency Management
apps/web/.env.example, package.json, tsconfig.json
Added environment variables for GOV.UK Notify template IDs and CATH service URL; added puppeteer dependency and path alias for pdf-generation library.
Planning & Documentation
docs/tickets/VIBE-341/*
Added technical planning, task checklist, and ticket documentation detailing modular subscription fulfilment workflow, PDF generation, GOV.UK Document Service integration, email templates, and comprehensive acceptance criteria.
PDF Generation Library
libs/pdf-generation/*
New ESM package providing generatePdfFromHtml function using Puppeteer to convert HTML to PDF with size validation and error handling.
Civil & Family Daily Cause List - PDF & Email
libs/list-types/civil-and-family-daily-cause-list/package.json, libs/list-types/civil-and-family-daily-cause-list/src/pdf/*, libs/list-types/civil-and-family-daily-cause-list/src/email-summary/*, libs/list-types/civil-and-family-daily-cause-list/src/index.ts, libs/list-types/civil-and-family-daily-cause-list/src/pages/*
Added PDF generation module with Nunjucks template, email summary builder (extractCaseSummary, formatCaseSummaryForEmail), new translations (cautionNote, cautionReporting), updated exports, and build scripts for PDF template compilation.
Renderer Enhancements
libs/list-types/civil-and-family-daily-cause-list/src/rendering/renderer.ts, libs/list-types/civil-and-family-daily-cause-list/src/rendering/renderer.test.ts
Improved address formatting to include town and county; refactored loops for null-safety and added computed fields (duration, channel, formatted restrictions) for PDF rendering.
GOV.UK Notify Integration
libs/notifications/src/govnotify/govnotify-client.ts, libs/notifications/src/govnotify/govnotify-client.test.ts, libs/notifications/src/govnotify/template-config.ts, libs/notifications/src/govnotify/template-config.test.ts
Extended SendEmailParams with optional templateId and pdfBuffer; added PDF upload handling via notify.prepareUpload; introduced getSubscriptionTemplateIdForListType to select templates based on list type and PDF size; added buildEnhancedTemplateParameters for case summaries.
Notification Service Refactoring
libs/notifications/src/notification/notification-service.ts, libs/notifications/src/notification/notification-service.test.ts, libs/notifications/src/notification/validation.ts
Centralised email data construction with buildEmailTemplateData; added Civil & Family specific flow with enhanced templates and PDF embedding; extended PublicationEvent interface with listTypeId, pdfFilePath, and jsonData; refactored validation and result aggregation.
Publication Processing Orchestration
libs/publication/src/processing/service.ts, libs/publication/src/processing/service.test.ts, libs/publication/src/index.ts
New module providing generatePublicationPdf, sendPublicationNotificationsForArtefact, and processPublication; orchestrates PDF generation (when jsonData present) and notification dispatch with structured result aggregation and error handling.
Admin Upload Integration
libs/admin-pages/src/pages/manual-upload-summary/index.ts, libs/admin-pages/src/pages/manual-upload-summary/index.test.ts
Replaced direct notification flow with unified processPublication call; added jsonData parsing for structured data; delegated PDF generation and notification to central processor.
API Blob Ingestion Integration
libs/api/src/blob-ingestion/repository/service.ts, libs/api/src/blob-ingestion/repository/service.test.ts
Replaced triggerPublicationNotifications with processPublication; updated payload to include listTypeId, contentDate, locale, jsonData, and provenance; simplified control flow with unified processing path.
End-to-End Tests
e2e-tests/tests/subscription-notifications.spec.ts
Added comprehensive Playwright test suite validating subscription email notifications, multi-subscriber handling, content verification (title, court, date, special category warnings), and GOV.UK Notify API integration.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin/API
    participant Upload as Upload Service
    participant PDF as PDF Generator
    participant Summary as Summary Builder
    participant Notify as GOV.UK Notify
    participant Email as Subscriber Email

    Admin->>Upload: Upload Publication (jsonData)
    Upload->>PDF: generatePublicationPdf(causeListData)
    PDF->>PDF: Render HTML + Nunjucks Template
    PDF->>PDF: Convert to PDF (Puppeteer)
    PDF-->>Upload: Return {pdfPath, sizeBytes, exceedsMaxSize}
    
    Upload->>Summary: extractCaseSummary(jsonData)
    Summary-->>Upload: Return CaseSummaryItem[]
    Upload->>Summary: formatCaseSummaryForEmail(items)
    Summary-->>Upload: Return formatted summary string
    
    Upload->>Notify: Select template based on PDF size & listTypeId
    alt PDF present and < 2MB
        Upload->>Notify: sendEmail(pdfBuffer, templateId_PDF_AND_SUMMARY)
    else No PDF or >= 2MB
        Upload->>Notify: sendEmail(templateId_SUMMARY_ONLY)
    end
    
    Notify->>Notify: Upload PDF via prepareUpload (if present)
    Notify->>Email: Send email with personalisation & link_to_file
    Email-->>Notify: Delivery status
    Notify-->>Upload: Notification result {sent, failed, skipped}
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarises the main objective of the pull request—adding PDF and email summary generation for Civil & Family Daily Cause List. It is concise, specific, and directly reflects the primary changes across the changeset.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

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

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

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 28, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright E2E Test Results

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

Results for commit de75f03.

♻️ This comment has been updated with latest results.

@KianKwa
KianKwa changed the base branch from master to feature/VIBE-323 January 29, 2026 09:09
@KianKwa
KianKwa changed the base branch from feature/VIBE-323 to master January 29, 2026 12:23
@KianKwa
KianKwa changed the base branch from master to feature/VIBE-323 January 29, 2026 14:20
Comment thread libs/notifications/src/govnotify/template-config.ts
Comment thread libs/notifications/src/notification/notification-service.ts Outdated
Comment thread libs/notifications/src/notification/notification-service.ts
@KianKwa
KianKwa changed the base branch from feature/VIBE-323 to master February 13, 2026 13:48
@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: 15

Caution

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

⚠️ Outside diff range comments (2)
libs/list-types/civil-and-family-daily-cause-list/src/rendering/renderer.ts (1)

192-241: ⚠️ Potential issue | 🟡 Minor

renderCauseListData mutates the input jsonData object in-place.

The function modifies the original object by adding properties like formattedJudiciaries to session objects (line 217) and directly assigning values to case items. This violates the immutability principle outlined in the coding guidelines. While current usage patterns pass a single locale per invocation—preventing the specific multi-render scenario initially described—in-place mutations remain a code quality issue that could cause problems if the usage patterns change.

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

100-110: ⚠️ Potential issue | 🟡 Minor

Retry count semantics: NOTIFICATION_RETRY_ATTEMPTS=1 means 2 total attempts.

retryWithBackoff calls fn() once, then on failure retries retries more times. So if NOTIFICATION_RETRY_ATTEMPTS is 1, the email send is attempted twice total. Ensure this matches the intended behaviour — typically "retry attempts" means additional attempts beyond the first.

🧹 Nitpick comments (13)
package.json (1)

87-87: Consider alternatives for production deployments.

Puppeteer v24.36.1 is a valid version, but bundling a full Chromium browser (~400MB+) as a runtime dependency will increase container image size and cold start times. Evaluate whether puppeteer-core paired with a pre-installed browser in your Docker image would be more efficient for production use.

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

1-6: PdfGenerationResult is not exported.

Downstream consumers (e.g., pdf-generator.ts in the cause-list module) cannot explicitly reference this type. Consider exporting the interface so callers can type their variables without relying solely on inference.

libs/admin-pages/src/pages/manual-upload-summary/index.ts (1)

112-120: Silent catch swallows all JSON parse errors — consider logging.

The fallback to flat-file behaviour is intentional, but a malformed JSON file (e.g. truncated upload) will silently proceed with jsonData as undefined. A debug-level log here would aid troubleshooting without changing control flow.

Suggested improvement
       try {
         jsonData = JSON.parse(uploadData.file.toString("utf8")) as CauseListData;
-      } catch {
-        // Not valid JSON, treat as flat file
+      } catch (parseError) {
+        console.debug("[Manual Upload] File is not valid JSON, treating as flat file:", parseError);
       }
libs/notifications/src/notification/notification-service.test.ts (1)

53-82: Verbose but necessary mock reset pattern.

The async beforeEach that re-imports and re-mocks every dependency is repetitive. If the boilerplate grows further, extracting a setupDefaultMocks() helper would reduce noise. Not blocking.

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

85-116: Heavy mutation via as any bypasses type safety and violates immutability guidelines.

calculateDuration, formatHearingChannel, processParties, and formatReportingRestrictions all mutate their input objects and cast to any to write computed fields. This creates invisible side effects and disables type checking on these augmented properties.

Consider extending the model types with optional computed fields (e.g. time?: string, durationAsHours?: number) or returning new enriched objects from renderCauseListData instead of mutating the input. This would eliminate every as any cast in the file.

As per coding guidelines: "Ensure data immutability by default — use const and avoid mutations. Functions should have no side effects and should not modify external state." and "Avoid any without justification."

docs/tickets/VIBE-341/plan.md (1)

73-135: Plan diverges from implementation — consider updating.

The plan describes libs/subscription-fulfilment and libs/document-service as new modules, but the actual PR uses libs/publication for orchestration and doesn't introduce a separate document-service module. Keeping the plan aligned with reality avoids confusion for future contributors.

Also, the file structure block on line 73 is missing a language specifier (flagged by markdownlint MD040). Use ```text or similar.

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

11-20: convertPartyRole is a no-op — every input maps to itself.

The roleMap maps each key to an identical value, and the fallback (|| role) returns the original string anyway. This function always returns its input unchanged.

Either this is scaffolding for future mapping (in which case a comment explaining intent would help), or it can be removed entirely.


60-84: Six levels of nesting — consider a flatMap chain for readability.

The deeply nested for loops mirror the data model faithfully, but a flatMap chain would flatten the traversal and reduce indentation.

♻️ Optional refactor
 export function extractCaseSummary(jsonData: CauseListData): CaseSummaryItem[] {
-  const summaries: CaseSummaryItem[] = [];
-
-  for (const courtList of jsonData.courtLists) {
-    for (const courtRoom of courtList.courtHouse.courtRoom) {
-      for (const session of courtRoom.session) {
-        for (const sitting of session.sittings) {
-          for (const hearing of sitting.hearing) {
-            for (const caseItem of hearing.case) {
-              summaries.push({
-                applicant: extractApplicant(caseItem),
-                caseReferenceNumber: caseItem.caseNumber || "",
-                caseName: caseItem.caseName || "",
-                caseType: caseItem.caseType || "",
-                hearingType: hearing.hearingType || ""
-              });
-            }
-          }
-        }
-      }
-    }
-  }
-
-  return summaries;
+  return jsonData.courtLists
+    .flatMap((cl) => cl.courtHouse.courtRoom)
+    .flatMap((cr) => cr.session)
+    .flatMap((s) => s.sittings)
+    .flatMap((sit) => sit.hearing)
+    .flatMap((hearing) =>
+      hearing.case.map((caseItem) => ({
+        applicant: extractApplicant(caseItem),
+        caseReferenceNumber: caseItem.caseNumber || "",
+        caseName: caseItem.caseName || "",
+        caseType: caseItem.caseType || "",
+        hearingType: hearing.hearingType || ""
+      }))
+    );
 }
libs/notifications/src/govnotify/template-config.ts (1)

62-66: Index signature string | unknown | undefined is effectively just unknown.

Since unknown already encompasses both string and undefined, the union is redundant. If the intent is to allow arbitrary keys while keeping the named properties typed as string, consider narrowing the index signature or using a mapped/intersection type instead — the current signature silently weakens the type of all named fields.

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

135-135: Unsafe cast event.jsonData as CauseListData — consider validation.

If jsonData doesn't conform to the CauseListData shape, extractCaseSummary will throw at runtime. The surrounding try/catch does catch this, but a schema validation step (e.g., using the existing validateCivilFamilyCauseList) before casting would provide clearer error messages and prevent silent fallback on malformed data.

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

36-46: Nunjucks environment re-created on every call.

configureNunjucks() is called per PDF generation. Since noCache: true is set, this is functionally correct, but creating a new Environment each time is unnecessary overhead. Consider initialising it once at module level.


13-15: Monorepo root resolution via relative path traversal is fragile and used consistently across the codebase.

This pattern (5 levels of .. traversal) is repeated in at least 12 files. While it works when all files are at the same relative depth, moving a package or changing directory structure breaks all uses simultaneously. Consider establishing a single, centralised mechanism—either via a shared utility, process.cwd(), or environment variable—rather than duplicating this logic across the codebase.

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

8-17: Interfaces are interleaved with their associated functions rather than grouped at the bottom.

Per the coding guidelines, interfaces and types should be at the bottom of the module. The current placement alongside each function aids readability for this file, so it's a trade-off. Consider moving them to the bottom if consistency with the guideline is preferred.

As per coding guidelines: "Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom."

Also applies to: 19-23, 58-74, 127-145

- [x] Create `libs/pdf-generation` module (package.json, tsconfig.json)
- [x] Register module in root tsconfig.json paths
- [x] Create `libs/pdf-generation/src/generator.ts`:
- `generatePdfFromHtml(html: string): Promise<Buffer>` - Puppeteer-based conversion

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

Documentation drift: return type doesn't match implementation.

The documented signature says Promise<Buffer>, but generatePdfFromHtml actually returns Promise<PdfGenerationResult>. Worth correcting to avoid misleading future readers.

8. **Email Format Requirements**:

**Opening Statement:**
> Note this email contains Special Category Data as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be handled appropriately.

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

🧩 Analysis chain

🏁 Script executed:

cat -n docs/tickets/VIBE-341/ticket.md | sed -n '50,55p'

Repository: hmcts/cath-service

Length of output: 641


🏁 Script executed:

rg -in "formally known as"

Repository: hmcts/cath-service

Length of output: 628


🏁 Script executed:

rg -in "formerly known as"

Repository: hmcts/cath-service

Length of output: 300


Fix typo in legal/compliance notice: "formally" should be "formerly".

The text uses "formally known as" which means "in a formal manner", but should be "formerly known as" meaning "previously". This typo has propagated to production code in libs/list-types/civil-and-family-daily-cause-list/src/pages/en.ts:41 and must be corrected in both locations, as this is user-facing legal language.

🧰 Tools
🪛 LanguageTool

[misspelling] ~52-~52: Did you mean “formerly known as” (=known earlier under the name of)?
Context: ...as defined by Data Protection Act 2018, formally known as Sensitive Personal Data, and should be ...

(FORMALLY_KNOWN_AS)

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 | 🟡 Minor

Status assertion accepts permanent-failure — weakens the delivery check.

Including permanent-failure in the expected status regex means the test passes even when email delivery fails. If this is a CI workaround, a comment would clarify intent. Otherwise, restrict to /delivered|sending|pending|created/.

Comment on lines +262 to +314
it("should handle missing optional fields with N/A", () => {
const testData: CauseListData = {
document: {
publicationDate: "2025-01-28T10:00:00Z"
},
venue: {
venueName: "Test Court",
venueAddress: {
line: ["123 Test Street"],
postCode: "TEST 123"
}
},
courtLists: [
{
courtHouse: {
courtHouseName: "Test Courthouse",
courtRoom: [
{
courtRoomName: "Court 1",
session: [
{
sittings: [
{
hearing: [
{
case: [
{
party: []
}
]
}
]
}
]
}
]
}
]
}
}
]
};

const result = extractCaseSummary(testData);

expect(result[0]).toEqual({
applicant: "",
caseReferenceNumber: "",
caseName: "",
caseType: "",
hearingType: ""
});
});

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

Misleading test description: says "N/A" but asserts empty strings.

The test name reads "should handle missing optional fields with N/A", yet all assertions check for "". The description should match the actual behaviour.

✏️ Suggested fix
-  it("should handle missing optional fields with N/A", () => {
+  it("should handle missing optional fields with empty strings", () => {
📝 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
it("should handle missing optional fields with N/A", () => {
const testData: CauseListData = {
document: {
publicationDate: "2025-01-28T10:00:00Z"
},
venue: {
venueName: "Test Court",
venueAddress: {
line: ["123 Test Street"],
postCode: "TEST 123"
}
},
courtLists: [
{
courtHouse: {
courtHouseName: "Test Courthouse",
courtRoom: [
{
courtRoomName: "Court 1",
session: [
{
sittings: [
{
hearing: [
{
case: [
{
party: []
}
]
}
]
}
]
}
]
}
]
}
}
]
};
const result = extractCaseSummary(testData);
expect(result[0]).toEqual({
applicant: "",
caseReferenceNumber: "",
caseName: "",
caseType: "",
hearingType: ""
});
});
it("should handle missing optional fields with empty strings", () => {
const testData: CauseListData = {
document: {
publicationDate: "2025-01-28T10:00:00Z"
},
venue: {
venueName: "Test Court",
venueAddress: {
line: ["123 Test Street"],
postCode: "TEST 123"
}
},
courtLists: [
{
courtHouse: {
courtHouseName: "Test Courthouse",
courtRoom: [
{
courtRoomName: "Court 1",
session: [
{
sittings: [
{
hearing: [
{
case: [
{
party: []
}
]
}
]
}
]
}
]
}
]
}
}
]
};
const result = extractCaseSummary(testData);
expect(result[0]).toEqual({
applicant: "",
caseReferenceNumber: "",
caseName: "",
caseType: "",
hearingType: ""
});
});

Comment on lines +40 to +41
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.",

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:

# Verify the typo in the en.ts file
find . -type f -name "en.ts" -path "*/civil-and-family-daily-cause-list/*" -exec head -50 {} + | tail -20

Repository: hmcts/cath-service

Length of output: 1234


🏁 Script executed:

# Search for "formally known" pattern to identify all occurrences
rg -n "formally known" --type=ts

Repository: hmcts/cath-service

Length of output: 307


🏁 Script executed:

# Check Welsh translation file for the same pattern
find . -type f -name "cy.ts" -path "*/civil-and-family-daily-cause-list/*" -exec cat {} +

Repository: hmcts/cath-service

Length of output: 3291


Fix typo in caution note message: "formally" should be "formerly".

Line 41 contains "formally known as Sensitive Personal Data" which should read "formerly known as Sensitive Personal Data". The word "formerly" (previously) is correct here; "formally" (in a formal manner) is incorrect.

The Welsh translation (cy.ts) is already correct, using "a elwid gynt" which properly conveys the past tense.

Proposed fix
   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.",
+    "Note this document contains Special Category Data as defined by Data Protection Act 2018, formerly known as Sensitive Personal Data, and should be handled appropriately.",
📝 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
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.",
cautionNote:
"Note this document contains Special Category Data as defined by Data Protection Act 2018, formerly known as Sensitive Personal Data, and should be handled appropriately.",

Comment on lines +84 to +96
export function buildEnhancedTemplateParameters(params: {
userName: string;
hearingListName: string;
publicationDate: Date;
locationName: string;
caseSummary: string;
}): TemplateParameters {
const baseParams = buildTemplateParameters({
userName: params.userName,
hearingListName: params.hearingListName,
publicationDate: params.publicationDate,
locationName: params.locationName
});

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:

cat -n libs/notifications/src/govnotify/template-config.ts

Repository: hmcts/cath-service

Length of output: 4530


🏁 Script executed:

rg -n 'buildTemplateParameters|buildEnhancedTemplateParameters' libs/notifications/src/

Repository: hmcts/cath-service

Length of output: 3736


🏁 Script executed:

sed -n '120,135p' libs/notifications/src/notification/notification-service.ts && echo "---" && sed -n '135,150p' libs/notifications/src/notification/notification-service.ts && echo "---" && sed -n '155,170p' libs/notifications/src/notification/notification-service.ts

Repository: hmcts/cath-service

Length of output: 1663


🏁 Script executed:

sed -n '138,195p' libs/notifications/src/govnotify/template-config.test.ts

Repository: hmcts/cath-service

Length of output: 2347


Remove unused userName parameter from both functions.

The userName parameter is accepted in both buildTemplateParameters and buildEnhancedTemplateParameters but is not included in the returned TemplateParameters object. Either add it to the template output if it's needed, or remove it from the function signatures to avoid confusion.

Comment on lines +518 to +522
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
templateId: undefined
})
);

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

🧩 Analysis chain

🏁 Script executed:

rg -n -B5 -A5 'templateId' libs/notifications/src/notification/notification-service.ts | head -100

Repository: hmcts/cath-service

Length of output: 2179


🏁 Script executed:

# Also check the fallback logic flow in the test and production
rg -n -B3 -A3 'buildTemplateParameters|buildEnhancedTemplateParameters' libs/notifications/src/notification/notification-service.ts | head -80

Repository: hmcts/cath-service

Length of output: 1345


🏁 Script executed:

# Examine the actual sendEmail call in the production code
rg -n -B10 -A10 'sendEmail' libs/notifications/src/notification/notification-service.ts | head -120

Repository: hmcts/cath-service

Length of output: 1656


Production code bug: fallback path omits templateId, causing undefined to be passed to sendEmail.

The test correctly captures broken production behaviour. When the enhanced template build fails (lines 154–161 of notification-service.ts), the catch block returns only templateParameters without setting templateId. This causes undefined to be passed to sendEmail at line 78, which will likely fail the notification.

The fallback mechanism must supply a default template ID. Ensure buildTemplateParameters returns proper template configuration or explicitly set templateId in the fallback return at line 156.

Comment on lines +133 to 165
async function buildCivilFamilyEmailData(event: PublicationEvent, userName: string): Promise<EmailTemplateData> {
try {
const caseSummaryItems = extractCaseSummary(event.jsonData as CauseListData);
const caseSummary = formatCaseSummaryForEmail(caseSummaryItems);

const templateParameters = buildEnhancedTemplateParameters({
userName,
hearingListName: event.hearingListName,
publicationDate: event.publicationDate,
locationName: event.locationName,
caseSummary
});

if (event.pdfFilePath) {
return buildCivilFamilyWithPdf(event.pdfFilePath, templateParameters);
}

return {
status: "failed",
error: `User ${subscription.userId}: ${emailResult.error}`
templateParameters,
templateId: getSubscriptionTemplateIdForListType(CIVIL_AND_FAMILY_DAILY_CAUSE_LIST_ID, false, false)
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Failed to build enhanced template parameters, falling back to standard template:", error);
return {
status: "failed",
error: `User ${subscription.userId}: ${errorMessage}`
templateParameters: buildTemplateParameters({
userName,
hearingListName: event.hearingListName,
publicationDate: event.publicationDate,
locationName: event.locationName
})
};
}
}

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

Fallback path omits templateId — will use the default subscription template.

When buildCivilFamilyEmailData catches an error (line 154), it falls back to buildTemplateParameters without setting templateId. Downstream, sendEmailInternal will then use getTemplateId() (the generic subscription template). This may send a Civil & Family notification using a template that lacks summary/PDF placeholders.

If this is intentional graceful degradation, a log message noting which template is being used would aid debugging. If not, consider explicitly setting the summary-only template ID in the fallback.

Comment on lines +19 to +55
export async function generatePdfFromHtml(html: string): Promise<PdfGenerationResult> {
// Dynamically import puppeteer to avoid ESM/CJS issues in tests
const puppeteer = await import("puppeteer");
let browser: Awaited<ReturnType<typeof puppeteer.default.launch>> | undefined;

try {
browser = await puppeteer.default.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
});

const page = await browser.newPage();

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

const pdfBuffer = await page.pdf(PDF_OPTIONS);

const sizeBytes = pdfBuffer.length;

return {
success: true,
pdfBuffer: Buffer.from(pdfBuffer),
sizeBytes
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
success: false,
error: `PDF generation failed: ${errorMessage}`
};
} finally {
if (browser) {
await browser.close();
}
}

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

Add a timeout to Puppeteer operations to prevent indefinite hangs.

Neither puppeteer.launch() nor page.pdf() has a timeout configured. In production, a malformed or very large HTML payload could cause this function to block indefinitely.

Consider adding a timeout to the launch options and wrapping the critical section in an AbortSignal.timeout or similar guard:

Proposed fix
     browser = await puppeteer.default.launch({
       headless: true,
-      args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
+      args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
+      timeout: 30_000
     });
 
     const page = await browser.newPage();
+    page.setDefaultTimeout(30_000);

import { type CauseListData, generateCauseListPdf } from "@hmcts/civil-and-family-daily-cause-list";
import { getLocationById } from "@hmcts/location";
import { sendPublicationNotifications } from "@hmcts/notifications";
import { mockListTypes } from "../index.js";

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 'mockListTypes' --type=ts -C 3

Repository: hmcts/cath-service

Length of output: 50374


🏁 Script executed:

# Find the index.js file being imported from
fd index.ts index.js -p '*publication*'

Repository: hmcts/cath-service

Length of output: 217


🏁 Script executed:

# Check the index.js or index.ts file in the publication directory
fd -t f 'index\.(ts|js)$' 'libs/publication/src/'

Repository: hmcts/cath-service

Length of output: 91


Rename mockListTypes to clarify it is the production list type registry.

The mockListTypes export used throughout the system contains actual production data (list type definitions with IDs and localised friendly names). The "mock" prefix is misleading—rename to LIST_TYPES or listTypeRegistry to accurately signal this is the authoritative list type registry, not test scaffolding.

@ChrisS1512
ChrisS1512 merged commit fa2577d into master Feb 24, 2026
125 checks passed
@KianKwa
KianKwa deleted the feature/VIBE-341 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.

3 participants