From 264c81a3ecb0c79c52da24cb3b614d055aba551b Mon Sep 17 00:00:00 2001 From: junaidiqbalmoj <84805836+junaidiqbalmoj@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:29:31 +0000 Subject: [PATCH 1/4] Add location data, Provenance to Lists and fix bug --- libs/list-types/common/src/mock-list-types.ts | 9 +++++ libs/location/src/seed-data.ts | 35 +++++++++++++++++++ .../services/download-service.ts | 2 +- package.json | 3 +- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/libs/list-types/common/src/mock-list-types.ts b/libs/list-types/common/src/mock-list-types.ts index 99f46a720..1a1def3b4 100644 --- a/libs/list-types/common/src/mock-list-types.ts +++ b/libs/list-types/common/src/mock-list-types.ts @@ -3,6 +3,7 @@ export interface ListType { name: string; englishFriendlyName: string; welshFriendlyName: string; + provenance: string; urlPath?: string; } @@ -12,6 +13,7 @@ export const mockListTypes: ListType[] = [ name: "CIVIL_DAILY_CAUSE_LIST", englishFriendlyName: "Civil Daily Cause List", welshFriendlyName: "Civil Daily Cause List", + provenance: "CFT_IDAM", urlPath: "civil-daily-cause-list" }, { @@ -19,6 +21,7 @@ export const mockListTypes: ListType[] = [ name: "FAMILY_DAILY_CAUSE_LIST", englishFriendlyName: "Family Daily Cause List", welshFriendlyName: "Family Daily Cause List", + provenance: "CFT_IDAM", urlPath: "family-daily-cause-list" }, { @@ -26,6 +29,7 @@ export const mockListTypes: ListType[] = [ name: "CRIME_DAILY_LIST", englishFriendlyName: "Crime Daily List", welshFriendlyName: "Crime Daily List", + provenance: "CFT_IDAM", urlPath: "crime-daily-list" }, { @@ -33,6 +37,7 @@ export const mockListTypes: ListType[] = [ name: "MAGISTRATES_PUBLIC_LIST", englishFriendlyName: "Magistrates Public List", welshFriendlyName: "Magistrates Public List", + provenance: "CFT_IDAM", urlPath: "magistrates-public-list" }, { @@ -40,6 +45,7 @@ export const mockListTypes: ListType[] = [ name: "CROWN_WARNED_LIST", englishFriendlyName: "Crown Warned List", welshFriendlyName: "Crown Warned List", + provenance: "CFT_IDAM", urlPath: "crown-warned-list" }, { @@ -47,6 +53,7 @@ export const mockListTypes: ListType[] = [ name: "CROWN_DAILY_LIST", englishFriendlyName: "Crown Daily List", welshFriendlyName: "Crown Daily List", + provenance: "CFT_IDAM", urlPath: "crown-daily-list" }, { @@ -54,6 +61,7 @@ export const mockListTypes: ListType[] = [ name: "CROWN_FIRM_LIST", englishFriendlyName: "Crown Firm List", welshFriendlyName: "Crown Firm List", + provenance: "CFT_IDAM", urlPath: "crown-firm-list" }, { @@ -61,6 +69,7 @@ export const mockListTypes: ListType[] = [ name: "CIVIL_AND_FAMILY_DAILY_CAUSE_LIST", englishFriendlyName: "Civil and Family Daily Cause List", welshFriendlyName: "Rhestr Achos Dyddiol Sifil a Theulu", + provenance: "CFT_IDAM", urlPath: "civil-and-family-daily-cause-list" } ]; diff --git a/libs/location/src/seed-data.ts b/libs/location/src/seed-data.ts index 0b7945ad4..0e0a573d0 100644 --- a/libs/location/src/seed-data.ts +++ b/libs/location/src/seed-data.ts @@ -1,7 +1,42 @@ import { prisma } from "@hmcts/postgres"; import { locationData } from "./location-data.js"; +async function shouldSeed(): Promise { + // Only seed in local development, not in CI or production + if (process.env.NODE_ENV === "production") { + console.log("Skipping seed: NODE_ENV is production"); + return false; + } + + if (process.env.CI === "true") { + console.log("Skipping seed: Running in CI environment"); + return false; + } + + // Check if tables are empty + const regionCount = await prisma.region.count(); + const jurisdictionCount = await prisma.jurisdiction.count(); + const locationCount = await prisma.location.count(); + + // Only seed if all tables are empty + const isEmpty = regionCount === 0 && jurisdictionCount === 0 && locationCount === 0; + + if (!isEmpty) { + console.log("Skipping seed: Tables already contain data"); + return false; + } + + return true; +} + export async function seedLocationData() { + console.log("Checking if location data seeding is needed..."); + + const needsSeeding = await shouldSeed(); + if (!needsSeeding) { + return; + } + console.log("Seeding location reference data..."); // Seed regions diff --git a/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts b/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts index c5b8f8e60..9e2de5eb0 100644 --- a/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts +++ b/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts @@ -1,5 +1,5 @@ import { prisma } from "@hmcts/postgres"; -import * as Papa from "papaparse"; +import Papa from "papaparse"; export async function generateReferenceDataCsv(): Promise { try { diff --git a/package.json b/package.json index 425af903e..9c142ba0b 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ "dev:api": "yarn workspace @hmcts/api run dev", "dev:web": "yarn workspace @hmcts/web run dev", "dev:web:nowatch": "yarn workspace @hmcts/web run dev:nowatch", - "dev:up": "docker compose up -d && yarn db:migrate:dev", + "dev:up": "docker compose up -d && yarn db:migrate:dev && yarn db:seed", "dev:down": "docker compose down", "db:migrate": "yarn workspace @hmcts/postgres run migrate", + "db:seed": "tsx libs/location/src/seed-data.ts", "db:migrate:dev": "yarn workspace @hmcts/postgres run migrate:dev", "db:generate": "yarn workspace @hmcts/postgres run generate", "db:studio": "yarn workspace @hmcts/postgres run studio", From 556d3cf196dcc5eddcdb2df4ef9ea79e755a2dd2 Mon Sep 17 00:00:00 2001 From: junaidiqbalmoj <84805836+junaidiqbalmoj@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:35:00 +0000 Subject: [PATCH 2/4] fix build --- package.json | 3 ++- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9c142ba0b..1fabbb059 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "resolutions": { "vite": "7.2.4", "glob": "13.0.0", - "body-parser": "2.2.1" + "body-parser": "2.2.1", + "node-forge": "1.3.2" }, "dependencies": { "@microsoft/microsoft-graph-client": "3.0.7", diff --git a/yarn.lock b/yarn.lock index 7b088b238..a52dd8546 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5372,10 +5372,10 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1.2.1": - version: 1.3.1 - resolution: "node-forge@npm:1.3.1" - checksum: 10c0/e882819b251a4321f9fc1d67c85d1501d3004b4ee889af822fd07f64de3d1a8e272ff00b689570af0465d65d6bf5074df9c76e900e0aff23e60b847f2a46fbe8 +"node-forge@npm:1.3.2": + version: 1.3.2 + resolution: "node-forge@npm:1.3.2" + checksum: 10c0/1def35652c93a588718a6d0d0b4f33e3e7de283aa6f4c00d01d1605d6ccce23fb3b59bcbfb6434014acd23a251cfcc2736052b406f53d94e1b19c09d289d0176 languageName: node linkType: hard From ccba81c1fe4b1c1ffdaa75bdb4264d3200481b4d Mon Sep 17 00:00:00 2001 From: junaidiqbalmoj <84805836+junaidiqbalmoj@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:41:53 +0000 Subject: [PATCH 3/4] improve code coverage --- libs/location/src/seed-data.test.ts | 371 ++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 libs/location/src/seed-data.test.ts diff --git a/libs/location/src/seed-data.test.ts b/libs/location/src/seed-data.test.ts new file mode 100644 index 000000000..61ece230b --- /dev/null +++ b/libs/location/src/seed-data.test.ts @@ -0,0 +1,371 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the prisma client +const mockPrisma = { + region: { + count: vi.fn(), + upsert: vi.fn() + }, + jurisdiction: { + count: vi.fn(), + upsert: vi.fn() + }, + subJurisdiction: { + upsert: vi.fn() + }, + location: { + count: vi.fn(), + upsert: vi.fn() + }, + locationRegion: { + deleteMany: vi.fn(), + createMany: vi.fn() + }, + locationSubJurisdiction: { + deleteMany: vi.fn(), + createMany: vi.fn() + } +}; + +vi.mock("@hmcts/postgres", () => ({ + prisma: mockPrisma +})); + +// Mock location data +const mockLocationData = { + regions: [ + { regionId: 1, name: "Test Region 1", welshName: "Rhanbarth Prawf 1" }, + { regionId: 2, name: "Test Region 2", welshName: "Rhanbarth Prawf 2" } + ], + jurisdictions: [ + { jurisdictionId: 1, name: "Test Jurisdiction 1", welshName: "Awdurdodaeth Prawf 1" }, + { jurisdictionId: 2, name: "Test Jurisdiction 2", welshName: "Awdurdodaeth Prawf 2" } + ], + subJurisdictions: [ + { + subJurisdictionId: 1, + name: "Test Sub-Jurisdiction 1", + welshName: "Is-awdurdodaeth Prawf 1", + jurisdictionId: 1 + }, + { + subJurisdictionId: 2, + name: "Test Sub-Jurisdiction 2", + welshName: "Is-awdurdodaeth Prawf 2", + jurisdictionId: 2 + } + ], + locations: [ + { + locationId: 1, + name: "Test Location 1", + welshName: "Lleoliad Prawf 1", + regions: [1], + subJurisdictions: [1] + }, + { + locationId: 2, + name: "Test Location 2", + welshName: "Lleoliad Prawf 2", + regions: [2], + subJurisdictions: [2] + }, + { + locationId: 3, + name: "Test Location 3", + welshName: "Lleoliad Prawf 3", + regions: [], + subJurisdictions: [] + } + ] +}; + +vi.mock("./location-data.js", () => ({ + locationData: mockLocationData +})); + +describe("seed-data", () => { + let originalEnv: NodeJS.ProcessEnv; + let consoleLogSpy: ReturnType; + + beforeEach(() => { + originalEnv = { ...process.env }; + consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + vi.clearAllMocks(); + }); + + afterEach(() => { + process.env = originalEnv; + consoleLogSpy.mockRestore(); + }); + + describe("shouldSeed", () => { + it("should return false when NODE_ENV is production", async () => { + process.env.NODE_ENV = "production"; + const { seedLocationData } = await import("./seed-data.js"); + + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Checking if location data seeding is needed..."); + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: NODE_ENV is production"); + expect(mockPrisma.region.count).not.toHaveBeenCalled(); + }); + + it("should return false when CI is true", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "true"; + const { seedLocationData } = await import("./seed-data.js"); + + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Checking if location data seeding is needed..."); + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Running in CI environment"); + expect(mockPrisma.region.count).not.toHaveBeenCalled(); + }); + + it("should return false when tables already contain data (region not empty)", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(1); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(0); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Checking if location data seeding is needed..."); + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Tables already contain data"); + expect(mockPrisma.region.upsert).not.toHaveBeenCalled(); + }); + + it("should return false when tables already contain data (jurisdiction not empty)", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(1); + mockPrisma.location.count.mockResolvedValue(0); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Tables already contain data"); + expect(mockPrisma.region.upsert).not.toHaveBeenCalled(); + }); + + it("should return false when tables already contain data (location not empty)", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(1); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Tables already contain data"); + expect(mockPrisma.region.upsert).not.toHaveBeenCalled(); + }); + + it("should return true when all tables are empty", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(0); + mockPrisma.region.upsert.mockResolvedValue({}); + mockPrisma.jurisdiction.upsert.mockResolvedValue({}); + mockPrisma.subJurisdiction.upsert.mockResolvedValue({}); + mockPrisma.location.upsert.mockResolvedValue({}); + mockPrisma.locationRegion.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationSubJurisdiction.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationRegion.createMany.mockResolvedValue({ count: 1 }); + mockPrisma.locationSubJurisdiction.createMany.mockResolvedValue({ count: 1 }); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding location reference data..."); + expect(mockPrisma.region.upsert).toHaveBeenCalled(); + }); + }); + + describe("seedLocationData", () => { + beforeEach(() => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(0); + mockPrisma.region.upsert.mockResolvedValue({}); + mockPrisma.jurisdiction.upsert.mockResolvedValue({}); + mockPrisma.subJurisdiction.upsert.mockResolvedValue({}); + mockPrisma.location.upsert.mockResolvedValue({}); + mockPrisma.locationRegion.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationSubJurisdiction.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationRegion.createMany.mockResolvedValue({ count: 1 }); + mockPrisma.locationSubJurisdiction.createMany.mockResolvedValue({ count: 1 }); + }); + + it("should seed regions correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding regions..."); + expect(mockPrisma.region.upsert).toHaveBeenCalledTimes(mockLocationData.regions.length); + expect(mockPrisma.region.upsert).toHaveBeenCalledWith({ + where: { regionId: 1 }, + create: { + regionId: 1, + name: "Test Region 1", + welshName: "Rhanbarth Prawf 1" + }, + update: { + name: "Test Region 1", + welshName: "Rhanbarth Prawf 1" + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.regions.length} regions`); + }); + + it("should seed jurisdictions correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding jurisdictions..."); + expect(mockPrisma.jurisdiction.upsert).toHaveBeenCalledTimes(mockLocationData.jurisdictions.length); + expect(mockPrisma.jurisdiction.upsert).toHaveBeenCalledWith({ + where: { jurisdictionId: 1 }, + create: { + jurisdictionId: 1, + name: "Test Jurisdiction 1", + welshName: "Awdurdodaeth Prawf 1" + }, + update: { + name: "Test Jurisdiction 1", + welshName: "Awdurdodaeth Prawf 1" + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.jurisdictions.length} jurisdictions`); + }); + + it("should seed sub-jurisdictions correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding sub-jurisdictions..."); + expect(mockPrisma.subJurisdiction.upsert).toHaveBeenCalledTimes(mockLocationData.subJurisdictions.length); + expect(mockPrisma.subJurisdiction.upsert).toHaveBeenCalledWith({ + where: { subJurisdictionId: 1 }, + create: { + subJurisdictionId: 1, + name: "Test Sub-Jurisdiction 1", + welshName: "Is-awdurdodaeth Prawf 1", + jurisdictionId: 1 + }, + update: { + name: "Test Sub-Jurisdiction 1", + welshName: "Is-awdurdodaeth Prawf 1", + jurisdictionId: 1 + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.subJurisdictions.length} sub-jurisdictions`); + }); + + it("should seed locations correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding locations..."); + expect(mockPrisma.location.upsert).toHaveBeenCalledTimes(mockLocationData.locations.length); + expect(mockPrisma.location.upsert).toHaveBeenCalledWith({ + where: { locationId: 1 }, + create: { + locationId: 1, + name: "Test Location 1", + welshName: "Lleoliad Prawf 1", + email: null, + contactNo: null + }, + update: { + name: "Test Location 1", + welshName: "Lleoliad Prawf 1" + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.locations.length} locations`); + }); + + it("should delete and create location region junction records", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + // Should delete existing records for each location + expect(mockPrisma.locationRegion.deleteMany).toHaveBeenCalledTimes(mockLocationData.locations.length); + expect(mockPrisma.locationRegion.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 1 } + }); + + // Should create new records only for locations with regions + const locationsWithRegions = mockLocationData.locations.filter((l) => l.regions.length > 0); + expect(mockPrisma.locationRegion.createMany).toHaveBeenCalledTimes(locationsWithRegions.length); + expect(mockPrisma.locationRegion.createMany).toHaveBeenCalledWith({ + data: [{ locationId: 1, regionId: 1 }] + }); + }); + + it("should delete and create location sub-jurisdiction junction records", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + // Should delete existing records for each location + expect(mockPrisma.locationSubJurisdiction.deleteMany).toHaveBeenCalledTimes(mockLocationData.locations.length); + expect(mockPrisma.locationSubJurisdiction.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 1 } + }); + + // Should create new records only for locations with sub-jurisdictions + const locationsWithSubJurisdictions = mockLocationData.locations.filter((l) => l.subJurisdictions.length > 0); + expect(mockPrisma.locationSubJurisdiction.createMany).toHaveBeenCalledTimes(locationsWithSubJurisdictions.length); + expect(mockPrisma.locationSubJurisdiction.createMany).toHaveBeenCalledWith({ + data: [{ locationId: 1, subJurisdictionId: 1 }] + }); + }); + + it("should not create junction records for locations with empty arrays", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + // Verify deleteMany was called for location 3 (has empty arrays) + expect(mockPrisma.locationRegion.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 3 } + }); + expect(mockPrisma.locationSubJurisdiction.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 3 } + }); + + // But createMany should only be called 2 times (for locations 1 and 2 that have data) + expect(mockPrisma.locationRegion.createMany).toHaveBeenCalledTimes(2); + expect(mockPrisma.locationSubJurisdiction.createMany).toHaveBeenCalledTimes(2); + }); + + it("should log completion message", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Location reference data seeding completed successfully"); + }); + + it("should complete full seeding workflow in correct order", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + const calls = consoleLogSpy.mock.calls.map((call) => call[0]); + + expect(calls.indexOf("Checking if location data seeding is needed...")).toBeLessThan(calls.indexOf("Seeding location reference data...")); + expect(calls.indexOf("Seeding location reference data...")).toBeLessThan(calls.indexOf("Seeding regions...")); + expect(calls.indexOf("Seeding regions...")).toBeLessThan(calls.indexOf("Seeding jurisdictions...")); + expect(calls.indexOf("Seeding jurisdictions...")).toBeLessThan(calls.indexOf("Seeding sub-jurisdictions...")); + expect(calls.indexOf("Seeding sub-jurisdictions...")).toBeLessThan(calls.indexOf("Seeding locations...")); + expect(calls.indexOf("Seeding locations...")).toBeLessThan(calls.indexOf("Location reference data seeding completed successfully")); + }); + }); +}); From 41e8bf852415ed7c9c234a932f5eb31dbdc92b23 Mon Sep 17 00:00:00 2001 From: junaidiqbalmoj <84805836+junaidiqbalmoj@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:54:55 +0000 Subject: [PATCH 4/4] Fix CI build by generating Prisma client before tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build was failing because the Prisma client wasn't generated before running tests. This resulted in TypeScript errors where the location, jurisdiction, region, and other models didn't exist on the PrismaClient type. Added a step to run 'yarn db:generate' before running tests to ensure the Prisma client is generated from the collated schemas. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7aa983240..2061c1dab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,9 @@ jobs: - name: Install dependencies run: yarn install --immutable + - name: Generate Prisma client + run: yarn db:generate + - name: Setup Turbo cache uses: actions/cache@v4 with: