Skip to content

VIBE-300 Add subscription by case name and case number - #318

Closed
KianKwa wants to merge 52 commits into
masterfrom
feature/VIBE-300
Closed

VIBE-300 Add subscription by case name and case number#318
KianKwa wants to merge 52 commits into
masterfrom
feature/VIBE-300

Conversation

@KianKwa

@KianKwa KianKwa commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

Jira link

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

Change description

Add subscription by case name and case number

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 case-based subscription functionality—users can now subscribe to hearing lists by case name or case reference number, receiving email notifications for matching cases.
    • Introduced system admin interface to configure case field extraction from list types.
    • Enhanced subscription management interface with dedicated views for court/tribunal and case subscriptions.
  • Bug Fixes

    • Updated subscription storage to use generic search field mechanism, improving system flexibility.
    • Fixed notification audit log deletion to cascade properly with parent subscriptions.
  • Documentation

    • Added comprehensive specification and implementation plan for case subscription features.

github-actions Bot and others added 27 commits January 13, 2026 17:10
…ion and subscription process

- Created specification document outlining database schema changes, admin configuration page, and publication processing requirements
- Created implementation plan with phased approach and critical files to create/modify
- Created detailed task list with clear dependencies and testing requirements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…system

This commit implements the database and application changes required to
support multiple subscription search types (not just location-based).

## Database Changes:
- Added `list_search_config` table to store JSON field mappings per list type
- Added `artefact_search` table to store extracted case data from publications
- Updated `subscription` table with `search_type` and `search_value` columns
- Created migration script to migrate existing location subscriptions
- Kept `location_id` column nullable for backwards compatibility

## New Module: list-search-config
- Created admin configuration page at `/system-admin/list-configuration/:listTypeId/search-config`
- Implemented repository and service layers with field name validation
- Added bilingual support (English/Welsh) for all content
- Includes form validation for field names (letters, numbers, underscores only)

## Publication Processing:
- Created `artefact-search-extractor` to extract case data from JSON publications
- Implemented `artefact-search-repository` for storing extracted data
- Added graceful error handling for missing configurations or JSON fields

## Subscription Updates:
- Updated subscription queries to use `search_type='LOCATION_ID'` and `search_value`
- Modified subscription creation to populate new fields
- Maintained backwards compatibility with existing location-based fulfilment

## Testing:
- Added unit tests for list-search-config service validation logic
- All builds passing without type errors
- Linter checks passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…factor-artefact-search-extraction-subscription

# Conflicts:
#	libs/publication/src/index.ts
Fixed type errors caused by making subscription.locationId nullable:
- Updated SubscriptionWithUser interface in notifications to include new fields
- Updated subscription queries to use searchType/searchValue
- Updated mapSubscriptionToDto to handle nullable locationId and location
- Added null filtering to subscription mapping functions
- Fixed artefact search extractor type annotations

All builds now passing (24/24 successful).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Completed the final migration to fully remove locationId from the subscription
system, consolidating to searchType/searchValue only.

## Database Changes:
- Dropped location_id column from subscription table
- Dropped location_id foreign key constraint
- Dropped unique_user_location constraint
- Dropped idx_subscription_location index
- Added unique_user_subscription constraint on (user_id, search_type, search_value)
- Made search_type and search_value NOT NULL

## Code Changes:

**Subscription Module:**
- Updated queries to use searchType/searchValue for duplicate detection
- Removed locationId from createSubscriptionRecord
- Removed location includes from subscription queries
- Updated service to fetch location data separately via getLocationById
- Updated replaceUserSubscriptions to extract locationId from searchValue

**Location Module:**
- Removed subscriptions relation from Location model
- Updated hasActiveSubscriptions to use searchType/searchValue

**Notifications Module:**
- Updated SubscriptionWithUser interface (removed nullable types)
- Already using searchType/searchValue queries

All builds passing (24/24 successful).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added list-search-config routes to the web application.

The page is now accessible at:
- GET/POST /list-search-config/:listTypeId

Requires SYSTEM_ADMIN role to access.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Renamed page files to use Express parameter syntax [listTypeId] instead
of :listTypeId to work with the simple-router file discovery.

The route is now accessible at:
- GET/POST /list-search-config/:listTypeId

Changes:
- Moved files to list-search-config/ subdirectory
- Renamed files to use [listTypeId] syntax for route params
- Updated import paths to reference service from parent directory
- Updated template name to include subdirectory path

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed template name from [listTypeId].njk to config.njk since
Nunjucks looks for literal filenames and can't resolve brackets.

The route file [listTypeId].ts still uses bracket syntax for Express
route parameters, but references the template as 'list-search-config/config'.
Added moduleRoot export to list-search-config config and registered it
in the web app's modulePaths array so Nunjucks can find the templates.

This fixes the 'template not found' error when accessing the page.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fixed template to use the correct base template name and block:
- Changed extends from 'base-templates.njk' to 'base-template.njk' (singular)
- Changed block from 'content' to 'page_content' to match the layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed POST success redirect from '/system-admin/list-configuration'
(which doesn't exist) to the same config page so users see their saved
configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added action="" to the form element to ensure proper form submission.
Without an explicit action, some browsers may not submit the form correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added type='submit' attribute to the govukButton to ensure it triggers
form submission. Without this, the button might default to type='button'
which doesn't submit forms.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The govukButton macro was causing the form to close prematurely, placing
the button outside the form tag. Replaced with a plain HTML button element
that has the same GOV.UK styling classes.

Also improved errorMessage conditionals to check both fieldErrors existence
and the specific field error.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request implements a multi-search-type subscription system, refactoring the location-based subscription model to support searchable subscriptions by case name, case reference, and location. It introduces list-search-config infrastructure for configurable field extraction from publication data, case search functionality, and new UI flows for subscription management and administration across database, library modules, pages, and tests.

Changes

Cohort / File(s) Summary
Database Migrations and Schema
apps/postgres/prisma/migrations/20260119150000_*, apps/postgres/prisma/migrations/20260119151000_*, apps/postgres/prisma/migrations/20260122150011_*, apps/postgres/prisma/migrations/20260123151728_*, apps/postgres/prisma/schema.prisma, libs/subscription/prisma/schema.prisma, libs/subscriptions/prisma/schema.prisma
Creates list_search_config and artefact_search tables; refactors subscription table to use searchType/searchValue instead of locationId; adds case_name and case_number fields; updates cascade delete policies on notification_audit_log; removes old subscriptions schema.
List Search Config Library
libs/list-search-config/*
New library providing configuration management for case field mapping (case number and case name extraction). Includes package.json, Prisma schema, config module, repository queries/service with validation, and comprehensive tests.
Subscription Module Refactoring
libs/subscription/*, libs/subscriptions/prisma/schema.prisma, libs/subscription/package.json
Renames subscriptions to subscription package; restructures subscription schema to support searchType/searchValue and case metadata; adds case search service (searchByCaseName, searchByCaseReference); extends repository with new query helpers for case-based and location-based subscriptions; refactors service layer to handle multiple subscription types.
Publication Artefact Search
libs/publication/src/artefact-search-extractor.*, libs/publication/src/repository/artefact-search-queries.*, libs/publication/src/repository/queries.*, libs/publication/src/index.ts
Adds extraction and storage of case data from publication JSON payloads; provides repository queries for creating, retrieving, and deleting artefact search records; exports public API for case search by name and number.
Admin Pages
libs/admin-pages/src/pages/manual-upload-summary/index.ts, libs/admin-pages/src/pages/non-strategic-upload-summary/index.ts, libs/system-admin-pages/src/pages/list-search-config/*, libs/system-admin-pages/src/pages/manage-list-types/*, libs/system-admin-pages/src/pages/system-admin-dashboard/*
Integrates artefact search extraction into upload handlers; adds new manage-list-types page and list-search-config configuration pages with English/Welsh translations and validation.
Verified Pages: Case Subscriptions
libs/verified-pages/src/pages/case-name-search/*, libs/verified-pages/src/pages/case-name-search-results/*, libs/verified-pages/src/pages/case-number-search/*, libs/verified-pages/src/pages/case-number-search-results/*, libs/verified-pages/src/pages/subscription-add/*
New pages for case subscription flows: case name/reference search forms, results selection, and subscription method selection page. Includes handlers, templates, and localisations (en/cy).
Verified Pages: Refactored Subscription Management
libs/verified-pages/src/pages/bulk-unsubscribe/*, libs/verified-pages/src/pages/pending-subscriptions/*, libs/verified-pages/src/pages/subscription-management/*, libs/verified-pages/src/pages/confirm-bulk-unsubscribe/*, libs/verified-pages/src/pages/delete-subscription/*, libs/verified-pages/src/pages/location-name-search/*, libs/verified-pages/src/pages/subscription-confirmed/*, libs/verified-pages/src/pages/unsubscribe-confirmation/*, libs/verified-pages/src/assets/css/verified-pages.scss
Updates existing pages to support both case and location subscriptions; replaces govukTabs with moj-sub-navigation for view selection; extends translations and templates to handle case data (name, reference); adds support for multi-view management and bulk operations on both subscription types.
Notification Integration
libs/notifications/prisma/schema.prisma, libs/notifications/src/govnotify/template-config.*, libs/notifications/src/notification/notification-service.ts, libs/notifications/src/notification/notification-queries.*, libs/notifications/src/notification/subscription-queries.*
Updates Prisma schema with cascade delete; extends template parameters to include case info; refactors notification service to derive case subscriptions from artefact search data, combine with location subscriptions, and send emails with appropriate case metadata; removes location-specific query file.
E2E Tests: New Suites
e2e-tests/tests/case-subscriptions.spec.ts, e2e-tests/tests/manage-list-types.spec.ts
New comprehensive test suites for case subscription flows (search, select, confirm) and manage-list-types admin feature, including accessibility checks and Welsh localisations.
E2E Tests: Updated Flows
e2e-tests/tests/bulk-unsubscribe.spec.ts, e2e-tests/tests/delete-court.spec.ts, e2e-tests/tests/email-subscriptions.spec.ts, e2e-tests/tests/system-admin-dashboard.spec.ts, e2e-tests/utils/notification-helpers.ts
Updates subscription creation and deletion logic to use searchType/searchValue; adds subscription method selection steps; updates dashboard tile count and manage-list-types navigation.
Documentation
docs/tickets/VIBE-300/*, docs/tickets/VIBE-316/*
Comprehensive specifications, plans, task checklists, and E2E test documentation for case subscriptions (VIBE-300) and search infrastructure refactoring (VIBE-316).
Configuration and Dependencies
libs/verified-pages/package.json, libs/subscription/tsconfig.json, tsconfig.json
Updates package dependencies from subscriptions to subscription; adds new module path aliases for subscription and list-search-config; adds TypeScript configuration for subscription module.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SearchPage as Case Search<br/>(UI)
    participant SearchService as Case Search<br/>Service
    participant Repository as Artefact Search<br/>Repository
    participant Database as Database
    participant PendingPage as Pending<br/>Subscriptions (UI)
    participant SubService as Subscription<br/>Service
    participant NotifyService as Notification<br/>Service

    User->>SearchPage: Enter case name/reference
    SearchPage->>SearchService: searchByCaseName(input)
    SearchService->>Repository: findByCaseName/findByCaseNumber
    Repository->>Database: Query artefact_search
    Database-->>Repository: Case results
    Repository-->>SearchService: CaseSearchResult[]
    SearchService-->>SearchPage: Return deduplicated results
    SearchPage-->>User: Display matching cases

    User->>SearchPage: Select case(s) to subscribe
    SearchPage->>PendingPage: POST selected cases (searchType: CASE_NAME)
    PendingPage->>User: Show pending subscriptions

    User->>PendingPage: Confirm subscriptions
    PendingPage->>SubService: createCaseSubscription(userId, searchType, caseNumber, caseName)
    SubService->>Database: INSERT into subscription (searchType, searchValue, caseNumber, caseName)
    Database-->>SubService: Subscription created
    SubService-->>PendingPage: Success

    PendingPage->>NotifyService: New case subscription created
    NotifyService->>Repository: findByCaseNumber for artefact data
    Repository->>Database: Query artefact_search
    Database-->>Repository: Artefact case data
    NotifyService->>NotifyService: Extract case info, build template
    NotifyService-->>User: Email notification sent

    PendingPage-->>User: Confirmation page with case details
Loading

Possibly related PRs

  • PR #130 – Modifies subscription data model and Prisma schema directly, with overlapping changes to subscription package structure and verified-pages route handlers
  • PR #185 – Updates location repository's subscription query logic (searchType/searchValue filtering), directly related to refactored subscription-location changes
  • PR #204 – Updates admin upload pages (manual and non-strategic) to invoke artefact extraction, paralleling the extractAndStoreArtefactSearch integration in this PR
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.58% 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 describes the main functionality added: enabling users to subscribe to hearing lists by case name and case reference number, which is the primary objective of the VIBE-300 ticket.

✏️ 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-300

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

Copy link
Copy Markdown
Contributor

🎭 Playwright E2E Test Results

255 tests   233 ✅  58m 51s ⏱️
 33 suites    0 💤
  1 files     22 ❌

For more details on these failures, see this check.

Results for commit 630d3ef.

♻️ This comment has been updated with latest results.

@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: 3

Note

Due to the large number of review comments, Critical 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 (5)
libs/system-admin-pages/src/pages/system-admin-dashboard/cy.ts (1)

24-28: Remove Welsh localization from admin dashboard.

The Welsh locale file is imported and used here, but per the established pattern, Welsh localization is not required for admin screens. The href inconsistency between /blob-explorer-locations (en.ts) and /blob-explorer (cy.ts) is a symptom of this—remove the cy.ts import and the conditional language loading logic entirely.

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

20-291: Add test coverage for case-based subscriptions in the notification service.

The main service implementation handles both case number and case name subscriptions (including deduplication), but the test file only covers location-based scenarios. Add tests for:

  • Sending notifications to case number subscribers
  • Sending notifications to case name subscribers
  • Deduplication when a user has both location and case subscriptions
libs/verified-pages/src/pages/bulk-unsubscribe/index.njk (1)

1-1: Use the required base layout name.

This template extends layouts/base-template.njk, but the required base is layouts/base-templates.njk.

Proposed fix
-{% extends "layouts/base-template.njk" %}
+{% extends "layouts/base-templates.njk" %}

As per coding guidelines.

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

101-117: Test expectation mismatches Prisma select in implementation.

findActiveSubscriptionsByCaseNumbers uses select (see libs/notifications/src/notification/subscription-queries.ts Lines 42–72), but the test expects include. This will fail. Update the expectation to match the actual query shape.

✅ Proposed fix
     expect(prisma.subscription.findMany).toHaveBeenCalledWith({
       where: {
         searchType: "CASE_NUMBER",
         searchValue: {
           in: ["CASE-123", "CASE-456"]
         }
       },
-      include: {
-        user: {
-          select: {
-            email: true,
-            firstName: true,
-            surname: true
-          }
-        }
-      }
+      select: {
+        subscriptionId: true,
+        userId: true,
+        searchType: true,
+        searchValue: true,
+        caseName: true,
+        caseNumber: true,
+        user: {
+          select: {
+            email: true,
+            firstName: true,
+            surname: true
+          }
+        }
+      }
     });
libs/subscription/src/repository/service.test.ts (1)

394-400: Stale test: case subscriptions are now implemented.

This test asserts "case subscriptions are not yet implemented" but lines 763-819 test the actual implementation. Update or remove this placeholder test.

Suggested fix

Remove the placeholder test block (lines 394-400) since the actual implementation tests exist at lines 763-819.

♻️ Duplicate comments (1)
apps/postgres/prisma/migrations/20260123151728_add_cascade_delete_to_notification_audit_log/migration.sql (1)

1-5: Same retention concern as the Prisma schema change.

See the earlier comment on cascading deletes and audit‑log retention.

🟠 Major comments (21)
libs/list-search-config/src/config.ts-1-8 (1)

1-8: Missing standard config exports for module registration.
config.ts should export pageRoutes, apiRoutes, prismaSchemas, and assets. Only prismaSchemas is present, which can break the standard module loader expectations.

Proposed minimal fix
 import path from "node:path";
 import { fileURLToPath } from "node:url";
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
 
-// Module configuration for app registration
-export const prismaSchemas = path.join(__dirname, "../prisma");
+// Module configuration for app registration
+export const pageRoutes = [];
+export const apiRoutes = [];
+export const assets = [];
+export const prismaSchemas = path.join(__dirname, "../prisma");

As per coding guidelines, module config files must export the standard interfaces.

libs/verified-pages/src/pages/case-number-search-results/index.njk-1-1 (1)

1-1: Use the required base layout filename.
Guidelines specify layouts/base-templates.njk (plural). Please align to prevent template resolution issues. As per coding guidelines, ...

🔧 Proposed fix
-{% extends "layouts/base-template.njk" %}
+{% extends "layouts/base-templates.njk" %}
libs/verified-pages/src/pages/case-name-search/index.njk-1-1 (1)

1-1: Use the required base layout filename.
Guidelines require layouts/base-templates.njk (plural). Please align to avoid template lookup failures. As per coding guidelines, ...

🔧 Proposed fix
-{% extends "layouts/base-template.njk" %}
+{% extends "layouts/base-templates.njk" %}
libs/verified-pages/src/pages/case-number-search-results/index.njk-2-32 (1)

2-32: Prefer GOV.UK table macro for compliance.
The table is rendered with raw HTML; guidelines require GOV.UK component macros. Consider switching to govukTable. As per coding guidelines, ...

♻️ Proposed refactor
-{% from "govuk/components/button/macro.njk" import govukButton %}
+{% from "govuk/components/button/macro.njk" import govukButton %}
+{% from "govuk/components/table/macro.njk" import govukTable %}

@@
-      <table class="govuk-table">
-        <thead class="govuk-table__head">
-          <tr class="govuk-table__row">
-            <th scope="col" class="govuk-table__header">{{ tableHeaderCaseName }}</th>
-            <th scope="col" class="govuk-table__header govuk-table__header--numeric">{{ tableHeaderCaseNumber }}</th>
-          </tr>
-        </thead>
-        <tbody class="govuk-table__body">
-          {% for result in results %}
-            <tr class="govuk-table__row">
-              <td class="govuk-table__cell">{{ result.caseName or notAvailable }}</td>
-              <td class="govuk-table__cell govuk-table__cell--numeric">{{ result.caseNumber or notAvailable }}</td>
-            </tr>
-          {% endfor %}
-        </tbody>
-      </table>
+      {% set rows = [] %}
+      {% for result in results %}
+        {% set rows = rows.concat([[{ text: result.caseName or notAvailable }, { text: result.caseNumber or notAvailable }]]) %}
+      {% endfor %}
+      {{ govukTable({
+        head: [
+          { text: tableHeaderCaseName },
+          { text: tableHeaderCaseNumber, classes: "govuk-table__header--numeric" }
+        ],
+        rows: rows
+      }) }}
e2e-tests/tests/case-subscriptions.spec.ts-15-31 (1)

15-31: Avoid hard-coded locationId: 1 to prevent brittle test data. This assumes seed data stability across environments and can break CI. Prefer deriving a location ID from configuration or an existing DB record.

🛠️ Example approach using an env var
 async function createTestCaseData(): Promise<TestCaseData> {
   const timestamp = Date.now();
   const randomId = Math.floor(Math.random() * 1000000);
+  const locationId = Number(process.env.TEST_LOCATION_ID);
+  if (!Number.isInteger(locationId)) {
+    throw new Error("TEST_LOCATION_ID must be set for e2e data seeding");
+  }
   const testCaseName = `Test Case ${timestamp}-${randomId}`;
   const testCaseNumber = `TC-${timestamp}-${randomId}`;
   const testArtefactId = `test-artefact-${timestamp}-${randomId}`;
@@
   await prisma.artefactSearch.create({
     data: {
       artefactId: testArtefactId,
       caseName: testCaseName,
       caseNumber: testCaseNumber,
-      locationId: 1 // Use first location from seed data
+      locationId
     }
   });
libs/api/src/blob-ingestion/repository/service.ts-85-86 (1)

85-86: Prevent case data appearing in logs during extraction.

Lines 85-86 invoke extractAndStoreArtefactSearch, which currently logs sample case numbers/names (see libs/publication/src/artefact-search-extractor.ts, Lines 151‑161). That is likely sensitive; please redact or remove those fields (or gate behind debug-only logging).

🔒 Suggested redaction (in libs/publication/src/artefact-search-extractor.ts)
-      console.log(`[ArtefactSearch] Extracted ${cases.length} cases for artefact ${artefactId}:`, {
-        caseSamples: cases.slice(0, 5).map((c) => ({ caseNumber: c.caseNumber, caseName: c.caseName }))
-      });
+      console.log(`[ArtefactSearch] Extracted ${cases.length} cases for artefact ${artefactId}`);

As per coding guidelines, avoid logging sensitive data.

libs/verified-pages/src/pages/pending-subscriptions/index.njk-7-7 (1)

7-7: Use govukTable macro instead of hand-built HTML tables.

The template imports the govukTable macro (line 2) but the subscription tables (lines 56–84, 86–112) are built with custom HTML. Per coding guidelines, Nunjucks templates must use GOV.UK Frontend component macros. Refactor both tables to use govukTable.

The concern about cases and locations being undefined is not applicable here—the handler always provides both as arrays before rendering (see lines 27–36, 67–74).

libs/notifications/prisma/schema.prisma-22-22 (1)

22-22: Use Restrict or SetNull for audit-log retention.

Line 22 cascades deletes from subscriptions to NotificationAuditLog, which permanently erases historical notification records. This contradicts audit-table design intent. With PostgreSQL's relationMode = "foreignKeys" (default), onDelete: Cascade enforces a real ON DELETE CASCADE constraint, deleting all audit logs when a subscription is deleted. Audit and history tables should resist or nullify such cascades to preserve compliance records. Consider onDelete: Restrict to prevent accidental erasure, or make subscriptionId nullable and use onDelete: SetNull.

libs/system-admin-pages/src/pages/list-search-config/index.njk-1-1 (1)

1-1: Use the required base layout name.

This template extends layouts/base-template.njk, but the required base is layouts/base-templates.njk. Please align to avoid layout resolution issues.

Proposed fix
-{% extends "layouts/base-template.njk" %}
+{% extends "layouts/base-templates.njk" %}

As per coding guidelines.

libs/verified-pages/src/pages/case-name-search-results/index.njk-1-1 (1)

1-1: Use the required base layout name.

This template extends layouts/base-template.njk, but the required base is layouts/base-templates.njk.

Proposed fix
-{% extends "layouts/base-template.njk" %}
+{% extends "layouts/base-templates.njk" %}

As per coding guidelines.

apps/postgres/prisma/migrations/20260119151000_refactor_subscription_and_add_artefact_search/migration.sql-11-14 (1)

11-14: Avoid adding NOT NULL columns without a backfill step.

Adding search_type and search_value as NOT NULL will fail if subscription already has rows. Consider adding them nullable, backfilling, then enforcing NOT NULL in a follow-up migration.

One possible migration pattern
-ALTER TABLE "subscription"
-ADD COLUMN "search_type" VARCHAR(50) NOT NULL,
-ADD COLUMN "search_value" TEXT NOT NULL,
-DROP COLUMN IF EXISTS "location_id";
+ALTER TABLE "subscription"
+ADD COLUMN "search_type" VARCHAR(50),
+ADD COLUMN "search_value" TEXT,
+DROP COLUMN IF EXISTS "location_id";
+
+-- backfill search_type/search_value here
+
+ALTER TABLE "subscription"
+ALTER COLUMN "search_type" SET NOT NULL,
+ALTER COLUMN "search_value" SET NOT NULL;
apps/postgres/scripts/migrate-subscriptions.ts-17-47 (1)

17-47: Handle missing locationId to avoid incomplete migration.

Subscriptions with searchType: null but no locationId are silently skipped, leaving searchType unset. That can break later constraints and your verification won’t flag it. Please count/log those records and verify remaining nulls.

🔧 Proposed fix
   for (const subscription of subscriptions) {
     try {
-      if (subscription.locationId) {
+      if (subscription.locationId != null) {
         await prisma.subscription.update({
           where: {
             subscriptionId: subscription.subscriptionId
           },
           data: {
             searchType: "LOCATION_ID",
             searchValue: subscription.locationId.toString()
           }
         });
         migratedCount++;
+      } else {
+        console.warn(`Skipping subscription ${subscription.subscriptionId}: missing locationId`);
+        errorCount++;
       }
     } catch (error) {
       console.error(`Failed to migrate subscription ${subscription.subscriptionId}:`, error);
       errorCount++;
     }
   }
@@
   const verifyCount = await prisma.subscription.count({
     where: {
       searchType: "LOCATION_ID"
     }
   });
 
   console.log(`Verification: ${verifyCount} subscriptions now have searchType='LOCATION_ID'`);
+  const remainingNull = await prisma.subscription.count({ where: { searchType: null } });
+  if (remainingNull > 0) {
+    console.warn(`Verification: ${remainingNull} subscriptions still missing searchType`);
+  }
 }
libs/verified-pages/src/pages/case-name-search/index.ts-59-96 (1)

59-96: Avoid logging raw case names (potential sensitive data).
caseName is user‑provided and may be sensitive; please remove or mask it in logs.

✅ Suggested change
-    console.log(`[case-name-search] Searching for case name: "${caseName}"`);
+    console.log("[case-name-search] Searching for case name");
     const results = await searchByCaseName(caseName);
     console.log(`[case-name-search] Found ${results.length} results`);
@@
-    console.error(`[case-name-search] Error searching for case name "${caseName}":`, error);
+    console.error("[case-name-search] Error searching for case name:", error);

As per coding guidelines, avoid logging sensitive data.

libs/verified-pages/src/pages/subscription-management/index.ts-16-17 (1)

16-17: Validate view query parameter before use.
req.query.view is user‑controlled; please whitelist allowed values and default safely.

✅ Suggested change
-  const view = (req.query.view as string) || "all";
+  const viewParam = typeof req.query.view === "string" ? req.query.view : undefined;
+  const allowedViews = new Set(["all", "courts", "cases"]); // adjust to UI values
+  const view = viewParam && allowedViews.has(viewParam) ? viewParam : "all";

As per coding guidelines, all endpoints must validate inputs.

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

68-77: User identifiers logged.

Logging userId and searchValue (which may contain case-identifying information) could be problematic per the guideline to not include sensitive data in logs.

Suggested improvement
   console.log("[notification-service] Case subscriptions found:", {
     totalCaseNumberSubscriptions: caseNumberSubscriptions.length,
     totalCaseNameSubscriptions: caseNameSubscriptions.length,
-    totalCaseSubscriptions: caseSubscriptions.length,
-    caseSubscriptionSamples: caseSubscriptions.slice(0, 3).map((cs) => ({
-      userId: cs.userId,
-      searchValue: cs.searchValue,
-      caseName: cs.caseName
-    }))
+    totalCaseSubscriptions: caseSubscriptions.length
   });
libs/verified-pages/src/pages/pending-subscriptions/index.ts-12-12 (1)

12-12: Full session data logged - potential PII exposure.

Logging the entire emailSubscriptions session object may expose sensitive user data. Consider removing or limiting to non-sensitive fields.

Suggested fix
-  console.log("[pending-subscriptions] Session data:", JSON.stringify(req.session.emailSubscriptions, null, 2));
+  console.log("[pending-subscriptions] Session data: pendingSubscriptions=%d, pendingCaseSubscriptions=%d",
+    req.session.emailSubscriptions?.pendingSubscriptions?.length || 0,
+    req.session.emailSubscriptions?.pendingCaseSubscriptions?.length || 0
+  );
libs/notifications/src/notification/notification-service.ts-45-51 (1)

45-51: Potential PII in logs.

Case names and numbers could identify individuals involved in legal proceedings. Per coding guidelines, sensitive data should not be included in logs. Consider logging only counts or anonymised identifiers.

Suggested improvement
   console.log("[notification-service] Artefact cases found:", {
     publicationId: event.publicationId,
-    totalCases: artefactCases.length,
-    caseNumbers,
-    caseNames,
-    caseSamples: artefactCases.slice(0, 3).map((ac) => ({ caseNumber: ac.caseNumber, caseName: ac.caseName }))
+    totalCases: artefactCases.length
   });
libs/subscription/src/repository/queries.ts-113-122 (1)

113-122: Duplicate function: findByUserId is identical to findSubscriptionsByUserId.

findByUserId (lines 113-122) has the same implementation as findSubscriptionsByUserId (lines 3-12). Consider removing the duplicate and using a single function.

e2e-tests/tests/manage-list-types.spec.ts-22-90 (1)

22-90: Single journey should include validation, Welsh, accessibility, and keyboard checks.
These are currently split across separate tests; please add a consolidated journey test (or extend the full-flow test) to cover all four in one run. Based on learnings, ...

libs/publication/src/artefact-search-extractor.ts-148-161 (1)

148-161: Avoid logging case identifiers and names.

Case numbers and names are likely sensitive; logging samples breaches the “no sensitive data in logs” guideline. Log counts only or redact.

Suggested fix
-      console.log(`[ArtefactSearch] Extracted ${cases.length} cases for artefact ${artefactId}:`, {
-        caseSamples: cases.slice(0, 5).map((c) => ({ caseNumber: c.caseNumber, caseName: c.caseName }))
-      });
+      console.log(`[ArtefactSearch] Extracted ${cases.length} cases for artefact ${artefactId}`);
libs/subscription/src/repository/service.ts-238-240 (1)

238-240: getSubscriptionsByCase omits CASE_NAME subscriptions.

Line 238 only fetches CASE_NUMBER, so case‑name subscriptions are excluded. Either include CASE_NAME or rename the method.

Suggested fix
 export async function getSubscriptionsByCase(userId: string) {
-  return findByUserIdAndType(userId, "CASE_NUMBER");
+  const [caseNumberSubs, caseNameSubs] = await Promise.all([
+    findByUserIdAndType(userId, "CASE_NUMBER"),
+    findByUserIdAndType(userId, "CASE_NAME")
+  ]);
+  return [...caseNumberSubs, ...caseNameSubs];
 }
🟡 Minor comments (21)
libs/verified-pages/src/pages/subscription-management/index.njk-79-79 (1)

79-79: Remove aria-sort="none" if sorting is not implemented.

The aria-sort="none" attribute on the Location header suggests sortable columns, but no sorting functionality is present. This may confuse screen reader users expecting interactive sorting.

🛠️ Proposed fix
-              <th scope="col" class="govuk-table__header" style="width: 60%;" aria-sort="none">{{ tableHeaderLocation }}</th>
+              <th scope="col" class="govuk-table__header" style="width: 60%;">{{ tableHeaderLocation }}</th>

Also applies to: 145-145

docs/tickets/VIBE-300/e2e-tests-summary.md-10-12 (1)

10-12: Use a relative path for portability.

The absolute path contains a local machine path (/Users/kian.kwa/...) which won't be valid for other contributors.

Suggested fix
 ## Test File Location
 
-`/Users/kian.kwa/IdeaProjects/cath-service/e2e-tests/tests/case-subscriptions.spec.ts`
+`e2e-tests/tests/case-subscriptions.spec.ts`
libs/verified-pages/src/pages/case-number-search/index.test.ts-59-71 (1)

59-71: Welsh translation test values don't match cy.ts.

Line 68 expects cy.title to be "Yn ôl rhif cyfeirnod yr achos, ID yr achos neu rif cyfeirnod unigryw (URN)", but the provided cy.ts defines it as "Beth yw'r rhif cyfeirnod?".

libs/verified-pages/src/pages/case-number-search/index.test.ts-43-56 (1)

43-56: Test assertions don't match actual translation values.

The test at line 47 expects en.referenceNumberHint, which is not defined in en.ts. The test at line 55 expects the title to be "By case reference number, case ID or unique reference number (URN)", but en.ts defines it as "What is the reference number?". Update either the translation file or the test assertions to align them.

libs/verified-pages/src/pages/case-number-search/index.test.ts-110-130 (1)

110-130: Use errorRequired instead of errorNoResults for empty field validation.

The implementation (lines 26–46 in index.ts) and this test both use errorNoResults when the reference number is empty. This is semantically incorrect. An empty required field should use errorRequired ("Enter a reference number"), reserving errorNoResults for when a search completes but returns zero results (lines 48–66). Update the handler to use t.errorRequired and t.errorNoResultsField for empty input validation.

libs/list-search-config/src/config.test.ts-27-31 (1)

27-31: Cross-platform path handling issue.

Using import.meta.url.replace("file://", "") doesn't handle Windows paths correctly. On Windows, file:///C:/path becomes /C:/path which is invalid.

Suggested fix using fileURLToPath
 import path from "node:path";
+import { fileURLToPath } from "node:url";
 import { describe, expect, it } from "vitest";
 import * as config from "./config.js";
     it("should exist as a resolvable path", () => {
       // The path should be constructible from __dirname and ../prisma
-      const expectedPath = path.join(path.dirname(import.meta.url.replace("file://", "")), "../prisma");
+      const expectedPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../prisma");
       expect(path.normalize(config.prismaSchemas)).toBe(path.normalize(expectedPath));
     });
libs/verified-pages/src/pages/subscription-add/index.njk-24-41 (1)

24-41: Add fieldset with legend for accessibility compliance.

GOV.UK radios should include a fieldset with legend to properly associate the question with the radio group for screen readers. The radioLabel translation key exists but isn't being used.

Proposed fix
       {{ govukRadios({
         name: "subscriptionMethod",
+        fieldset: {
+          legend: {
+            text: radioLabel,
+            classes: "govuk-fieldset__legend--m"
+          }
+        },
         errorMessage: fieldErrors.subscriptionMethod if fieldErrors and fieldErrors.subscriptionMethod,
         items: [
libs/verified-pages/src/pages/bulk-unsubscribe/index.njk-80-83 (1)

80-83: Provide a fallback for empty aria-labels.

If both caseName and caseNumber are missing, the label becomes empty. Please include notAvailable as a final fallback.

Proposed fix
-aria-label="Select {{ subscription.caseName or subscription.caseNumber }}"
+aria-label="Select {{ subscription.caseName or subscription.caseNumber or notAvailable }}"
...
-<span class="govuk-visually-hidden">Select {{ subscription.caseName or subscription.caseNumber }}</span>
+<span class="govuk-visually-hidden">Select {{ subscription.caseName or subscription.caseNumber or notAvailable }}</span>

Also applies to: 178-182

docs/tickets/VIBE-316/tasks.md-114-116 (1)

114-116: Minor grammar tweak for readability.

Consider inserting a comma to separate clauses:

Suggested edit
-- [ ] Test publication is processed and case data extracted
+- [ ] Test publication is processed, and case data extracted
libs/list-search-config/src/repository/service.test.ts-118-130 (1)

118-130: Test does not verify trimming behaviour as claimed.

The test description states "should trim whitespace from valid field names before saving", but the input values "caseNumber" and "caseName" contain no whitespace. However, the suggested fix to pass " caseNumber " would not work with the current implementation.

The issue is a design mismatch: validateFieldName() is called before trimming (line 44 of service.ts), and the pattern /^[a-zA-Z0-9_]+$/ rejects strings with leading/trailing spaces. Strings with spaces would fail validation before trimming ever occurs (lines 58-60).

Either rename the test to describe what it actually tests (e.g., "should save valid field names"), or refactor the implementation to trim before validation if leading/trailing spaces should be handled gracefully.

docs/tickets/VIBE-316/plan.md-175-178 (1)

175-178: Update the migration script path to match the repo.

The plan references libs/postgres/scripts/migrate-subscriptions.js, but the script lives under apps/postgres/scripts. This will mislead anyone following the runbook.

📝 Proposed doc fix
-   node libs/postgres/scripts/migrate-subscriptions.js
+   node apps/postgres/scripts/migrate-subscriptions.js
libs/subscription/src/case-search-service.ts-21-21 (1)

21-21: Edge case: records with both caseNumber and caseName as null will collide.

When both fields are null, the key becomes "|". Multiple distinct records (different id/artefactId) would incorrectly deduplicate to one. Consider including artefactId in the key or handling this scenario explicitly if it's a valid data state.

libs/verified-pages/src/pages/case-number-search/index.ts-27-46 (1)

27-46: Incorrect error message for empty input.

Lines 35 and 38 use t.errorNoResults ("There is nothing matching your criteria") for empty input validation, but t.errorRequired ("Enter a reference number") is the appropriate message. The locale files define both messages for distinct purposes.

Suggested fix
     return res.render("case-number-search/index", {
       ...t,
-      errors: [{ text: t.errorNoResults, href: "#referenceNumber" }],
+      errors: [{ text: t.errorRequired, href: "#referenceNumber" }],
       errorSummary: {
         titleText: t.errorSummaryTitle,
-        errorList: [{ text: t.errorNoResults, href: "#referenceNumber" }]
+        errorList: [{ text: t.errorRequired, href: "#referenceNumber" }]
       },
       fieldErrors: {
-        referenceNumber: { text: t.errorNoResultsField }
+        referenceNumber: { text: t.errorRequired }
       },
libs/verified-pages/src/pages/pending-subscriptions/index.ts-159-167 (1)

159-167: Potential null searchValue passed to createCaseSubscription.

If searchType is "CASE_NAME" but caseItem.caseName is null, searchValue will be null. The service may not handle this gracefully. Consider adding validation.

Suggested fix
         pendingCases.map((caseItem: any) => {
           const searchType = caseItem.searchType || "CASE_NUMBER";
           const searchValue = searchType === "CASE_NAME" ? caseItem.caseName : caseItem.caseNumber;
+          if (!searchValue) {
+            console.warn("[pending-subscriptions] Skipping case with null searchValue:", caseItem.id);
+            return Promise.resolve();
+          }
           return createCaseSubscription(userId, searchType, searchValue, caseItem.caseNumber, caseItem.caseName);
         })
libs/verified-pages/src/pages/case-number-search/index.ts-87-96 (1)

87-96: Hardcoded English error message breaks Welsh support.

The error messages at lines 89 and 92 are hardcoded in English. Add translations to the locale files and use them here for proper bilingual support.

Suggested approach

Add to en.ts and cy.ts:

// en.ts
errorGeneric: "An error occurred while searching"

// cy.ts  
errorGeneric: "Bu gwall wrth chwilio"

Then use:

-      errors: [{ text: "An error occurred while searching", href: "#referenceNumber" }],
+      errors: [{ text: t.errorGeneric, href: "#referenceNumber" }],
libs/publication/src/repository/queries.test.ts-1202-1241 (1)

1202-1241: Missing beforeEach block for mock cleanup.

All other test suites in this file include beforeEach(() => { vi.clearAllMocks(); }). Adding it here would ensure consistency and prevent potential test pollution.

Suggested fix
 describe("deleteArtefactSearchByArtefactId", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
   it("should delete all artefact search entries for given artefact ID", async () => {
e2e-tests/tests/manage-list-types.spec.ts-93-109 (1)

93-109: Test name says alphabetical order, but the order isn’t asserted.
Either add an order check or rename the test to match the assertions.

docs/tickets/VIBE-300/plan.md-97-101 (1)

97-101: Wording nit: avoid colon after “with”.

Small grammar tweak in the bullet list.

Suggested fix
-   - Return array of matching cases with: case_number, case_name, party_name (if available)
+   - Return array of matching cases with case_number, case_name, party_name (if available)
docs/tickets/VIBE-300/plan.md-280-283 (1)

280-283: Use “log in” as a verb.

Minor grammar tweak in the access‑control section.

Suggested fix
-   - Redirect to login if not authenticated
+   - Redirect to log in if not authenticated
libs/publication/src/artefact-search-extractor.ts-84-99 (1)

84-99: Guard against non‑object payloads before in checks.

extractFromRootLevel will throw if jsonPayload is a primitive. Add a type guard before using in.

Suggested fix
-  if (Array.isArray(jsonPayload)) {
+  if (!jsonPayload || typeof jsonPayload !== "object" || Array.isArray(jsonPayload)) {
     return null;
   }
docs/tickets/VIBE-300/plan.md-398-400 (1)

398-400: Hyphenate “multi‑page”.

Minor spelling/style fix.

Suggested fix
-This ticket involves creating a new multi-page user journey, case search functionality, subscription management UI, and integration with existing fulfilment logic. The complexity is high due to the number of pages, search logic, and dependency on VIBE-316.
+This ticket involves creating a new multi‑page user journey, case search functionality, subscription management UI, and integration with existing fulfilment logic. The complexity is high due to the number of pages, search logic, and dependency on VIBE‑316.
🧹 Nitpick comments (23)
libs/verified-pages/src/pages/subscription-management/index.njk (2)

45-72: Consider extracting case table to a reusable macro.

The case table markup is duplicated between the 'all' view (lines 45-72) and 'case' view (lines 107-134). Similarly, the location table is duplicated (lines 76-101 and 142-167).

Extract these into Nunjucks macros to reduce duplication and ease future maintenance.

♻️ Suggested approach

Create macros at the top of the file or in a shared partial:

{% macro caseTable(subscriptions, tableHeaderCaseName, tableHeaderCaseNumber, tableHeaderDate, tableHeaderActions, notAvailable, removeLink, csrfToken) %}
  <table class="govuk-table" id="cases-table">
    <thead class="govuk-table__head">
      <tr class="govuk-table__row">
        <th scope="col" class="govuk-table__header" style="width: 30%;">{{ tableHeaderCaseName }}</th>
        <th scope="col" class="govuk-table__header" style="width: 30%;">{{ tableHeaderCaseNumber }}</th>
        <th scope="col" class="govuk-table__header" style="width: 30%;">{{ tableHeaderDate }}</th>
        <th scope="col" class="govuk-table__header govuk-table__header--numeric" style="width: 10%; text-align: right;">{{ tableHeaderActions }}</th>
      </tr>
    </thead>
    <tbody class="govuk-table__body">
      {% for subscription in subscriptions %}
        {# ... row content ... #}
      {% endfor %}
    </tbody>
  </table>
{% endmacro %}

Then call {{ caseTable(caseSubscriptions, ...) }} in both views.

Also applies to: 107-134


48-51: Consider moving inline styles to CSS classes.

Multiple inline styles for widths, alignment, and vertical positioning are repeated throughout. Extracting these to CSS classes would improve maintainability and allow easier theming.

Also applies to: 57-60

libs/verified-pages/src/pages/case-number-search/index.test.ts (1)

36-39: Unusual console.error reassignment placement.

Reassigning console.error in afterEach means it only takes effect after the first test runs. If the intent is to suppress error logs during tests, move this to beforeEach or use vi.spyOn.

Suggested fix
   beforeEach(() => {
+    vi.spyOn(console, "error").mockImplementation(() => {});
     mockReq = {
       body: {},
       session: {} as any,
       path: "/case-number-search",
       csrfToken: vi.fn(() => "mock-csrf-token")
     };
     // ...
   });

   afterEach(() => {
     vi.clearAllMocks();
-    console.error = vi.fn();
   });
e2e-tests/tests/system-admin-dashboard.spec.ts (1)

24-27: Centralise the expected tile count to avoid repeated magic numbers. This reduces update churn when tiles change again.

♻️ Suggested refactor
 import AxeBuilder from "@axe-core/playwright";
 import { expect, test } from "@playwright/test";
 import { loginWithSSO } from "../utils/sso-helpers.js";
 
+const EXPECTED_TILE_COUNT = 9;
+
 test.describe("System Admin Dashboard", () => {
   test.beforeEach(async ({ page }) => {
@@
     test("should display all 9 admin tiles", async ({ page }) => {
       const tiles = page.locator(".admin-tile");
-      await expect(tiles).toHaveCount(9);
+      await expect(tiles).toHaveCount(EXPECTED_TILE_COUNT);
     });
@@
     test("should display descriptions for all tiles", async ({ page }) => {
       const descriptions = page.locator(".admin-tile__description");
-      await expect(descriptions).toHaveCount(9);
+      await expect(descriptions).toHaveCount(EXPECTED_TILE_COUNT);
@@
     test("should display tiles in 2-column grid", async ({ page }) => {
       const gridColumns = page.locator(".govuk-grid-column-one-half");
-      await expect(gridColumns).toHaveCount(9);
+      await expect(gridColumns).toHaveCount(EXPECTED_TILE_COUNT);
     });
@@
     test("should have accessible links `@nightly`", async ({ page }) => {
       const links = page.locator("a.admin-tile");
-      await expect(links).toHaveCount(9);
+      await expect(links).toHaveCount(EXPECTED_TILE_COUNT);
 
-      for (let i = 0; i < 9; i++) {
+      for (let i = 0; i < EXPECTED_TILE_COUNT; i++) {
         await expect(links.nth(i)).toBeVisible();
       }
     });
@@
       // Verify all 9 tiles are accessible links
-      await expect(tileLinks).toHaveCount(9);
+      await expect(tileLinks).toHaveCount(EXPECTED_TILE_COUNT);
     });
@@
       const tiles = page.locator(".admin-tile");
-      await expect(tiles).toHaveCount(9);
+      await expect(tiles).toHaveCount(EXPECTED_TILE_COUNT);
     });
@@
       const tiles = page.locator(".admin-tile");
-      await expect(tiles).toHaveCount(9);
+      await expect(tiles).toHaveCount(EXPECTED_TILE_COUNT);
     });
@@
       const tiles = page.locator(".admin-tile");
-      await expect(tiles).toHaveCount(9);
+      await expect(tiles).toHaveCount(EXPECTED_TILE_COUNT);
     });

Also applies to: 49-52, 64-67, 82-87, 131-133, 145-168

apps/postgres/prisma/migrations/20260122150011_add_case_name_and_case_number_to_subscription/migration.sql (1)

1-6: **Consider indexing strategy for case_name.**The index on case_number is appropriate for exact lookups. If case_name will be queried using LIKE '%...%' patterns, a standard btree index won't help—it will still perform a full table scan and ignore the index.

The pg_trgm module supports GIN indexes that enable fast searching for similar strings and LIKE/ILIKE queries. If case name searches use wildcard patterns, consider adding a trigram index in a future migration.

libs/system-admin-pages/src/pages/manage-list-types/index.njk (1)

13-15: Consider adding screen reader context for the action column.

The empty <th> for the configure link column may be unclear for screen reader users. Adding visually hidden text improves accessibility.

Suggested improvement
           <th scope="col" class="govuk-table__header">{{ nameColumnHeading }}</th>
-          <th scope="col" class="govuk-table__header"></th>
+          <th scope="col" class="govuk-table__header"><span class="govuk-visually-hidden">{{ actionColumnHeading | default("Actions") }}</span></th>
libs/verified-pages/src/pages/pending-subscriptions/index.njk (1)

56-112: Use the imported govukTable macro for both tables.

The govukTable macro (already imported at line 2) can render the tables with form actions by passing HTML content in the html property of cell objects. This aligns with the coding guideline to use GOV.UK Frontend component macros.

libs/publication/src/repository/artefact-search-queries.test.ts (1)

49-73: Consider adding an empty results test for findByCaseName.

For consistency with findByCaseNumber (line 106-112), adding a test case for when no results are found would improve test symmetry.

♻️ Suggested test case
+    it("should return empty array when no match found", async () => {
+      vi.mocked(prisma.artefactSearch.findMany).mockResolvedValue([]);
+
+      const result = await findByCaseName("NONEXISTENT");
+
+      expect(result).toEqual([]);
+    });
docs/tickets/VIBE-316/specification.md (1)

46-50: Consider using a heading instead of bold text.

Line 48 uses **Page: List Search Configuration** which triggers MD036 (emphasis used instead of heading). Using ### Page: List Search Configuration would improve document structure and accessibility.

📝 Suggested fix
 ### Admin Configuration Page
 
-**Page: List Search Configuration**
+### Page: List Search Configuration
libs/system-admin-pages/src/pages/manage-list-types/index.test.ts (1)

86-94: Hardcoded list type count may be fragile.

The assertion expect(listTypes.length).toBe(9) couples this test to the current mock data size. If mockListTypes changes, this test will fail without a clear reason.

♻️ Consider importing mock data for dynamic count
+import { mockListTypes } from "@hmcts/publication";
+
 // ...
 
     it("should map all mock list types", async () => {
       await getHandler(req as Request, res as Response);
 
       const renderCall = (res.render as any).mock.calls[0];
       const listTypes = renderCall[1].listTypes;
 
-      // We expect 9 list types from mock-list-types.ts
-      expect(listTypes.length).toBe(9);
+      expect(listTypes.length).toBe(mockListTypes.length);
     });
libs/verified-pages/src/pages/subscription-add/index.ts (1)

17-17: Consider typing the CSRF token access.

The (req as any).csrfToken?.() pattern is repeated. If CSRF middleware is commonly used, consider extending the Express Request type to include csrfToken to avoid the any cast.

libs/subscription/src/case-search-service.ts (1)

20-30: Consider extracting the deduplication logic.

The deduplication pattern is identical in both searchByCaseName and searchByCaseReference. A shared helper would reduce duplication.

♻️ Suggested helper
function deduplicateResults(results: Array<{ id: string; caseNumber: string | null; caseName: string | null; artefactId: string }>): CaseSearchResult[] {
  const seen = new Map<string, CaseSearchResult>();
  for (const result of results) {
    const key = `${result.caseNumber || ""}|${result.caseName || ""}`;
    if (!seen.has(key)) {
      seen.set(key, {
        id: result.id,
        caseNumber: result.caseNumber,
        caseName: result.caseName,
        artefactId: result.artefactId
      });
    }
  }
  return Array.from(seen.values());
}
libs/list-search-config/src/repository/queries.test.ts (1)

151-155: Test name is slightly misleading.

"should update only specified fields" suggests partial updates, but the test updates both fields. Consider renaming to "should update with different field values" or similar.

libs/system-admin-pages/src/pages/list-search-config/[listTypeId].ts (2)

15-15: Consider adding error handling for the service call.

If service.getConfigForListType throws (e.g., database error), the request will fail with an unhandled exception. Consider wrapping in try/catch to render an appropriate error page.

Suggested improvement
+  try {
     const existingConfig = await service.getConfigForListType(listTypeId);
+  } catch (error) {
+    console.error("Error fetching list search config:", error);
+    return res.status(500).send("Error loading configuration");
+  }

51-55: Brittle field name mapping.

The ternary at line 53 assumes error.field is always "Case number field name" for case number errors. If the service returns different field names, mapping will default to caseNameFieldName incorrectly.

Consider using a lookup map for clarity and robustness:

Suggested improvement
     const fieldErrors: Record<string, { text: string }> = {};
+    const fieldIdMap: Record<string, string> = {
+      "Case number field name": "caseNumberFieldName",
+      "Case name field name": "caseNameFieldName"
+    };
     for (const error of result.errors) {
-      const fieldId = error.field === "Case number field name" ? "caseNumberFieldName" : "caseNameFieldName";
+      const fieldId = fieldIdMap[error.field] || error.field;
       fieldErrors[fieldId] = { text: error.message };
     }
libs/verified-pages/src/pages/case-number-search-results/index.ts (1)

40-47: Excessive use of any type casts.

Multiple any casts reduce type safety. Consider defining proper types for the session data structures to catch errors at compile time.

Suggested approach
interface CaseSearchResult {
  id: string;
  caseNumber: string | null;
  caseName: string | null;
  artefactId: string;
  searchType?: string;
}

// Then use:
const casesWithSearchType: CaseSearchResult[] = searchResults.map((c: CaseSearchResult) => ({
  ...c,
  searchType: "CASE_NUMBER"
}));

This would require extending the session type declarations accordingly.

libs/verified-pages/src/pages/pending-subscriptions/index.ts (1)

53-57: Consider typing sort comparators.

Using any in sort comparators loses type safety. Define interfaces for the location and case objects for better maintainability.

libs/verified-pages/src/pages/bulk-unsubscribe/index.ts (1)

26-26: Consider validating currentView input.

currentView is read directly from query/body without validation. If unexpected values could cause template issues, consider constraining to expected values ("all", "cases", "courts").

Suggested approach
-  const currentView = (req.query.view as string) || "all";
+  const allowedViews = ["all", "cases", "courts"];
+  const currentView = allowedViews.includes(req.query.view as string) ? (req.query.view as string) : "all";
libs/publication/src/repository/queries.test.ts (1)

1042-1134: Consider adding an error handling test.

Other test suites (e.g., createArtefact) include database error scenarios. Adding a similar test here would ensure consistent coverage.

it("should handle database errors", async () => {
  vi.mocked(prisma.artefactSearch.create).mockRejectedValue(new Error("Database connection error"));

  await expect(createArtefactSearch("artefact-123", "CASE-456", "Smith vs Jones")).rejects.toThrow("Database connection error");
});
libs/notifications/src/notification/subscription-queries.ts (1)

75-105: Consider extracting the shared select projection.

The same select object is repeated across all three functions. Extracting it to a constant would reduce duplication.

Optional refactor
const SUBSCRIPTION_SELECT = {
  subscriptionId: true,
  userId: true,
  searchType: true,
  searchValue: true,
  caseName: true,
  caseNumber: true,
  user: {
    select: {
      email: true,
      firstName: true,
      surname: true
    }
  }
} as const;
libs/publication/src/artefact-search-extractor.test.ts (1)

38-41: Avoid any in mocks to keep typings honest.
Consider typed helpers or explicit mock types instead of as any for config and repository responses. As per coding guidelines, ...

e2e-tests/tests/manage-list-types.spec.ts (1)

24-45: Prefer role/label/text selectors over CSS locators.
Switching to getByRole/getByLabel/getByText improves resilience and aligns with test guidelines. Based on learnings, ...

libs/verified-pages/src/pages/case-name-search-results/index.ts (1)

18-79: Type the search result/session shapes to avoid any.
Introduce an interface for case search results and pending subscriptions so the sort/filter/map logic is type-safe. As per coding guidelines, ...

Comment thread apps/postgres/src/schema-discovery.test.ts Outdated
Comment thread libs/subscription/src/repository/service.test.ts
Comment on lines +1 to +11
export const en = {
title: "What is the reference number?",
heading: "What is the reference number?",
insetText: "Please enter either a case reference number, case ID or unique reference number (URN). You must enter an exact match.",
referenceNumberLabel: "Reference number",
continueButton: "Continue",
errorRequired: "Enter a reference number",
errorNoResults: "There is nothing matching your criteria",
errorNoResultsField: "Enter a valid case reference number",
errorSummaryTitle: "There is a problem"
};

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:

#!/bin/bash
# Check test file for referenceNumberHint reference
echo "=== Searching for test file ==="
fd "case-number-search" libs/verified-pages/src/pages/ -type d

echo -e "\n=== Check for test files in the directory ==="
ls -la libs/verified-pages/src/pages/case-number-search/

echo -e "\n=== Search for referenceNumberHint in the entire case-number-search directory ==="
rg -n "referenceNumberHint" libs/verified-pages/src/pages/case-number-search/

echo -e "\n=== Check cy.ts for the same key ==="
cat -n libs/verified-pages/src/pages/case-number-search/cy.ts 2>/dev/null || echo "cy.ts not found"

echo -e "\n=== Check test file if it exists ==="
cat -n libs/verified-pages/src/pages/case-number-search/index.test.ts 2>/dev/null | head -60

Repository: hmcts/cath-service

Length of output: 3878


🏁 Script executed:

#!/bin/bash
# Check more of the test file
echo "=== Full translations test section ==="
sed -n '41,80p' libs/verified-pages/src/pages/case-number-search/index.test.ts

echo -e "\n=== Check the template for referenceNumberHint usage ==="
cat -n libs/verified-pages/src/pages/case-number-search/index.njk

echo -e "\n=== Check the controller for hint usage ==="
cat -n libs/verified-pages/src/pages/case-number-search/index.ts

Repository: hmcts/cath-service

Length of output: 7273


Fix missing translation keys and correct title values to match test expectations.

The test file expects en.referenceNumberHint (line 47) but the localisation object is missing this key. Additionally, the title values are incorrect:

  • en.title should be "By case reference number, case ID or unique reference number (URN)" not "What is the reference number?"
  • cy.title should be "Yn ôl rhif cyfeirnod yr achos, ID yr achos neu rif cyfeirnod unigryw (URN)" not "Beth yw'r rhif cyfeirnod?"

The test also verifies that Welsh keys match English keys, which will fail while referenceNumberHint is missing from both files.

Required changes to en.ts
 export const en = {
-  title: "What is the reference number?",
+  title: "By case reference number, case ID or unique reference number (URN)",
   heading: "What is the reference number?",
   insetText: "Please enter either a case reference number, case ID or unique reference number (URN). You must enter an exact match.",
   referenceNumberLabel: "Reference number",
+  referenceNumberHint: "",
   continueButton: "Continue",
   errorRequired: "Enter a reference number",
   errorNoResults: "There is nothing matching your criteria",
   errorNoResultsField: "Enter a valid case reference number",
   errorSummaryTitle: "There is a problem"
 };
Required changes to cy.ts
 export const cy = {
-  title: "Beth yw'r rhif cyfeirnod?",
+  title: "Yn ôl rhif cyfeirnod yr achos, ID yr achos neu rif cyfeirnod unigryw (URN)",
   heading: "Beth yw'r rhif cyfeirnod?",
   insetText: "Rhowch naill ai rif cyfeirnod yr achos, ID yr achos neu rif cyfeirnod unigryw (URN). Rhaid i chi nodi union gyfatebiaeth.",
   referenceNumberLabel: "Rhif cyfeirnod",
+  referenceNumberHint: "",
   continueButton: "Parhau",
   errorRequired: "Rhowch rif cyfeirnod",
   errorNoResults: "Nid oes unrhyw beth yn cyfateb i'ch meini prawf",
   errorNoResultsField: "Rhowch rif cyfeirnod achos dilys",
   errorSummaryTitle: "Mae problem wedi codi"
 };

@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

🧹 Nitpick comments (1)
libs/verified-pages/src/pages/pending-subscriptions/index.test.ts (1)

80-84: Stabilise translation key parity checks by sorting.
Object.keys order can shift if properties are rearranged, making this test brittle. Sorting avoids order sensitivity.

Proposed change
-        const enKeys = Object.keys(en);
-        const cyKeys = Object.keys(cy);
+        const enKeys = Object.keys(en).sort();
+        const cyKeys = Object.keys(cy).sort();

Comment on lines +214 to +219
it("should sort locations alphabetically by name", async () => {
vi.mocked(locationService.getLocationById).mockImplementation((id: number) => ({
locationId: id,
name: id === 456 ? "Zebra Court" : "Alpha Court",
welshName: id === 456 ? "Llys Zebra" : "Llys Alpha"
}));

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

Normalise mocked location IDs to avoid string/number mismatches.
pendingSubscriptions are strings in the session, so strict numeric checks can miss the intended branch and weaken the test. Coerce the ID in the mock.

Proposed change
-      vi.mocked(locationService.getLocationById).mockImplementation((id: number) => ({
-        locationId: id,
-        name: id === 456 ? "Zebra Court" : "Alpha Court",
-        welshName: id === 456 ? "Llys Zebra" : "Llys Alpha"
-      }));
+      vi.mocked(locationService.getLocationById).mockImplementation((id: number | string) => {
+        const numericId = Number(id);
+        return {
+          locationId: numericId,
+          name: numericId === 456 ? "Zebra Court" : "Alpha Court",
+          welshName: numericId === 456 ? "Llys Zebra" : "Llys Alpha"
+        };
+      });
-      vi.mocked(locationService.getLocationById).mockImplementation((id: number) =>
-        id === 456 ? null : { locationId: id, name: `Location ${id}`, welshName: `Lleoliad ${id}` }
-      );
+      vi.mocked(locationService.getLocationById).mockImplementation((id: number | string) => {
+        const numericId = Number(id);
+        return numericId === 456
+          ? null
+          : { locationId: numericId, name: `Location ${numericId}`, welshName: `Lleoliad ${numericId}` };
+      });

Also applies to: 248-250

@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: 8

Caution

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

⚠️ Outside diff range comments (1)
libs/verified-pages/src/pages/subscription-management/index.njk (1)

1-1: Extend the required base template.

Line 1 extends layouts/base-template.njk, but the guideline requires layouts/base-templates.njk. Please align with the mandated base layout.

🔧 Proposed fix
-{% extends "layouts/base-template.njk" %}
+{% extends "layouts/base-templates.njk" %}

As per coding guidelines, this template must extend layouts/base-templates.njk.

🧹 Nitpick comments (3)
libs/verified-pages/src/pages/delete-subscription/index.ts (2)

38-45: Consider parallelising ownership checks.

Sequential await in a loop can be slow when multiple IDs are provided. Using Promise.all would fetch all subscriptions concurrently.

♻️ Suggested refactor
   try {
-    // Verify user owns all subscriptions
-    for (const id of idsArray) {
-      const subscription = await getSubscriptionById(id, userId);
-      if (!subscription) {
-        return res.redirect("/subscription-management");
-      }
+    // Verify user owns all subscriptions
+    const subscriptions = await Promise.all(
+      idsArray.map((id) => getSubscriptionById(id, userId))
+    );
+    if (subscriptions.some((sub) => !sub)) {
+      return res.redirect("/subscription-management");
     }

89-100: Same parallelisation opportunity as getHandler.

The sequential ownership verification loop could use Promise.all for consistency with any refactor applied to getHandler.

libs/verified-pages/src/pages/subscription-management/index.ts (1)

82-85: Potential undefined access on existing.allSubscriptionIds.

When retrieving from the Map, existing could theoretically be undefined if the key check and retrieval aren't atomic. The non-null assertion or a guard would make this safer.

Proposed fix
     } else {
-      const existing = seen.get(key);
-      existing.allSubscriptionIds.push(sub.subscriptionId);
+      const existing = seen.get(key);
+      if (existing) {
+        existing.allSubscriptionIds.push(sub.subscriptionId);
+      }
     }

Comment on lines 103 to 106
if (!unsubscribeConfirm) {
// Redirect to GET to show confirmation page
return res.redirect(`/delete-subscription?subscriptionId=${subscriptionId}`);
return res.redirect(`/delete-subscription?subscriptionId=${subscriptionIds}`);
}

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

URL-encode comma-separated IDs in redirect.

The subscriptionIds value contains commas which should be encoded when used in a query string to avoid potential parsing issues.

🔧 Proposed fix
   if (!unsubscribeConfirm) {
     // Redirect to GET to show confirmation page
-    return res.redirect(`/delete-subscription?subscriptionId=${subscriptionIds}`);
+    return res.redirect(`/delete-subscription?subscriptionId=${encodeURIComponent(subscriptionIds)}`);
   }

Note: If encoding is applied here, the GET handler would need to decode the value, though req.query typically handles this automatically.

📝 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
if (!unsubscribeConfirm) {
// Redirect to GET to show confirmation page
return res.redirect(`/delete-subscription?subscriptionId=${subscriptionId}`);
return res.redirect(`/delete-subscription?subscriptionId=${subscriptionIds}`);
}
if (!unsubscribeConfirm) {
// Redirect to GET to show confirmation page
return res.redirect(`/delete-subscription?subscriptionId=${encodeURIComponent(subscriptionIds)}`);
}

Comment on lines +45 to +72
<table class="govuk-table" id="cases-table">
<thead class="govuk-table__head">
<tr class="govuk-table__row">
<th scope="col" class="govuk-table__header" style="width: 30%;">{{ tableHeaderCaseName }}</th>
<th scope="col" class="govuk-table__header" style="width: 30%;">{{ tableHeaderCaseNumber }}</th>
<th scope="col" class="govuk-table__header" style="width: 30%;">{{ tableHeaderDate }}</th>
<th scope="col" class="govuk-table__header govuk-table__header--numeric" style="width: 10%; text-align: right;">{{ tableHeaderActions }}</th>
</tr>
</thead>
<tbody class="govuk-table__body">
{% for subscription in caseSubscriptions %}
<tr class="govuk-table__row">
<td class="govuk-table__cell" style="vertical-align: middle;">{{ subscription.caseName or notAvailable }}</td>
<td class="govuk-table__cell" style="vertical-align: middle;">{{ subscription.caseNumber or notAvailable }}</td>
<td class="govuk-table__cell" style="white-space: nowrap; vertical-align: middle;">{{ subscription.dateAdded | date('D MMMM YYYY') }}</td>
<td class="govuk-table__cell govuk-table__cell--numeric" style="text-align: right;">
<form method="post" action="/delete-subscription" style="display: inline; margin: 0; padding: 0;">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<input type="hidden" name="subscriptionId" value="{{ subscription.allSubscriptionIds.join(',') if subscription.allSubscriptionIds else subscription.subscriptionId }}">
<button type="submit" class="govuk-link govuk-button-as-link" aria-label="Remove subscription for {{ subscription.caseName or subscription.caseNumber }}">
{{ removeLink }}
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>

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:

#!/bin/bash

# Check if the file exists
fd "subscription-management/index.njk" libs/verified-pages/src/pages/ 2>/dev/null || echo "File path search completed"

# List files in the target directory
git ls-files "libs/verified-pages/src/pages/subscription-management/" 2>/dev/null | head -20

Repository: hmcts/cath-service

Length of output: 376


🏁 Script executed:

#!/bin/bash

# Read the template file to check its content
cat -n libs/verified-pages/src/pages/subscription-management/index.njk 2>/dev/null || echo "File not found or not readable"

Repository: hmcts/cath-service

Length of output: 11061


🌐 Web query:

GOV.UK Frontend Nunjucks govukTable macro documentation syntax

💡 Result:

govukTable (GOV.UK Frontend) Nunjucks macro syntax

{% from "govuk/components/table/macro.njk" import govukTable %}

{{ govukTable({
  caption: "Dates and amounts",
  captionClasses: "govuk-table__caption--m",
  firstCellIsHeader: true,
  head: [
    { text: "Date" },
    { text: "Amount" }
  ],
  rows: [
    [ { text: "First 6 weeks" }, { text: "£109.80 per week" } ],
    [ { text: "Next 33 weeks" }, { text: "£109.80 per week" } ]
  ]
}) }}

[1]

Options (summary)

  • rows (array, required): array of row arrays; each row is an array of cell objects. [1]
  • head (array, optional): array of header cell objects. [1]
  • caption (string), captionClasses (string), firstCellIsHeader (boolean), classes (string), attributes (object). [1]
  • Cell object (for rows and head): text or html, plus optional format (for example "numeric"), classes, colspan, rowspan, attributes. [1]

Source: GOV.UK Design System “Table” component page. [1]


Use GOV.UK Frontend table macro instead of raw table markup.

The template contains four tables using hand-rolled HTML. The coding guideline requires GOV.UK Frontend component macros; replace the raw <table> blocks with the govukTable macro (imported from govuk/components/table/macro.njk). The macro supports HTML content in cells, enabling conversion of the embedded form actions.

♻️ Suggested direction (illustrative)
-{% from "govuk/components/button/macro.njk" import govukButton %}
+{% from "govuk/components/button/macro.njk" import govukButton %}
+{% from "govuk/components/table/macro.njk" import govukTable %}

-<table class="govuk-table" id="cases-table">
-  ...
-</table>
+{{ govukTable({
+  attributes: { id: "cases-table" },
+  head: [
+    { text: tableHeaderCaseName },
+    { text: tableHeaderCaseNumber },
+    { text: tableHeaderDate },
+    { text: tableHeaderActions, classes: "govuk-table__header--numeric" }
+  ],
+  rows: caseSubscriptions | map(subscription => [
+    { text: subscription.caseName or notAvailable },
+    { text: subscription.caseNumber or notAvailable },
+    { text: subscription.dateAdded | date('D MMMM YYYY') },
+    { html: /* action form */ }
+  ])
+}) }}

Comment on lines +64 to +65
<button type="submit" class="govuk-link govuk-button-as-link" aria-label="Remove subscription for {{ subscription.caseName or subscription.caseNumber }}">
{{ removeLink }}

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

Provide a non-empty aria-label fallback.

If both caseName and caseNumber are missing, the aria-label becomes empty. Add notAvailable as a fallback to keep assistive text meaningful.

🔧 Proposed fix
-aria-label="Remove subscription for {{ subscription.caseName or subscription.caseNumber }}"
+aria-label="Remove subscription for {{ subscription.caseName or subscription.caseNumber or notAvailable }}"

Comment on lines +143 to +152
const mockCaseSubscriptions = [
{
subscriptionId: "sub1",
type: "case" as const,
caseName: "Test Case",
partyName: "John Doe",
referenceNumber: "REF123",
dateAdded: new Date()
}
];

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

Mock data structure doesn't match service return type.

The mock includes partyName and referenceNumber, but per getCaseSubscriptionsByUserId in libs/subscription/src/repository/service.ts, the service returns caseName, caseNumber, and dateAdded. This inconsistency could mask bugs if the view relies on caseNumber.

Proposed fix
       const mockCaseSubscriptions = [
         {
           subscriptionId: "sub1",
           type: "case" as const,
           caseName: "Test Case",
-          partyName: "John Doe",
-          referenceNumber: "REF123",
+          caseNumber: "CASE123",
           dateAdded: new Date()
         }
       ];

Comment on lines +170 to +204
it("should render page with both court and case subscriptions", async () => {
const mockCourtSubscriptions = [
{
subscriptionId: "sub1",
type: "court" as const,
courtOrTribunalName: "Birmingham Crown Court",
locationId: 456,
dateAdded: new Date()
}
];
const mockCaseSubscriptions = [
{
subscriptionId: "sub2",
type: "case" as const,
caseName: "Test Case",
partyName: "John Doe",
referenceNumber: "REF123",
dateAdded: new Date()
}
];

vi.mocked(subscriptionService.getAllSubscriptionsByUserId).mockResolvedValue(mockCourtSubscriptions);
vi.mocked(subscriptionService.getCaseSubscriptionsByUserId).mockResolvedValue(mockCaseSubscriptions);

await GET[GET.length - 1](mockReq as Request, mockRes as Response, vi.fn());

expect(mockRes.render).toHaveBeenCalledWith(
"subscription-management/index",
expect.objectContaining({
courtCount: 1,
caseCount: 1,
totalCount: 2
})
);
});

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

Mock data structure inconsistency.

The mock at lines 180-189 also uses partyName and referenceNumber instead of caseNumber, which doesn't align with the actual service response.

Proposed fix
       const mockCaseSubscriptions = [
         {
           subscriptionId: "sub2",
           type: "case" as const,
           caseName: "Test Case",
-          partyName: "John Doe",
-          referenceNumber: "REF123",
+          caseNumber: "CASE123",
           dateAdded: new Date()
         }
       ];

Comment on lines +305 to +323
it("should sort case subscriptions alphabetically by case name", async () => {
const mockCaseSubscriptions = [
{
subscriptionId: "sub1",
type: "case" as const,
caseName: "Zebra Case",
partyName: "Party A",
referenceNumber: "REF1",
dateAdded: new Date()
},
{
subscriptionId: "sub2",
type: "case" as const,
caseName: "Alpha Case",
partyName: "Party B",
referenceNumber: "REF2",
dateAdded: new Date()
}
];

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

Mock data inconsistent with service return type.

Same issue as above — these mocks use partyName and referenceNumber instead of caseNumber.

Proposed fix
       const mockCaseSubscriptions = [
         {
           subscriptionId: "sub1",
           type: "case" as const,
           caseName: "Zebra Case",
-          partyName: "Party A",
-          referenceNumber: "REF1",
+          caseNumber: "CASE001",
           dateAdded: new Date()
         },
         {
           subscriptionId: "sub2",
           type: "case" as const,
           caseName: "Alpha Case",
-          partyName: "Party B",
-          referenceNumber: "REF2",
+          caseNumber: "CASE002",
           dateAdded: new Date()
         }
       ];

Comment on lines +63 to +93
const sortCourtSubscriptions = (subscriptions: any[]): any[] => {
return subscriptions
.map((sub) => ({
...sub,
locationName: sub.courtOrTribunalName
}))
.sort((a, b) => (a.locationName || "").localeCompare(b.locationName || ""));
};

const deduplicateCaseSubscriptions = (subscriptions: any[]): any[] => {
const seen = new Map<string, any>();

for (const sub of subscriptions) {
const caseNumber = sub.caseNumber || "";
const caseName = sub.caseName || "";
const key = `${caseNumber}:${caseName}`;

if (!seen.has(key)) {
seen.set(key, { ...sub, allSubscriptionIds: [sub.subscriptionId] });
} else {
const existing = seen.get(key);
existing.allSubscriptionIds.push(sub.subscriptionId);
}
}

return Array.from(seen.values());
};

const sortCaseSubscriptions = (subscriptions: any[]): any[] => {
return [...subscriptions].sort((a, b) => (a.caseName || "").localeCompare(b.caseName || ""));
};

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

Avoid untyped any in helper functions.

These helper functions use any[] throughout. Per coding guidelines, strict TypeScript mode should be enabled with no any without justification. Consider defining interfaces for the subscription types.

Proposed fix
+interface CourtSubscription {
+  subscriptionId: string;
+  courtOrTribunalName?: string;
+  locationName?: string;
+  [key: string]: unknown;
+}
+
+interface CaseSubscription {
+  subscriptionId: string;
+  caseName?: string;
+  caseNumber?: string;
+  allSubscriptionIds?: string[];
+  [key: string]: unknown;
+}
+
-const sortCourtSubscriptions = (subscriptions: any[]): any[] => {
+const sortCourtSubscriptions = (subscriptions: CourtSubscription[]): CourtSubscription[] => {
   return subscriptions
     .map((sub) => ({
       ...sub,
       locationName: sub.courtOrTribunalName
     }))
     .sort((a, b) => (a.locationName || "").localeCompare(b.locationName || ""));
 };

-const deduplicateCaseSubscriptions = (subscriptions: any[]): any[] => {
-  const seen = new Map<string, any>();
+const deduplicateCaseSubscriptions = (subscriptions: CaseSubscription[]): CaseSubscription[] => {
+  const seen = new Map<string, CaseSubscription>();

   for (const sub of subscriptions) {
     const caseNumber = sub.caseNumber || "";
     const caseName = sub.caseName || "";
     const key = `${caseNumber}:${caseName}`;

     if (!seen.has(key)) {
       seen.set(key, { ...sub, allSubscriptionIds: [sub.subscriptionId] });
     } else {
-      const existing = seen.get(key);
+      const existing = seen.get(key)!;
       existing.allSubscriptionIds.push(sub.subscriptionId);
     }
   }

   return Array.from(seen.values());
 };

-const sortCaseSubscriptions = (subscriptions: any[]): any[] => {
+const sortCaseSubscriptions = (subscriptions: CaseSubscription[]): CaseSubscription[] => {
   return [...subscriptions].sort((a, b) => (a.caseName || "").localeCompare(b.caseName || ""));
 };

Comment on lines +16 to +28
const subscriptionIds = req.session.emailSubscriptions?.subscriptionToRemove;

if (!subscriptionId) {
if (!subscriptionIds) {
return res.redirect("/subscription-management");
}

// Split comma-separated IDs if present
const idsArray = subscriptionIds.split(",").map((id: string) => id.trim());

try {
await removeSubscription(subscriptionId, userId);
// Delete all subscriptions
for (const id of idsArray) {
await removeSubscription(id, userId);

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

Validate and normalise subscriptionToRemove before deletion.

The endpoint only checks presence; empty/whitespace IDs or duplicates can trigger spurious errors and redirects. Ensure the value is a non‑empty string, filter blanks, and de‑duplicate before deletion.

💡 Proposed fix
-  const subscriptionIds = req.session.emailSubscriptions?.subscriptionToRemove;
+  const subscriptionIds = req.session.emailSubscriptions?.subscriptionToRemove;

-  if (!subscriptionIds) {
+  if (typeof subscriptionIds !== "string" || subscriptionIds.trim() === "") {
     return res.redirect("/subscription-management");
   }

   // Split comma-separated IDs if present
-  const idsArray = subscriptionIds.split(",").map((id: string) => id.trim());
+  const idsArray = Array.from(
+    new Set(subscriptionIds.split(",").map((id: string) => id.trim()).filter(Boolean))
+  );
+  if (idsArray.length === 0) {
+    return res.redirect("/subscription-management");
+  }

As per coding guidelines, input validation is required on all endpoints.

@KianKwa
KianKwa changed the base branch from master to feature/VIBE-316-refactor-artefact-search-extraction-subscription January 26, 2026 15:17
@KianKwa
KianKwa changed the base branch from feature/VIBE-316-refactor-artefact-search-extraction-subscription to master January 26, 2026 16:24

@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: 3

♻️ Duplicate comments (2)
libs/verified-pages/src/pages/pending-subscriptions/index.ts (2)

79-95: Validate POST body before branching on action.
action, locationId, and caseId are trusted directly; invalid combinations currently fall through without a response. Add explicit validation and return 400 for bad input. As per coding guidelines, input validation must be performed on all endpoints.

Proposed guard
-  const { action, locationId, caseId } = req.body;
+  const action = typeof req.body.action === "string" ? req.body.action : "";
+  const locationId = typeof req.body.locationId === "string" ? req.body.locationId : undefined;
+  const caseId = typeof req.body.caseId === "string" ? req.body.caseId : undefined;
+
+  const isValidAction =
+    action === "confirm" ||
+    (action === "remove" && !!locationId) ||
+    (action === "removeCase" && !!caseId);
+  if (!isValidAction) {
+    return res.status(400).send("Bad Request");
+  }

198-206: Guard against missing case name/number before creating subscriptions.
searchValue becomes undefined when both caseName and caseNumber are absent, which can lead to invalid calls to createCaseSubscription. Add a guard and fail fast (or skip) before invoking the service.

Example safeguard
     uniqueCases.map((caseItem: any) => {
       const searchType = caseItem.searchType || "CASE_NUMBER";
       const searchValue = searchType === "CASE_NAME" ? caseItem.caseName : caseItem.caseNumber;
+      if (!searchValue) {
+        throw new Error("Invalid case subscription data");
+      }
       return createCaseSubscription(userId, searchType, searchValue, caseItem.caseNumber, caseItem.caseName);
     })
🧹 Nitpick comments (2)
libs/publication/src/artefact-search-extractor.ts (1)

4-13: Place types at the bottom to match module ordering.

Module ordering calls for interfaces/types after functions; consider moving CaseData and CaseObject below the helper and exported functions to keep structure consistent. As per coding guidelines, please keep interfaces/types at the bottom.

libs/verified-pages/src/pages/pending-subscriptions/index.test.ts (1)

79-83: Stabilise translation key comparison to avoid order sensitivity.
Object.keys preserves insertion order, so the equality check can fail if keys are reordered without changing content. Consider sorting both arrays before comparison.

Suggested tweak
-        const enKeys = Object.keys(en);
-        const cyKeys = Object.keys(cy);
+        const enKeys = Object.keys(en).sort();
+        const cyKeys = Object.keys(cy).sort();
 
         expect(cyKeys).toEqual(enKeys);

Comment on lines +84 to +104
function extractFromRootLevel(
jsonPayload: unknown,
caseNumberFieldName: string,
caseNameFieldName: string,
hasCaseNumberField: boolean,
hasCaseNameField: boolean
): CaseData[] | null {
if (Array.isArray(jsonPayload)) {
return null;
}

const rootObj = jsonPayload as Record<string, unknown>;
const hasRootFields = (hasCaseNumberField && caseNumberFieldName in rootObj) || (hasCaseNameField && caseNameFieldName in rootObj);

if (!hasRootFields) {
return null;
}

const caseData = extractCaseDataFromObject(rootObj, caseNumberFieldName, caseNameFieldName, hasCaseNumberField, hasCaseNameField);
return caseData ? [caseData] : null;
}

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

Guard against non‑object root payloads to avoid a TypeError.

Line 95 uses the in operator on rootObj; if jsonPayload is a string/number/boolean, this throws and the extraction is skipped. Add a non‑object guard before the cast.

Suggested fix
 function extractFromRootLevel(
   jsonPayload: unknown,
   caseNumberFieldName: string,
   caseNameFieldName: string,
   hasCaseNumberField: boolean,
   hasCaseNameField: boolean
 ): CaseData[] | null {
-  if (Array.isArray(jsonPayload)) {
+  if (!jsonPayload || typeof jsonPayload !== "object" || Array.isArray(jsonPayload)) {
     return null;
   }
 
   const rootObj = jsonPayload as Record<string, unknown>;
📝 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 extractFromRootLevel(
jsonPayload: unknown,
caseNumberFieldName: string,
caseNameFieldName: string,
hasCaseNumberField: boolean,
hasCaseNameField: boolean
): CaseData[] | null {
if (Array.isArray(jsonPayload)) {
return null;
}
const rootObj = jsonPayload as Record<string, unknown>;
const hasRootFields = (hasCaseNumberField && caseNumberFieldName in rootObj) || (hasCaseNameField && caseNameFieldName in rootObj);
if (!hasRootFields) {
return null;
}
const caseData = extractCaseDataFromObject(rootObj, caseNumberFieldName, caseNameFieldName, hasCaseNumberField, hasCaseNameField);
return caseData ? [caseData] : null;
}
function extractFromRootLevel(
jsonPayload: unknown,
caseNumberFieldName: string,
caseNameFieldName: string,
hasCaseNumberField: boolean,
hasCaseNameField: boolean
): CaseData[] | null {
if (!jsonPayload || typeof jsonPayload !== "object" || Array.isArray(jsonPayload)) {
return null;
}
const rootObj = jsonPayload as Record<string, unknown>;
const hasRootFields = (hasCaseNumberField && caseNumberFieldName in rootObj) || (hasCaseNameField && caseNameFieldName in rootObj);
if (!hasRootFields) {
return null;
}
const caseData = extractCaseDataFromObject(rootObj, caseNumberFieldName, caseNameFieldName, hasCaseNumberField, hasCaseNameField);
return caseData ? [caseData] : null;
}

Comment on lines +144 to +165
export async function extractAndStoreArtefactSearch(artefactId: string, listTypeId: number, jsonPayload: unknown): Promise<void> {
try {
const config = await getConfigForListType(listTypeId);

if (config && jsonPayload) {
// Extract all cases from the payload (handles both objects and arrays)
const cases = extractCases(jsonPayload, config.caseNumberFieldName, config.caseNameFieldName);

if (cases.length > 0) {
// Delete existing entries for this artefact to ensure idempotency
await repository.deleteArtefactSearchByArtefactId(artefactId);

// Create new entries for all cases
for (const caseData of cases) {
await repository.createArtefactSearch(artefactId, caseData.caseNumber, caseData.caseName);
}
}
}
} catch (error) {
console.error(`[ArtefactSearch] Failed to extract/store for artefact ${artefactId}:`, error);
}
}

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

Make delete+recreate atomic to avoid partial artefact‑search rows.

If any insert fails after the delete, the artefact ends up with missing rows and the error is only logged. Consider a repository helper that performs delete+bulk create in a single transaction (and optionally rethrows so ingestion can react).

Suggested direction
-        // Delete existing entries for this artefact to ensure idempotency
-        await repository.deleteArtefactSearchByArtefactId(artefactId);
-
-        // Create new entries for all cases
-        for (const caseData of cases) {
-          await repository.createArtefactSearch(artefactId, caseData.caseNumber, caseData.caseName);
-        }
+        // Replace entries atomically to avoid partial updates
+        await repository.replaceArtefactSearch(artefactId, cases);

Comment on lines +36 to +39
afterEach(() => {
vi.clearAllMocks();
console.error = vi.fn();
});

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

console.error mock placement issue.

Assigning console.error = vi.fn() in afterEach means the first test runs with the real console.error. Move this to beforeEach or use vi.spyOn for proper restoration.

Proposed fix
 beforeEach(() => {
+  vi.spyOn(console, 'error').mockImplementation(() => {});
   mockReq = {
     body: {},
     session: {} as any,
     path: "/case-number-search",
     csrfToken: vi.fn(() => "mock-csrf-token")
   };
   mockRes = {
     render: vi.fn(),
     redirect: vi.fn(),
     locals: {}
   };
 });

 afterEach(() => {
   vi.clearAllMocks();
-  console.error = vi.fn();
+  vi.restoreAllMocks();
 });

@KianKwa
KianKwa changed the base branch from master to feature/VIBE-316-refactor-artefact-search-extraction-subscription January 26, 2026 17:01
…extraction-subscription

# Conflicts:
#	libs/publication/src/index.ts
#	libs/publication/src/repository/queries.test.ts
#	package.json
#	yarn.lock
@KianKwa
KianKwa changed the base branch from feature/VIBE-316-refactor-artefact-search-extraction-subscription to master February 17, 2026 11:19
…factor-artefact-search-extraction-subscription

# Conflicts:
#	e2e-tests/tests/system-admin-dashboard.spec.ts
#	libs/location/prisma/schema.prisma
#	libs/system-admin-pages/src/pages/system-admin-dashboard/cy.ts
#	libs/system-admin-pages/src/pages/system-admin-dashboard/en.ts
#	libs/system-admin-pages/src/pages/system-admin-dashboard/index.njk.test.ts
…bscription' into feature/VIBE-300

# Conflicts:
#	libs/publication/src/index.ts
#	tsconfig.json

@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: 5

🧹 Nitpick comments (3)
libs/system-admin-pages/src/pages/system-admin-dashboard/index.njk.test.ts (1)

18-78: Consider adding Welsh (cy) locale coverage.

The AI summary notes that cy.ts was also updated with a new tile. This test file only validates the English locale. As per coding guidelines, "Every page must support both English and Welsh by providing en and cy content objects to the renderer, and templates should test with ?lng=cy query parameter."

libs/publication/src/repository/queries.ts (1)

275-275: Redundant comment — describes "what", not "why".

The re-export statement is self-explanatory. As per coding guidelines, "Do not add comments unless meaningful - explain why, not what."

Suggested removal
-// Re-export artefact search functions from artefact-search-queries
 export {
libs/verified-pages/src/pages/pending-subscriptions/index.ts (1)

229-245: Navigation setup is duplicated in three places.

getHandler (lines 16–19), handleConfirm error path (lines 169–172), and renderEmptyPendingSubscriptions (lines 230–233) all repeat the same navigation-initialisation block. Consider extracting a small helper to keep it DRY.

Also applies to: 15-31

}

model ArtefactSearch {
id String @id @default(uuid())

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

Missing @db.Uuid on primary key.

Every other UUID primary key in this schema (Artefact.artefactId, User.userId, MediaApplication.id, IngestionLog.id) specifies @db.Uuid to use a native PostgreSQL uuid column type. Without it, this id defaults to text, which is inconsistent and less efficient for indexing and storage.

Proposed fix
-  id         String    `@id` `@default`(uuid())
+  id         String    `@id` `@default`(uuid()) `@db.Uuid`
📝 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
id String @id @default(uuid())
id String `@id` `@default`(uuid()) `@db.Uuid`

Comment on lines 92 to +95
if (action === "confirm") {
if (pendingLocationIds.length === 0) {
return res.redirect("/location-name-search");
return handleConfirm(req, res, userId, pendingLocationIds, pendingCases, locale, t);
}
};

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

postHandler silently drops requests with unrecognised actions.

If action doesn't match "remove", "removeCase", or "confirm", execution falls off the end and the response hangs indefinitely. Add a fallback.

Proposed fix
   if (action === "confirm") {
     return handleConfirm(req, res, userId, pendingLocationIds, pendingCases, locale, t);
   }
+
+  return res.redirect("/pending-subscriptions");
 };

locale: string,
t: typeof en
) => {
req.session.emailSubscriptions.pendingSubscriptions = pendingLocationIds.filter((id: string) => id !== locationId);

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

Potential NPE when accessing req.session.emailSubscriptions without a null guard.

Lines 81–82 use optional chaining (?.), but the helper functions write directly to req.session.emailSubscriptions.pendingSubscriptions (line 106), .pendingCaseSubscriptions (line 119), and several properties in handleConfirm (lines 145–149). If emailSubscriptions is undefined, these assignments throw at runtime.

Proposed fix — initialise before mutating
+ req.session.emailSubscriptions ??= {};
  req.session.emailSubscriptions.pendingSubscriptions = pendingLocationIds.filter((id: string) => id !== locationId);

Apply the same guard at the top of handleRemoveCase and handleConfirm.

Also applies to: 119-119, 145-149

Comment on lines +177 to +186
res.render("pending-subscriptions/index", {
...t,
errors: {
titleText: t.errorSummaryTitle,
errorList: [{ text: errorMessage }]
},
locations: pendingLocations,
cases: pendingCases,
isPlural
});

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

csrfToken missing from the error re-render in handleConfirm.

The GET handler passes csrfToken (line 66), but the error render here omits it. If the template's form includes a CSRF field, resubmission from this page will fail.

Proposed fix
     res.render("pending-subscriptions/index", {
       ...t,
       errors: {
         titleText: t.errorSummaryTitle,
         errorList: [{ text: errorMessage }]
       },
       locations: pendingLocations,
       cases: pendingCases,
-      isPlural
+      isPlural,
+      csrfToken: (req as any).csrfToken?.() || ""
     });

Comment thread tsconfig.json
Comment on lines +35 to +36
"@hmcts/subscriptions": ["libs/subscription/src"],
"@hmcts/subscriptions/config": ["libs/subscription/src/config"],

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:

#!/bin/bash
# Check which form is used in imports across the codebase
echo "=== Imports using `@hmcts/subscriptions` ==="
rg -n "@hmcts/subscriptions" --type=ts --type=tsx -g '!node_modules' -g '!dist' 2>/dev/null | head -30

echo ""
echo "=== Imports using `@hmcts/subscription` (singular, no trailing s) ==="
rg -n "@hmcts/subscription[^s]" --type=ts --type=tsx -g '!node_modules' -g '!dist' 2>/dev/null | head -30

echo ""
echo "=== Verify libs/subscription directory exists ==="
fd -t d '^subscription$' libs/ --max-depth 1 2>/dev/null

echo ""
echo "=== Verify libs/subscriptions directory does NOT exist ==="
fd -t d '^subscriptions$' libs/ --max-depth 1 2>/dev/null

echo ""
echo "=== Check current tsconfig.json entries for subscriptions ==="
rg -A2 "@hmcts/subscriptions" tsconfig.json 2>/dev/null | head -20

Repository: hmcts/cath-service

Length of output: 608


Alias name does not match directory: change @hmcts/subscriptions to @hmcts/subscription

The alias @hmcts/subscriptions (plural) points to libs/subscription/src (singular), which violates the coding guideline requiring @hmcts/* aliases to match libs/[module]/src naming. Update line 35 to "@hmcts/subscription" and line 36 to "@hmcts/subscription/config" to align the alias with the actual directory name.

@sonarqubecloud

Copy link
Copy Markdown

@KianKwa KianKwa closed this Aug 10, 2026
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.

2 participants