Skip to content

feat(301): third-party user management#458

Open
alexbottenberg wants to merge 11 commits intomasterfrom
feature/301-third-party-user-management
Open

feat(301): third-party user management#458
alexbottenberg wants to merge 11 commits intomasterfrom
feature/301-third-party-user-management

Conversation

@alexbottenberg
Copy link
Copy Markdown
Contributor

@alexbottenberg alexbottenberg commented Mar 20, 2026

Summary

  • Implements third-party user management for system admins ([VIBE-313] Third Party User Management - Future #301)
  • Adds create, view, delete flows for third-party users
  • Adds subscription management with LaunchDarkly-gated radio button UI (third-party-subscriptions-radio-buttons flag)
  • Fixes dotenv loading order in both servers so env vars are available at module initialisation time (fixes LaunchDarkly SDK key not being picked up)

Test plan

  • Log in as a system admin
  • Navigate to third-party users list
  • Create a new third-party user and verify confirmation page
  • View a user and manage their subscriptions
  • Enable the third-party-subscriptions-radio-buttons flag in LaunchDarkly — subscriptions page should show radio buttons instead of dropdowns
  • Delete a user and verify confirmation page
  • Run yarn test — all unit tests pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added third‑party user management admin UI (create, summary, confirm, manage, delete)
    • A/B tested subscriptions UI (radio vs dropdown) via feature flag; paginated subscriptions (20/page)
    • Welsh localisation across third‑party user pages; deletion confirmation and idempotent create flow
    • Audit logging for create, update and delete actions
    • Database migration to add third‑party user and subscription tables
  • Tests

    • New unit and end‑to‑end tests covering create/manage/delete and accessibility
  • Documentation

    • Added technical plan and implementation tasks for the feature

github-actions bot and others added 5 commits March 18, 2026 14:53
Adds ticket, plan, and tasks documentation for issue #301 covering
the full CRUD workflow for third-party users including LaunchDarkly
A/B testing of the subscriptions UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add libs/third-party-user module with Prisma schema, service, and validation
- Add third-party user pages (list, create, manage, subscriptions, delete)
- Add LaunchDarkly feature flag integration for subscription UI variant
- Update LD flag key to third-party-subscriptions-radio-buttons
- Add E2E tests for third-party user management journeys
- Fix web dev script to preload DATABASE_URL before ESM module evaluation
- Add database migration for third_party_user and third_party_subscription tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Static imports are hoisted in ESM, so env vars were unavailable when
modules like LaunchDarkly initialised their SDK key at load time.
Switching to dynamic import ensures dotenv.config() runs first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alexbottenberg alexbottenberg linked an issue Mar 20, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 20, 2026

Warning

Rate limit exceeded

@alexbottenberg has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 8 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 708af5a2-9ab5-417f-bb81-797b5378eb96

📥 Commits

Reviewing files that changed from the base of the PR and between 5d9bbc9 and f77c5aa.

📒 Files selected for processing (5)
  • apps/web/package.json
  • libs/system-admin-pages/src/pages/third-party-users/create/summary/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/create/summary/index.ts
  • libs/third-party-user/package.json
  • libs/third-party-user/tsconfig.json
📝 Walkthrough

Walkthrough

Adds a new third‑party user feature: a new library with Prisma schema and services, DB migration, admin UI (create/summary/confirmation, manage, subscriptions with A/B via LaunchDarkly, delete), session-backed flows, tests, docs, and env loading changes in server startup; tooling hook and tsconfig/path aliases updated.

Changes

Cohort / File(s) Summary
Module init / dev scripts
apps/api/src/server.ts, apps/web/src/server.ts, apps/web/package.json
Load .env earlier via dotenv.config() and switch to dynamic top-level await import("./app.js"); update web dev scripts to pass --env-file ../../.env.
Tooling hook
.claude/hooks/post-write.sh
Run Biome only when an executable binary is resolved and CLAUDE_FILE_PATHS supplies existing JS/TS files; format/check only those files and treat failures as non-blocking (log instead of exit).
Database migration & discovery
apps/postgres/prisma/migrations/.../migration.sql, apps/postgres/src/schema-discovery.ts, apps/postgres/src/schema-discovery.test.ts
Add migration to create third_party_user and third_party_subscription tables with FK, unique composite index; extend Prisma schema discovery to include third‑party user schemas and update tests.
New package: third-party-user
libs/third-party-user/package.json, libs/third-party-user/prisma/schema.prisma, libs/third-party-user/src/*, libs/third-party-user/tsconfig.json
Introduce @hmcts/third-party-user package: Prisma schema + client generation path, config export (prismaSchemas), service layer (CRUD + subscriptions update), name validation, tests, build/lint/test scripts, and tsconfig.
System admin pages & templates
libs/system-admin-pages/src/pages/third-party-users/... (many files), libs/system-admin-pages/src/pages/.../*.njk, *.test.ts
Implement full admin UI: list, create (entry/summary/confirmation), detail, subscriptions (paginated, session-backed), delete (confirmation), English/Welsh locales, Nunjucks templates, route handlers (GET/POST) with role gating, audit metadata wiring and extensive tests.
LaunchDarkly integration
libs/system-admin-pages/src/feature-flags/launch-darkly.ts
Add lazy singleton LD client wrapper isFeatureEnabled(flagKey, userId) with safe fallback when SDK key missing or init fails.
Package & TS path wiring
libs/system-admin-pages/package.json, tsconfig.json
Add dependencies (@launchdarkly/node-server-sdk, @hmcts/list-types-common, @hmcts/third-party-user) and TypeScript path aliases for the new library.
End-to-end tests
e2e-tests/tests/third-party-user-management.spec.ts
Add Playwright suite covering create, subscriptions management, deletion flows, accessibility checks, Welsh locale checks, and cleanup via Prisma.
Documentation & planning
docs/tickets/301/*
Add full technical plan, task breakdown and ticket spec describing requirements, edge cases, acceptance criteria, LaunchDarkly considerations and WCAG/Welsh requirements.

Sequence Diagram(s)

sequenceDiagram
    participant User as System Admin
    participant Browser as Browser
    participant Server as Express Server
    participant LD as LaunchDarkly
    participant DB as Prisma/Database
    participant Session as Session Store

    User->>Browser: Open create page
    Browser->>Server: GET /third-party-users/create
    Server->>Session: Load session.thirdPartyCreate
    Server->>Browser: Render create form

    User->>Browser: Submit name
    Browser->>Server: POST /third-party-users/create {name}
    Server->>Server: validateName
    Server->>Session: Save session.thirdPartyCreate{name}
    Server->>Browser: Redirect to summary

    Browser->>Server: POST /third-party-users/create/summary (confirm)
    Server->>DB: createThirdPartyUser(name)
    DB-->>Server: user {id,name}
    Server->>Session: Set createdId/createdName
    Server->>Browser: Redirect to confirmation
Loading
sequenceDiagram
    participant User as System Admin
    participant Browser as Browser
    participant Server as Express Server
    participant LD as LaunchDarkly
    participant DB as Prisma/Database
    participant Session as Session Store

    User->>Browser: Open subscriptions page
    Browser->>Server: GET /third-party-users/{id}/subscriptions?page=1
    Server->>DB: findThirdPartyUserById(id)
    Server->>LD: isFeatureEnabled(flag, userId)
    LD-->>Server: flagResult
    Server->>Session: Init/restore pending subscriptions
    Server->>DB: fetch list types (via list-types-common)
    Server->>Browser: Render subscriptions page (UI variant from flag)

    User->>Browser: Submit page selections
    Browser->>Server: POST /.../subscriptions?page=N
    Server->>Session: Update pending
    alt not last page
        Server->>Browser: Redirect to next page
    else last page
        Server->>DB: $transaction(deleteMany, createMany)
        DB-->>Server: transaction result
        Server->>Session: Clear pending
        Server->>Browser: Redirect to confirmation
    end
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% 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 PR title 'feat(301): third-party user management' clearly summarises the primary change—implementing third-party user management functionality—and directly aligns with the main objectives and file changes in the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/301-third-party-user-management
📝 Coding Plan
  • Generate coding plan for human review comments

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
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 20, 2026

🎭 Playwright E2E Test Results

257 tests   257 ✅  24m 38s ⏱️
 34 suites    0 💤
  1 files      0 ❌

Results for commit f77c5aa.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (13)
libs/system-admin-pages/src/feature-flags/launch-darkly.ts (1)

14-18: Please avoid silent LaunchDarkly initialisation failures.

Line 16 swallows the error completely. Add a warning/error log (ideally once) so LD outages or bad SDK config are observable in production.

libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/confirmation/index.njk (1)

8-11: Prefer text over html for panel body content.

If panelBody is plain copy, render it with text to reduce XSS exposure from future content changes.

Suggested change
-    {{ govukPanel({
-      titleText: panelTitle,
-      html: panelBody
-    }) }}
+    {{ govukPanel({
+      titleText: panelTitle,
+      text: panelBody
+    }) }}
libs/system-admin-pages/src/pages/third-party-users/index.njk (1)

4-4: Unused import: govukBackLink macro.

The macro is imported but the backLink block uses a plain anchor tag instead. Remove the unused import for consistency.

🧹 Proposed fix
 {% from "govuk/components/table/macro.njk" import govukTable %}
-{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
libs/system-admin-pages/src/pages/third-party-users/create/confirmation/index.njk (1)

3-3: Unused import: govukButton macro.

The button macro is imported but not used on this confirmation page. Remove to keep imports clean.

🧹 Proposed fix
 {% from "govuk/components/panel/macro.njk" import govukPanel %}
-{% from "govuk/components/button/macro.njk" import govukButton %}
libs/system-admin-pages/src/pages/third-party-users/create/index.njk (1)

5-5: Unused import: govukBackLink macro.

Same as other templates - the macro is imported but the backLink block uses a plain anchor. Remove for consistency.

🧹 Proposed fix
 {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
-{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
libs/third-party-user/src/name-validation.ts (1)

2-2: ASCII regex limits names to basic Latin characters, inconsistent with potential international users.

The regex currently restricts names to ASCII letters (a–z, A–Z). Whilst this works for English and Welsh, if third-party users include those with accented characters (é, ñ, ü) or non-Latin scripts, names will be rejected. The codebase supports only English and Welsh locales currently, so this is not an immediate concern, but consider using Unicode-aware patterns (/^[\p{L}\p{N} '-]+$/u) if international character support becomes required.

libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/index.njk (2)

1-5: Missing error handling with govukErrorSummary.

The template imports govukRadios but doesn't use it (raw HTML radio inputs are used instead). More importantly, there's no govukErrorSummary import or usage for displaying validation errors. As per coding guidelines, Nunjucks templates should include error handling with govukErrorSummary.

Consider adding error handling:

{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}

And conditionally render it when errors exist.


6-8: Consider using the govukBackLink macro.

The back link is implemented with a raw anchor element, but govukBackLink is imported and unused. Using the macro ensures consistent styling and behaviour.

♻️ Suggested change
 {% block backLink %}
-  <a href="/third-party-users/{{ userId }}{{ lngParam }}" class="govuk-back-link">{{ back }}</a>
+  {{ govukBackLink({
+    href: "/third-party-users/" + userId + lngParam,
+    text: back
+  }) }}
 {% endblock %}
libs/system-admin-pages/src/pages/third-party-users/[id]/index.ts (1)

29-29: Date locale is hardcoded regardless of language selection.

The date is formatted with "en-GB" locale even when Welsh (cy) is selected. Consider using the selected language for date formatting.

♻️ Suggested fix
-    createdAt: user.createdAt.toLocaleDateString("en-GB"),
+    createdAt: user.createdAt.toLocaleDateString(language === "cy" ? "cy-GB" : "en-GB"),
libs/system-admin-pages/src/pages/third-party-users/[id]/delete/index.test.ts (1)

24-51: Consider adding Welsh language test for getHandler.

The postHandler tests include Welsh parameter handling (line 116-128), but getHandler lacks a test verifying Welsh content rendering when req.query.lng === "cy".

libs/third-party-user/src/third-party-user-service.ts (1)

44-46: Consider handling non-existent user deletion gracefully.

prisma.delete throws PrismaClientKnownRequestError with code P2025 if the record doesn't exist. Depending on requirements, you may want to handle this case explicitly.

docs/tickets/301/tasks.md (1)

19-19: Incomplete task: CATH_LD_KEY environment variable configuration.

This task remains unchecked. Ensure the environment variable is documented in .env.example before merging, or track it as a follow-up issue.

Would you like me to open an issue to track adding CATH_LD_KEY to .env.example?

libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/index.test.ts (1)

142-185: Consider adding a test for pagination redirect.

The postHandler tests cover the final-page save scenario but don't verify the redirect to the next page when isLastPage is false. This is an important code path in the source handler (lines 90-93).

📝 Suggested test case
+    it("should redirect to next page when not on last page", async () => {
+      // Arrange
+      vi.mocked(findThirdPartyUserById).mockResolvedValue(mockUser as never);
+      req.session = { thirdPartySubscriptions: { userId: "user-1", pending: {} } } as never;
+      req.query = { page: "1" };
+      req.body = { CIVIL_DAILY_CAUSE_LIST: "PUBLIC" };
+      // Mock more list types to have multiple pages
+      vi.mocked(await import("@hmcts/list-types-common")).mockListTypes = Array(25).fill(mockUser.subscriptions[0]);
+
+      // Act
+      await postHandler(req as Request, res as Response);
+
+      // Assert
+      expect(res.redirect).toHaveBeenCalledWith("/third-party-users/user-1/subscriptions?page=2");
+    });

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c38d5512-3025-4a16-8ff4-05d82f7ec3f2

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea04f2 and fbc3def.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (67)
  • .claude/hooks/post-write.sh
  • apps/api/src/server.ts
  • apps/postgres/prisma/migrations/20260318000000_add_third_party_user/migration.sql
  • apps/postgres/src/schema-discovery.ts
  • apps/web/package.json
  • apps/web/src/server.ts
  • docs/tickets/301/plan.md
  • docs/tickets/301/tasks.md
  • docs/tickets/301/ticket.md
  • e2e-tests/tests/third-party-user-management.spec.ts
  • libs/system-admin-pages/package.json
  • libs/system-admin-pages/src/feature-flags/launch-darkly.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/confirmation/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/confirmation/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/confirmation/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/confirmation/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/confirmation/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/delete/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/[id]/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/confirmation/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/confirmation/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/confirmation/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/confirmation/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/confirmation/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/[id]/subscriptions/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/confirmation/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/confirmation/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/confirmation/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/create/confirmation/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/confirmation/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/create/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/summary/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/summary/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/summary/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/create/summary/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/create/summary/index.ts
  • libs/system-admin-pages/src/pages/third-party-users/cy.ts
  • libs/system-admin-pages/src/pages/third-party-users/en.ts
  • libs/system-admin-pages/src/pages/third-party-users/index.njk
  • libs/system-admin-pages/src/pages/third-party-users/index.test.ts
  • libs/system-admin-pages/src/pages/third-party-users/index.ts
  • libs/third-party-user/package.json
  • libs/third-party-user/prisma/schema.prisma
  • libs/third-party-user/src/config.ts
  • libs/third-party-user/src/index.ts
  • libs/third-party-user/src/name-validation.test.ts
  • libs/third-party-user/src/name-validation.ts
  • libs/third-party-user/src/third-party-user-service.test.ts
  • libs/third-party-user/src/third-party-user-service.ts
  • libs/third-party-user/tsconfig.json
  • tsconfig.json

Comment on lines +42 to +57
# Filter to only TypeScript/JavaScript files (skip .njk, .json, .prisma, .sh, etc.)
TS_FILES=""
while IFS= read -r f; do
case "$f" in
*.ts|*.tsx|*.js|*.jsx)
if [ -f "$f" ]; then
TS_FILES="$TS_FILES $f"
fi
;;
esac
done <<< "$FILES_TO_CHECK"

if [ -z "$TS_FILES" ]; then
log_hook "No TS/JS files to check"
exit 0
fi
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

Paths with spaces will break due to word-splitting.

The $TS_FILES variable is built by concatenation and later expanded unquoted (lines 63, 67). Filenames containing spaces will be incorrectly split.

🛠️ Suggested fix using an array
-# Filter to only TypeScript/JavaScript files (skip .njk, .json, .prisma, .sh, etc.)
-TS_FILES=""
-while IFS= read -r f; do
-    case "$f" in
-        *.ts|*.tsx|*.js|*.jsx)
-            if [ -f "$f" ]; then
-                TS_FILES="$TS_FILES $f"
-            fi
-            ;;
-    esac
-done <<< "$FILES_TO_CHECK"
-
-if [ -z "$TS_FILES" ]; then
-    log_hook "No TS/JS files to check"
-    exit 0
-fi
+# Filter to only TypeScript/JavaScript files (skip .njk, .json, .prisma, .sh, etc.)
+TS_FILES=()
+while IFS= read -r f; do
+    case "$f" in
+        *.ts|*.tsx|*.js|*.jsx)
+            if [ -f "$f" ]; then
+                TS_FILES+=("$f")
+            fi
+            ;;
+    esac
+done <<< "$FILES_TO_CHECK"
+
+if [ ${`#TS_FILES`[@]} -eq 0 ]; then
+    log_hook "No TS/JS files to check"
+    exit 0
+fi

Then update the biome invocations:

-if ! $BIOME_BIN format --write $TS_FILES 2>&1; then
+if ! "$BIOME_BIN" format --write "${TS_FILES[@]}" 2>&1; then
     log_hook "Format had issues (non-blocking)"
 fi

-if ! $BIOME_BIN check --write $TS_FILES 2>&1; then
+if ! "$BIOME_BIN" check --write "${TS_FILES[@]}" 2>&1; then
     log_hook "Lint had issues (non-blocking)"
 fi


Each page follows the existing pattern: `index.ts` (controller), `en.ts`, `cy.ts`, `index.njk`, `index.test.ts`.

```
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

Add language specifier to fenced code block.

Static analysis flagged this code block as missing a language specifier.

📝 Proposed fix
-```
+```text
 libs/system-admin-pages/src/pages/
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 75-75: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Comment on lines +84 to +85
- LaunchDarkly flag key: `third-party-subscriptions-radio-buttons` (false = dropdown, true = radio buttons)
- `CATH_LD_KEY` environment variable must be set for LaunchDarkly to function; the feature flag defaults to `false` (radio button variant) when unavailable.
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

Same inconsistency as in ticket.md regarding feature flag default.

Line 84 says false = dropdown, but line 85 says it defaults to "radio button variant". Please align with the correction in ticket.md.

Comment on lines +84 to +85
* Manage subscriptions - Rheoli tanysgrifiadau
* Delete user - Dileu Defnyddiwr
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

Inconsistent documentation about feature flag default behaviour.

Line 84 states false = dropdown, true = radio buttons, but line 85 says the flag "defaults to false (radio button variant)". These contradict each other.

📝 Suggested fix
-- `CATH_LD_KEY` environment variable must be set for LaunchDarkly to function; the feature flag defaults to `false` (radio button variant) when unavailable.
+- `CATH_LD_KEY` environment variable must be set for LaunchDarkly to function; the feature flag defaults to `false` (dropdown variant) when unavailable.

Comment on lines +163 to +166
test("user can delete a third party user @nightly", async ({ page }) => {
// Create a test user directly in DB
const testUser = await prisma.thirdPartyUser.create({ data: { name: `Delete Test Corp ${Date.now()}` } });

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

Add user to cleanup list for resilience.

If this test fails before the deletion step, the created user will remain in the database. Track it in createdUserIds for cleanup.

🛡️ Proposed fix
   test("user can delete a third party user `@nightly`", async ({ page }) => {
     // Create a test user directly in DB
     const testUser = await prisma.thirdPartyUser.create({ data: { name: `Delete Test Corp ${Date.now()}` } });
+    createdUserIds.push(testUser.id);
 
     await page.goto(`/third-party-users/${testUser.id}`);

Comment on lines +10 to +17
model ThirdPartyUser {
id String @id @default(cuid())
name String @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at")
subscriptions ThirdPartySubscription[]

@@map("third_party_user")
}
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

Consider adding a unique constraint on name.

Without a unique constraint, the idempotent create logic in third-party-user-service.ts is vulnerable to race conditions, potentially creating duplicate users with the same name.

♻️ Suggested change
 model ThirdPartyUser {
   id            String                   `@id` `@default`(cuid())
-  name          String                   `@db.VarChar`(255)
+  name          String                   `@unique` `@db.VarChar`(255)
   createdAt     DateTime                 `@default`(now()) `@map`("created_at")
   subscriptions ThirdPartySubscription[]

   @@map("third_party_user")
 }
📝 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
model ThirdPartyUser {
id String @id @default(cuid())
name String @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at")
subscriptions ThirdPartySubscription[]
@@map("third_party_user")
}
model ThirdPartyUser {
id String `@id` `@default`(cuid())
name String `@unique` `@db.VarChar`(255)
createdAt DateTime `@default`(now()) `@map`("created_at")
subscriptions ThirdPartySubscription[]
@@map("third_party_user")
}

alexbottenberg and others added 6 commits March 20, 2026 14:09
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Node.js 24 throws if the --env-file path doesn't exist. In CI there is no
.env file so the web server process exits immediately with no output, causing
Playwright's webServer timeout. server.ts already loads .env via dotenv.config()
so the flag is redundant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

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.

[VIBE-313] Third Party User Management - Future

1 participant