Skip to content

Feature/582 Update reference data upload to support multiple provenances - #660

Merged
junaidiqbalmoj merged 9 commits into
masterfrom
feature/582-reference-data-upload
Jun 5, 2026
Merged

Feature/582 Update reference data upload to support multiple provenances#660
junaidiqbalmoj merged 9 commits into
masterfrom
feature/582-reference-data-upload

Conversation

@KianKwa

@KianKwa KianKwa commented May 21, 2026

Copy link
Copy Markdown
Contributor

Jira link

#582

Change description

Update reference data upload to support multiple provenances

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 provenance-aware location reference tracking for multiple external sources (SNL, Common Platform, CP-CaTH, PDDA)
    • Reference data uploads now support capturing location source metadata
    • Upload summary page displays provenance details for each location reference
  • Documentation

    • Added implementation planning documentation for provenance feature support

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@KianKwa, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 30 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba6196bd-6ee0-4892-b6a3-dff467d9cd9a

📥 Commits

Reviewing files that changed from the base of the PR and between 1d21571 and 923e752.

📒 Files selected for processing (2)
  • libs/system-admin-pages/src/list-type/queries.test.ts
  • libs/system-admin-pages/src/list-type/queries.ts
📝 Walkthrough

Walkthrough

This PR implements location reference provenance tracking across the reference data upload system and blob ingestion APIs. It adds a new database table, CSV parsing and validation, location resolution via external references, and updates summary UI to display provenance details alongside validation errors.

Changes

Location Provenance Reference Data

Layer / File(s) Summary
Database schema and location reference model
apps/postgres/prisma/migrations/20260520113517_add_location_reference/*, libs/postgres-prisma/prisma/schema/location.prisma
Introduces location_reference table with provenance fields and composite uniqueness constraint; Prisma schema adds LocationReference model with cascading delete, and optional locationType to ListType.
Provenance and type enumerations
libs/location/src/repository/location-reference-model.ts, libs/publication/src/provenance.ts, libs/list-types/common/src/*, libs/list-types/*/src/pages/*
Location module exports provenance and type constants (SNL, COMMON_PLATFORM, CP_CATH, PDDA); publication enum adds CP_CATH and PDDA, removes XHIBIT; all list-type pages import shared provenance labels, removing local XHIBIT definitions.
CSV data model and provenance parsing
libs/system-admin-pages/src/reference-data-upload/model.ts, libs/system-admin-pages/src/reference-data-upload/parsers/csv-parser.ts, libs/system-admin-pages/src/reference-data-upload/parsers/csv-parser.test.ts
Extends CsvRow and ParsedLocationData with provenance columns; parser splits semicolon-delimited fields into locationReferences array; comprehensive test coverage for multiple references and edge cases.
CSV validation rules for provenance
libs/system-admin-pages/src/reference-data-upload/validation/validation.ts, libs/system-admin-pages/src/reference-data-upload/validation/validation.test.ts
Validates locationReferences presence, required fields, and allowed values; detects in-file (provenance, provenanceLocationId) duplicates; reworks location name duplicates to track by locationId; extensive test scenarios covering all validation paths.
Location reference queries and seeding
libs/location/src/repository/location-reference-queries.ts, libs/location/src/index.ts, libs/location/src/seed-data.ts, libs/location/src/seed-data.test.ts
Implements getLocationByProvenanceLocationId for external reference resolution with optional type filtering; exports re-exports and query function; seed data creates SNL references for all locations with computed provenance IDs.
Upload repository and CSV export
libs/system-admin-pages/src/reference-data-upload/repository/upload-repository.ts, libs/system-admin-pages/src/reference-data-upload/repository/upload-repository.test.ts, libs/system-admin-pages/src/reference-data-upload/services/download-service.ts, libs/system-admin-pages/src/reference-data-upload/services/download-service.test.ts
Merges rows by locationId to consolidate references before upserting; deletes and recreates locationReference records per location; exports CSV with provenance columns concatenated; test coverage for aggregation and empty reference scenarios.
Blob ingestion validation with external provenance resolution
libs/api/src/blob-ingestion/validation.ts, libs/api/src/blob-ingestion/validation.test.ts
Branches location resolution by provenance type: external provenances resolve via getLocationByProvenanceLocationId with optional type filter; internal provenances use numeric parsing and getLocationById; returns resolvedLocationId; comprehensive test coverage for all paths.
Blob ingestion service and model integration
libs/api/src/blob-ingestion/repository/model.ts, libs/api/src/blob-ingestion/repository/service.ts, libs/api/src/blob-ingestion/repository/service.test.ts, libs/api/src/blob-ingestion/repository/queries.test.ts, vitest.setup.ts, e2e-tests/tests/api/blob-ingestion.spec.ts, e2e-tests/tests/api/blob-ingestion-notifications.spec.ts, libs/publication/src/repository/queries.test.ts
Updates processBlobIngestion to compute locationId from validation.resolvedLocationId; passes resolved ID to artifact/publication; adds resolvedLocationId field to model; updates test fixtures and provenance mappings; adds locationReference.findFirst Vitest mock.
Reference data upload summary page and error rendering
libs/system-admin-pages/src/pages/reference-data-upload-summary/index.ts, libs/system-admin-pages/src/pages/reference-data-upload-summary/index.njk, libs/system-admin-pages/src/pages/reference-data-upload-summary/index.test.ts, libs/system-admin-pages/src/pages/reference-data-upload-summary/en.ts, libs/system-admin-pages/src/pages/reference-data-upload-summary/cy.ts, libs/system-admin-pages/src/pages/reference-data-upload/index.njk, libs/system-admin-pages/src/assets/css/reference-data-upload.scss
Displays preview and provenance columns alongside validation errors; renders GOV.UK error summary with linked errors; POST handler renders errors inline instead of redirecting; updates page labels for provenance headers; replaces custom CSS with GOV.UK classes.
Ticket documentation and implementation plan
docs/tickets/582/plan.md, docs/tickets/582/tasks.md, docs/tickets/582/ticket.md
Documentation outlines implementation scope, affected modules, CSV format changes, validation rules, publication processing logic, error handling, acceptance criteria, and open clarifications.

Possibly related issues

  • Issue #582: This PR directly implements the ticket requirements: location reference model, provenance fields, location type enum, CSV parsing/validation, external provenance location resolution, and display updates.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly describes the main change: updating reference data upload to support multiple provenances, which aligns with the comprehensive changeset across database schema, validation, CSV parsing, and blob ingestion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/582-reference-data-upload

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 May 21, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright E2E Test Results

84 tests   51 ✅  7m 10s ⏱️
33 suites  33 💤
 1 files     0 ❌

Results for commit 923e752.

♻️ 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: 10

Caution

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

⚠️ Outside diff range comments (1)
libs/system-admin-pages/src/pages/reference-data-upload-summary/index.njk (1)

14-30: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the govukErrorSummary macro rather than hand-rolled error summary markup.

The template imports govukErrorSummary but renders a custom block. Please switch to the macro for consistency and accessibility compliance.

Proposed fix
 {% if hasErrors %}
-  <div class="govuk-error-summary" data-module="govuk-error-summary">
-    <div role="alert">
-      <h2 class="govuk-error-summary__title">{{ errorSummaryTitle }}</h2>
-      <div class="govuk-error-summary__body">
-        <p class="govuk-body">{{ errorTitle }}</p>
-        <ul class="govuk-list govuk-error-summary__list">
-          {% for error in errors %}
-            <li>
-              <a href="{{ error.href }}">{{ error.text }}</a>
-            </li>
-          {% endfor %}
-        </ul>
-      </div>
-    </div>
-  </div>
+  {{ govukErrorSummary({
+    titleText: errorSummaryTitle,
+    errorList: errors
+  }) }}
+  <p class="govuk-body">{{ errorTitle }}</p>
 {% endif %}

As per coding guidelines, "Nunjucks templates must extend layouts/base-template.njk, use govuk macros from govuk/components, and include error handling with govukErrorSummary."

🧹 Nitpick comments (1)
libs/system-admin-pages/src/pages/reference-data-upload-summary/index.test.ts (1)

186-197: ⚡ Quick win

Lock the no-write contract on validation failure.

Add an explicit assertion that persistence is not triggered in this path.

Proposed test assertion
       expect(mockResponse.redirect).not.toHaveBeenCalled();
+      expect(repository.upsertLocations).not.toHaveBeenCalled();
       expect(mockRequest.session!.uploadData).toBeDefined();

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 66751057-2bcd-4f21-900b-b97b0c5d2830

📥 Commits

Reviewing files that changed from the base of the PR and between 447a418 and 4ff5ee4.

⛔ Files ignored due to path filters (1)
  • e2e-tests/fixtures/test-reference-data.csv is excluded by !**/*.csv
📒 Files selected for processing (46)
  • apps/postgres/prisma/migrations/20260520113517_add_location_reference/migration.sql
  • docs/tickets/582/plan.md
  • docs/tickets/582/tasks.md
  • docs/tickets/582/ticket.md
  • e2e-tests/tests/api/blob-ingestion-notifications.spec.ts
  • libs/api/src/blob-ingestion/repository/model.ts
  • libs/api/src/blob-ingestion/repository/queries.test.ts
  • libs/api/src/blob-ingestion/repository/service.test.ts
  • libs/api/src/blob-ingestion/repository/service.ts
  • libs/api/src/blob-ingestion/validation.test.ts
  • libs/api/src/blob-ingestion/validation.ts
  • libs/list-types/administrative-court-daily-cause-list/src/pages/cy.ts
  • libs/list-types/administrative-court-daily-cause-list/src/pages/en.ts
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/cy.ts
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/en.ts
  • libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/cy.ts
  • libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/en.ts
  • libs/list-types/london-administrative-court-daily-cause-list/src/pages/cy.ts
  • libs/list-types/london-administrative-court-daily-cause-list/src/pages/en.ts
  • libs/list-types/rcj-standard-daily-cause-list/src/pages/cy.ts
  • libs/list-types/rcj-standard-daily-cause-list/src/pages/en.ts
  • libs/location/prisma/schema.prisma
  • libs/location/src/index.ts
  • libs/location/src/repository/location-reference-model.ts
  • libs/location/src/repository/location-reference-queries.ts
  • libs/location/src/seed-data.test.ts
  • libs/location/src/seed-data.ts
  • libs/publication/src/provenance.ts
  • libs/publication/src/repository/queries.test.ts
  • libs/system-admin-pages/src/assets/css/reference-data-upload.scss
  • libs/system-admin-pages/src/pages/reference-data-upload-summary/cy.ts
  • libs/system-admin-pages/src/pages/reference-data-upload-summary/en.ts
  • libs/system-admin-pages/src/pages/reference-data-upload-summary/index.njk
  • libs/system-admin-pages/src/pages/reference-data-upload-summary/index.test.ts
  • libs/system-admin-pages/src/pages/reference-data-upload-summary/index.ts
  • libs/system-admin-pages/src/pages/reference-data-upload/index.njk
  • libs/system-admin-pages/src/reference-data-upload/model.ts
  • libs/system-admin-pages/src/reference-data-upload/parsers/csv-parser.test.ts
  • libs/system-admin-pages/src/reference-data-upload/parsers/csv-parser.ts
  • libs/system-admin-pages/src/reference-data-upload/repository/upload-repository.test.ts
  • libs/system-admin-pages/src/reference-data-upload/repository/upload-repository.ts
  • libs/system-admin-pages/src/reference-data-upload/services/download-service.test.ts
  • libs/system-admin-pages/src/reference-data-upload/services/download-service.ts
  • libs/system-admin-pages/src/reference-data-upload/validation/validation.test.ts
  • libs/system-admin-pages/src/reference-data-upload/validation/validation.ts
  • vitest.setup.ts
💤 Files with no reviewable changes (11)
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/cy.ts
  • libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/cy.ts
  • libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/en.ts
  • libs/list-types/rcj-standard-daily-cause-list/src/pages/en.ts
  • libs/list-types/administrative-court-daily-cause-list/src/pages/cy.ts
  • libs/list-types/rcj-standard-daily-cause-list/src/pages/cy.ts
  • libs/system-admin-pages/src/assets/css/reference-data-upload.scss
  • libs/list-types/london-administrative-court-daily-cause-list/src/pages/en.ts
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/en.ts
  • libs/list-types/london-administrative-court-daily-cause-list/src/pages/cy.ts
  • libs/list-types/administrative-court-daily-cause-list/src/pages/en.ts

Comment thread docs/tickets/582/plan.md
**Enums** (kept as TypeScript string literal unions, not Prisma enums, to remain consistent with how `provenance` is already stored as a plain `String` column in the `artefact` and `list_types` tables):

```typescript
// libs/location/src/location-reference/model.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 | ⚡ Quick win

Align documented file paths with the implemented module path.

Line 57 and Line 283 reference libs/location/src/location-reference/model.ts, but the stack context for this PR points to libs/location/src/repository/location-reference-model.ts. Please update one side so the ticket plan matches the actual code locations.

Also applies to: 283-285

Comment thread docs/tickets/582/plan.md
Comment on lines +279 to +304
```
libs/location/
prisma/schema.prisma MODIFY - add LocationReference model, locationType to ListType
src/location-reference/
model.ts NEW - LOCATION_REFERENCE_PROVENANCES, LOCATION_REFERENCE_TYPES constants
queries.ts NEW - getLocationByProvenanceLocationId()
src/index.ts MODIFY - export getLocationByProvenanceLocationId

apps/postgres/prisma/migrations/
<timestamp>_add_location_reference/migration.sql NEW
<timestamp>_add_list_type_location_type/migration.sql NEW

libs/system-admin-pages/src/reference-data-upload/
model.ts MODIFY - add provenance fields to CsvRow, ParsedLocationData
parsers/csv-parser.ts MODIFY - add PROVENANCE, PROVENANCE_LOCATION_ID, PROVENANCE_LOCATION_TYPE to REQUIRED_HEADERS; parse and pass through
validation/validation.ts MODIFY - fix name-uniqueness logic; add provenance field validation
repository/upload-repository.ts MODIFY - upsert location_reference rows

libs/api/src/blob-ingestion/
repository/model.ts MODIFY - add resolvedLocationId to BlobValidationResult
repository/service.ts MODIFY - use resolvedLocationId for locationId passed to createArtefact/processPublication
validation.ts MODIFY - add external provenance resolution; extend ALLOWED_PROVENANCES

libs/publication/src/
provenance.ts MODIFY - add CP_CATH, PDDA to Provenance enum and PROVENANCE_LABELS
```

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 | ⚡ Quick win

Add a language tag to the fenced code block.

Line 279 opens a fenced code block without a language, which triggers markdown linting (MD040). Please annotate it (for example, text) to keep docs lint-clean.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

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

(MD040, fenced-code-language)

Comment on lines +13 to +15
- PROVENANCE
- PROVENANCE LOCATION ID
- PROVENANCE LOCATION TYPE

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 | ⚡ Quick win

Use the exact CSV header names consistently across ticket docs.

Lines 13–15 use spaced names (PROVENANCE LOCATION ID, PROVENANCE LOCATION TYPE), while the plan/tasks and parser changes use underscore headers (PROVENANCE_LOCATION_ID, PROVENANCE_LOCATION_TYPE). Please standardise these to avoid ambiguity in implementation and test data.

🧰 Tools
🪛 LanguageTool

[duplication] ~13-~13: Possible typo: you repeated a word.
Context: ...ant model classes and the database: - PROVENANCE - PROVENANCE LOCATION ID - PROVENANCE LOCATION TYP...

(ENGLISH_WORD_REPEAT_RULE)

Comment on lines +46 to +47
regions: loc.locationRegions.map((lr: any) => lr.region.regionId),
subJurisdictions: loc.locationSubJurisdictions.map((lsj: any) => lsj.subJurisdiction.subJurisdictionId)

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify explicit `any` usage in this module.
rg -nP --type=ts '\b:\s*any\b' libs/location/src/repository/location-reference-queries.ts

Repository: hmcts/cath-service

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/location/src/repository/location-reference-queries.ts"

echo "== any occurrences =="
rg -nP --type=ts '\b:\s*any\b' "$FILE" || true

echo
echo "== context around lines 35-70 =="
nl -ba "$FILE" | sed -n '30,75p'

Repository: hmcts/cath-service

Length of output: 365


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/location/src/repository/location-reference-queries.ts"

echo "== file info =="
wc -l "$FILE"
echo

echo "== any occurrences (typed) =="
rg -nP --type=ts '\b:\s*any\b' "$FILE" || true
echo

echo "== any occurrences (broader) =="
rg -n --type=ts '\bany\b' "$FILE" || true
echo

echo "== context around lines 35-70 =="
cat -n "$FILE" | sed -n '35,70p'

Repository: hmcts/cath-service

Length of output: 1091


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/location/src/repository/location-reference-queries.ts"
cat -n "$FILE"

Repository: hmcts/cath-service

Length of output: 1618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the imported Location type definition
FILE_JS="libs/location/src/repository/model.js"
FILE_TS="libs/location/src/repository/model.ts"

if [ -f "$FILE_JS" ]; then
  echo "== cat $FILE_JS =="
  cat -n "$FILE_JS"
elif [ -f "$FILE_TS" ]; then
  echo "== cat $FILE_TS =="
  cat -n "$FILE_TS"
else
  echo "Neither $FILE_JS nor $FILE_TS exists; searching for Location type..."
  fd -e ts -e tsx -e js -e jsx -e njk "model" libs/location/src/repository | head -n 50
  fd -e ts -e tsx "export (type|interface) Location" libs/location/src/repository
fi

Repository: hmcts/cath-service

Length of output: 1880


Remove explicit any from relation mapping callbacks (lines 46-47).
Those callback parameters bypass strict typing; rely on the Prisma-inferred types instead.

Suggested diff
-    regions: loc.locationRegions.map((lr: any) => lr.region.regionId),
-    subJurisdictions: loc.locationSubJurisdictions.map((lsj: any) => lsj.subJurisdiction.subJurisdictionId)
+    regions: loc.locationRegions.map((lr) => lr.region.regionId),
+    subJurisdictions: loc.locationSubJurisdictions.map((lsj) => lsj.subJurisdiction.subJurisdictionId)
📝 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
regions: loc.locationRegions.map((lr: any) => lr.region.regionId),
subJurisdictions: loc.locationSubJurisdictions.map((lsj: any) => lsj.subJurisdiction.subJurisdictionId)
regions: loc.locationRegions.map((lr) => lr.region.regionId),
subJurisdictions: loc.locationSubJurisdictions.map((lsj) => lsj.subJurisdiction.subJurisdictionId)

Comment on lines +16 to +20
provenanceHeader: "Welsh placeholder",
provenanceLocationIdHeader: "Welsh placeholder",
provenanceLocationTypeHeader: "Welsh placeholder",
errorSummaryTitle: "Welsh placeholder",
errorTitle: "Welsh placeholder",
errorMessage: "Welsh placeholder"
errorTitle: "Welsh placeholder"

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 | ⚡ Quick win

Replace new Welsh placeholder labels with real translations.

These values are user-facing and currently render as placeholders, which weakens Welsh-language support.

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."

Comment on lines +19 to +25
async function buildPreviewData(data: any[], page: number) {
const enrichedData = await enrichLocationData(data);
const itemsPerPage = 10;
const totalItems = enrichedData.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startIndex = (page - 1) * itemsPerPage;
const paginatedData = enrichedData.slice(startIndex, startIndex + itemsPerPage);

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 | ⚡ Quick win

Normalise and clamp page before slicing and pagination metadata.

NaN, negative, or out-of-range pages can render empty preview data despite valid rows.

Suggested guard inside helper
 async function buildPreviewData(data: any[], page: number) {
   const enrichedData = await enrichLocationData(data);
   const itemsPerPage = 10;
   const totalItems = enrichedData.length;
   const totalPages = Math.ceil(totalItems / itemsPerPage);
-  const startIndex = (page - 1) * itemsPerPage;
+  const maxPage = Math.max(totalPages, 1);
+  const safePage = Number.isInteger(page) && page > 0 ? Math.min(page, maxPage) : 1;
+  const startIndex = (safePage - 1) * itemsPerPage;
   const paginatedData = enrichedData.slice(startIndex, startIndex + itemsPerPage);
@@
-      current: i === page
+      current: i === safePage
@@
-      previous: page > 1 ? { href: `/reference-data-upload-summary?page=${page - 1}` } : undefined,
-      next: page < totalPages ? { href: `/reference-data-upload-summary?page=${page + 1}` } : undefined
+      previous: safePage > 1 ? { href: `/reference-data-upload-summary?page=${safePage - 1}` } : undefined,
+      next: safePage < totalPages ? { href: `/reference-data-upload-summary?page=${safePage + 1}` } : undefined

Also applies to: 36-44

});
}

async function buildPreviewData(data: any[], page: number) {

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify available row types and related function signatures before replacing `any[]`.
rg -n "export (type|interface) (CsvRow|ParsedLocationData)" libs/system-admin-pages/src/reference-data-upload/model.ts
rg -n "buildPreviewData|enrichLocationData\\(" libs/system-admin-pages/src/pages/reference-data-upload-summary/index.ts libs/system-admin-pages/src/reference-data-upload/services/enrichment-service.ts

Repository: hmcts/cath-service

Length of output: 870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show helper implementation and its usages (pagination + typing)
sed -n '1,200p' libs/system-admin-pages/src/pages/reference-data-upload-summary/index.ts

# Show the row types and how parsed data is typed
sed -n '1,120p' libs/system-admin-pages/src/reference-data-upload/model.ts

# Show enrichment function signature (expected input type)
sed -n '1,160p' libs/system-admin-pages/src/reference-data-upload/services/enrichment-service.ts

Repository: hmcts/cath-service

Length of output: 7331


Fix type-safety, pagination bounds, and Welsh locale handling in reference-data-upload-summary

  • Replace buildPreviewData(data: any[], page: number) with buildPreviewData(data: ParsedLocationData[], page: number) (it currently passes any[] into enrichLocationData, which expects ParsedLocationData[]).
  • Normalise/clamp page (handle NaN, < 1, and > totalPages) before calculating startIndex/pagination state.
  • Remove hardcoded locale = "en" in both GET and the POST validation error render path; derive locale from ?lng=cy (so the renderer receives matching en/cy content for Welsh).

Comment on lines +114 to +124
const locale = "en";
const t = getTranslations(locale);
const preview = await buildPreviewData(parseResult.data, 1);
return res.render("reference-data-upload-summary/index", {
...t,
...preview,
fileName: uploadData.fileName,
errors: validationErrors,
hasErrors: true,
locale
});

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 | ⚡ Quick win

Do not hardcode English in POST validation-error rendering.

This path ignores ?lng=cy and renders Welsh users back in English after submit errors.

Suggested locale selection
-    const locale = "en";
+    const locale = req.query.lng === "cy" ? "cy" : "en";

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."

📝 Committable suggestion

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

Suggested change
const locale = "en";
const t = getTranslations(locale);
const preview = await buildPreviewData(parseResult.data, 1);
return res.render("reference-data-upload-summary/index", {
...t,
...preview,
fileName: uploadData.fileName,
errors: validationErrors,
hasErrors: true,
locale
});
const locale = req.query.lng === "cy" ? "cy" : "en";
const t = getTranslations(locale);
const preview = await buildPreviewData(parseResult.data, 1);
return res.render("reference-data-upload-summary/index", {
...t,
...preview,
fileName: uploadData.fileName,
errors: validationErrors,
hasErrors: true,
locale
});

Comment on lines +10 to +12
existing.locationReferences = [...existing.locationReferences, ...row.locationReferences];
existing.subJurisdictionNames = [...new Set([...existing.subJurisdictionNames, ...row.subJurisdictionNames])];
existing.regionNames = [...new Set([...existing.regionNames, ...row.regionNames])];

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 | ⚡ Quick win

De-duplicate merged provenance references before createMany.

Concatenating references blindly can cause insert failures if duplicate reference tuples reach this layer (e.g., repeated rows for the same location). Please normalise by key before persisting.

Proposed fix
 function mergeByLocationId(data: ParsedLocationData[]): ParsedLocationData[] {
   const merged = new Map<number, ParsedLocationData>();
   for (const row of data) {
     const existing = merged.get(row.locationId);
     if (existing) {
-      existing.locationReferences = [...existing.locationReferences, ...row.locationReferences];
+      const refByKey = new Map(
+        existing.locationReferences.map((ref) => [
+          `${ref.provenance}::${ref.provenanceLocationId}::${ref.provenanceLocationType}`,
+          ref
+        ])
+      );
+      for (const ref of row.locationReferences) {
+        refByKey.set(
+          `${ref.provenance}::${ref.provenanceLocationId}::${ref.provenanceLocationType}`,
+          ref
+        );
+      }
+      existing.locationReferences = [...refByKey.values()];
       existing.subJurisdictionNames = [...new Set([...existing.subJurisdictionNames, ...row.subJurisdictionNames])];
       existing.regionNames = [...new Set([...existing.regionNames, ...row.regionNames])];
     } else {
       merged.set(row.locationId, { ...row, locationReferences: [...row.locationReferences] });
     }

Also applies to: 110-118

Comment on lines +131 to 143
for (const ref of row.locationReferences) {
if (ref.provenance && ref.provenanceLocationId) {
const provenanceKey = `${ref.provenance}::${ref.provenanceLocationId}`;
if (provenanceKeys.has(provenanceKey)) {
errors.push({
text: `Duplicate (PROVENANCE, PROVENANCE_LOCATION_ID) combination "${ref.provenance}, ${ref.provenanceLocationId}" in the file (rows ${provenanceKeys.get(provenanceKey)} and ${rowNumber})`,
href: "#file"
});
} else {
provenanceKeys.set(provenanceKey, rowNumber);
}
}
}

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 | ⚡ Quick win

Add database conflict validation for (PROVENANCE, PROVENANCE_LOCATION_ID) before upload.

You only detect duplicate provenance keys within the CSV. The migration enforces global uniqueness in DB, so conflicts against existing rows can still fail later with a unique-constraint error. Add a pre-check against persisted location_reference rows (excluding same locationId) and return row-level validation errors.

Suggested approach
+  const provenanceKeyToRows = new Map<string, number[]>();
+  for (let i = 0; i < data.length; i++) {
+    const row = data[i];
+    const rowNumber = i + 1;
+    for (const ref of row.locationReferences) {
+      if (ref.provenance && ref.provenanceLocationId) {
+        const key = `${ref.provenance}::${ref.provenanceLocationId}`;
+        provenanceKeyToRows.set(key, [...(provenanceKeyToRows.get(key) ?? []), rowNumber]);
+      }
+    }
+  }
+
+  const existingReferences = await prisma.locationReference.findMany({
+    where: {
+      OR: [...provenanceKeyToRows.keys()].map((key) => {
+        const [provenance, provenanceLocationId] = key.split("::");
+        return { provenance, provenanceLocationId };
+      })
+    },
+    select: { provenance: true, provenanceLocationId: true, locationId: true }
+  });
+
+  for (const existing of existingReferences) {
+    const key = `${existing.provenance}::${existing.provenanceLocationId}`;
+    for (const rowNumber of provenanceKeyToRows.get(key) ?? []) {
+      const row = data[rowNumber - 1];
+      if (row.locationId !== existing.locationId) {
+        errors.push({
+          text: `Row ${rowNumber}: (PROVENANCE, PROVENANCE_LOCATION_ID) "${existing.provenance}, ${existing.provenanceLocationId}" already exists for a different location ID`,
+          href: "`#file`"
+        });
+      }
+    }
+  }

Also applies to: 170-218

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5f52b87-aebf-4c08-a462-12ca4af13599

📥 Commits

Reviewing files that changed from the base of the PR and between 4ff5ee4 and 0aa9d1f.

📒 Files selected for processing (14)
  • e2e-tests/tests/api/blob-ingestion.spec.ts
  • libs/list-types/administrative-court-daily-cause-list/src/pages/cy.ts
  • libs/list-types/administrative-court-daily-cause-list/src/pages/en.ts
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/cy.ts
  • libs/list-types/care-standards-tribunal-weekly-hearing-list/src/pages/en.ts
  • libs/list-types/common/src/index.ts
  • libs/list-types/common/src/locales/cy.ts
  • libs/list-types/common/src/locales/en.ts
  • libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/cy.ts
  • libs/list-types/court-of-appeal-civil-daily-cause-list/src/pages/en.ts
  • libs/list-types/london-administrative-court-daily-cause-list/src/pages/cy.ts
  • libs/list-types/london-administrative-court-daily-cause-list/src/pages/en.ts
  • libs/list-types/rcj-standard-daily-cause-list/src/pages/cy.ts
  • libs/list-types/rcj-standard-daily-cause-list/src/pages/en.ts

Comment on lines +236 to +255
test("submits publication using provenance location ID as court_id for SNL provenance @nightly", async ({ request }) => {
const token = await getApiAuthToken();

const response = await request.post(ENDPOINT, {
data: {
...validPayload,
court_id: "9001",
provenance: "SNL"
},
headers: {
Authorization: `Bearer ${token}`
}
});

// 201 = ingested and matched to a location, 200 = ingested but no matching location found
expect([200, 201]).toContain(response.status());
const body = await response.json();
expect(body.success).toBe(true);
expect(body.artefact_id).toBeDefined();
});

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 | ⚡ Quick win

Assertion is too permissive to catch the regression this test targets.

The test title says it verifies the provenance location ID is resolved as court_id for SNL, but accepting both 200 and 201 means the test passes even when the location lookup silently fails to match (200 path). If the SNL location seed for 9001 is missing or the resolution logic regresses, this test will go green instead of red.

Consider tightening to expect(response.status()).toBe(201), or alternatively asserting a body field that confirms location resolution actually happened.

🧪 Proposed tightening
-    // 201 = ingested and matched to a location, 200 = ingested but no matching location found
-    expect([200, 201]).toContain(response.status());
+    // 201 confirms the SNL provenance location ID was resolved to a known location.
+    expect(response.status()).toBe(201);
     const body = await response.json();
     expect(body.success).toBe(true);
     expect(body.artefact_id).toBeDefined();
📝 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
test("submits publication using provenance location ID as court_id for SNL provenance @nightly", async ({ request }) => {
const token = await getApiAuthToken();
const response = await request.post(ENDPOINT, {
data: {
...validPayload,
court_id: "9001",
provenance: "SNL"
},
headers: {
Authorization: `Bearer ${token}`
}
});
// 201 = ingested and matched to a location, 200 = ingested but no matching location found
expect([200, 201]).toContain(response.status());
const body = await response.json();
expect(body.success).toBe(true);
expect(body.artefact_id).toBeDefined();
});
test("submits publication using provenance location ID as court_id for SNL provenance `@nightly`", async ({ request }) => {
const token = await getApiAuthToken();
const response = await request.post(ENDPOINT, {
data: {
...validPayload,
court_id: "9001",
provenance: "SNL"
},
headers: {
Authorization: `Bearer ${token}`
}
});
// 201 confirms the SNL provenance location ID was resolved to a known location.
expect(response.status()).toBe(201);
const body = await response.json();
expect(body.success).toBe(true);
expect(body.artefact_id).toBeDefined();
});

@@ -1,3 +1,5 @@
import { provenanceLabelsCy as provenanceLabels } from "@hmcts/list-types-common";

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 | ⚡ Quick win

Build failing: test mock for @hmcts/list-types-common is missing provenanceLabelsCy.

Same root cause as the sibling court-of-appeal-civil-daily-cause-list failure — src/pages/index.test.ts mocks @hmcts/list-types-common without the new provenanceLabelsCy export, so importing cy.ts blows up under test. Extend the mock with importOriginal (or add the missing export explicitly) to unblock CI.

🔧 Suggested mock fix (apply in `src/pages/index.test.ts`)
vi.mock("`@hmcts/list-types-common`", async (importOriginal) => {
  const actual = await importOriginal<typeof import("`@hmcts/list-types-common`")>();
  return {
    ...actual,
    // keep any existing overrides here
  };
});

@@ -1,3 +1,5 @@
import { provenanceLabelsCy as provenanceLabels } from "@hmcts/list-types-common";

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 | ⚡ Quick win

Build failing: test mock for @hmcts/list-types-common is missing provenanceLabelsCy.

Per the CI failure on src/pages/index.test.ts, the vi.mock("@hmcts/list-types-common") in the test does not expose provenanceLabelsCy, so importing this module under test throws. Update the mock to include the new export (or use importOriginal) so the suite passes.

🔧 Suggested mock fix (apply in `src/pages/index.test.ts`)
vi.mock("`@hmcts/list-types-common`", async (importOriginal) => {
  const actual = await importOriginal<typeof import("`@hmcts/list-types-common`")>();
  return {
    ...actual,
    // keep any existing overrides here
  };
});

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

…ce-data-upload

# Conflicts:
#	libs/system-admin-pages/src/reference-data-upload/services/download-service.ts

@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.

Caution

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

⚠️ Outside diff range comments (1)
libs/postgres-prisma/prisma/schema/location.prisma (1)

83-93: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an index on locationId.

Line 90 adds the FK, but the model never indexes location_id. The new upload path mutates references per location, and cascade deletes will also hit this column; without an index those operations will degrade badly as the table grows.

Suggested fix
 model LocationReference {
   locationReferenceId    String `@id` `@default`(cuid()) `@map`("location_reference_id")
   locationId             Int    `@map`("location_id")
   provenance             String `@map`("provenance") `@db.VarChar`(50)
   provenanceLocationId   String `@map`("provenance_location_id") `@db.VarChar`(255)
   provenanceLocationType String `@map`("provenance_location_type") `@db.VarChar`(50)
 
   location Location `@relation`(fields: [locationId], references: [locationId], onDelete: Cascade)
 
+  @@index([locationId])
   @@unique([provenance, provenanceLocationId])
   @@map("location_reference")
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4de6fa42-29b3-4897-8133-c4d9109d007e

📥 Commits

Reviewing files that changed from the base of the PR and between 483c321 and 1d21571.

📒 Files selected for processing (10)
  • e2e-tests/tests/api/blob-ingestion-notifications.spec.ts
  • libs/api/src/blob-ingestion/repository/queries.test.ts
  • libs/api/src/blob-ingestion/validation.test.ts
  • libs/api/src/blob-ingestion/validation.ts
  • libs/list-types/common/src/index.ts
  • libs/location/src/index.ts
  • libs/location/src/seed-data.ts
  • libs/postgres-prisma/prisma/schema/location.prisma
  • libs/publication/src/repository/queries.test.ts
  • libs/system-admin-pages/src/reference-data-upload/services/download-service.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • e2e-tests/tests/api/blob-ingestion-notifications.spec.ts
  • libs/location/src/seed-data.ts
  • libs/system-admin-pages/src/reference-data-upload/services/download-service.ts
  • libs/api/src/blob-ingestion/repository/queries.test.ts
  • libs/publication/src/repository/queries.test.ts
  • libs/location/src/index.ts
  • libs/list-types/common/src/index.ts
  • libs/api/src/blob-ingestion/validation.test.ts
  • libs/api/src/blob-ingestion/validation.ts

@sonarqubecloud

sonarqubecloud Bot commented Jun 1, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@junaidiqbalmoj
junaidiqbalmoj merged commit cbe4731 into master Jun 5, 2026
45 of 46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System admin reference data upload - Backend logic update

3 participants