From a7782a1781f8e8fb146a7c0474e56ea83edaf65f Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 12:14:51 +0530 Subject: [PATCH 1/9] config: use dashed schema code RAINMAKER-PGR.ServiceDefs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend schema is registered as RAINMAKER-PGR.ServiceDefs (dash). Verified on naipepea, bomet — no underscore variant exists. Fixes #1 Co-Authored-By: Claude Opus 4.7 (1M context) --- utilities/crs_dataloader/ui-mockup/src/api/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/config.ts b/utilities/crs_dataloader/ui-mockup/src/api/config.ts index 1b3c7e7fc..b72f35c5b 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/config.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/config.ts @@ -53,7 +53,7 @@ export const MDMS_SCHEMAS = { EMPLOYEE_STATUS: 'egov-hrms.EmployeeStatus', EMPLOYEE_TYPE: 'egov-hrms.EmployeeType', ROLES: 'ACCESSCONTROL-ROLES.roles', - PGR_SERVICE_DEFS: 'RAINMAKER_PGR.ServiceDefs', + PGR_SERVICE_DEFS: 'RAINMAKER-PGR.ServiceDefs', TENANT: 'tenant.tenants', }; From 11004e0563c3bd932a3db3d440f7e13ae9243ac5 Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 12:15:43 +0530 Subject: [PATCH 2/9] mdms: populate description + array department on createDesignation payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit common-masters.Designation requires description and expects department as string[]. Configurator was sending neither — creation returned INVALID_REQUEST_TYPE1 and INVALID_REQUEST_REQUIRED2. - Designation / DesignationExcelRow: add description, change department to string[] to match schema - createDesignation: pass description through - parseDesignationExcel: accept description column (fallback to name), split department on commas to allow multi-department assignments Fixes #2 Co-Authored-By: Claude Opus 4.7 (1M context) --- utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts | 1 + utilities/crs_dataloader/ui-mockup/src/api/types.ts | 6 ++++-- utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts index 62a9bbad7..a2d73831a 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts @@ -102,6 +102,7 @@ export const mdmsService = { return this.create(tenantId, MDMS_SCHEMAS.DESIGNATION, designation.code, { code: designation.code, name: designation.name, + description: designation.description, department: designation.department, active: designation.active, }); diff --git a/utilities/crs_dataloader/ui-mockup/src/api/types.ts b/utilities/crs_dataloader/ui-mockup/src/api/types.ts index 2683effd9..89233c275 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/types.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/types.ts @@ -137,7 +137,8 @@ export interface Department { export interface Designation { code: string; name: string; - department?: string; + description: string; + department?: string[]; active: boolean; tenantId?: string; } @@ -370,7 +371,8 @@ export interface DepartmentExcelRow { export interface DesignationExcelRow { code: string; name: string; - department?: string; + description: string; + department?: string[]; active: boolean; } diff --git a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts index 133861601..ee83b5355 100644 --- a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts +++ b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts @@ -521,7 +521,9 @@ export function parseDesignationExcel(workbook: XLSX.WorkBook): { jsonData.forEach((row, index) => { const code = String(row['code'] || row['Code'] || row['designationCode'] || '').trim(); const name = String(row['name'] || row['Name'] || row['designationName'] || '').trim(); - const department = String(row['department'] || row['Department'] || '').trim() || undefined; + const description = String(row['description'] || row['Description'] || '').trim() || name; + const deptRaw = String(row['department'] || row['Department'] || '').trim(); + const department = deptRaw ? deptRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined; const activeStr = String(row['active'] || row['Active'] || row['isActive'] || 'true').trim().toLowerCase(); const active = activeStr === 'true' || activeStr === 'yes' || activeStr === '1'; @@ -545,7 +547,7 @@ export function parseDesignationExcel(workbook: XLSX.WorkBook): { return; } - designations.push({ code, name, department, active }); + designations.push({ code, name, description, department, active }); }); return { From 36213af38b21a13566d0176ea10c2f3c81da3c55 Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 12:17:25 +0530 Subject: [PATCH 3/9] mdms: rename serviceName to name, add keywords on ServiceDefs payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAINMAKER-PGR.ServiceDefs schema requires `name` (not `serviceName`) and `keywords`. Configurator was sending `serviceName` (rejected as extraneous) and no `keywords` — creation returned INVALID_REQUEST_ADDITIONALPROPERTIES3 and REQUIRED1/2 errors. - ComplaintType / ComplaintTypeExcelRow: rename serviceName→name, add keywords - createComplaintType / getComplaintTypes: swap field names in payloads - parseComplaintTypeExcel: accept name column (fallback to legacy serviceName alias for existing templates), accept keywords column (default to name.toLowerCase() comma-split) - Phase3Page preview + progress list: use type.name - localization service: rename serviceName param to name for consistency Fixes #3 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ui-mockup/src/api/services/localization.ts | 10 +++++----- .../crs_dataloader/ui-mockup/src/api/services/mdms.ts | 6 ++++-- utilities/crs_dataloader/ui-mockup/src/api/types.ts | 6 ++++-- .../crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx | 9 +++++---- .../crs_dataloader/ui-mockup/src/utils/excelParser.ts | 10 ++++++---- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts index 8d2caaab4..f9db7ce20 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts @@ -115,19 +115,19 @@ export const localizationService = { buildComplaintTypeLocalizations( _tenantId: string, serviceCode: string, - serviceName: string, + name: string, locale: string = 'en_IN' ): LocalizationMessage[] { return [ { code: `SERVICEDEFS.${serviceCode}`, - message: serviceName, + message: name, module: 'rainmaker-pgr', locale, }, { code: `SERVICEDEFS.${serviceCode.toUpperCase()}`, - message: serviceName, + message: name, module: 'rainmaker-pgr', locale, }, @@ -201,11 +201,11 @@ export const localizationService = { // Upload localizations for all complaint types async uploadComplaintTypeLocalizations( tenantId: string, - types: { serviceCode: string; serviceName: string }[], + types: { serviceCode: string; name: string }[], locale: string = 'en_IN' ): Promise<{ success: number; failed: number }> { const messages = types.flatMap((t) => - this.buildComplaintTypeLocalizations(tenantId, t.serviceCode, t.serviceName, locale) + this.buildComplaintTypeLocalizations(tenantId, t.serviceCode, t.name, locale) ); return this.upsertMessages(tenantId, locale, messages); }, diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts index a2d73831a..ea30d5ca3 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts @@ -142,7 +142,8 @@ export const mdmsService = { return results.map((r) => ({ serviceCode: r.serviceCode as string, - serviceName: r.serviceName as string, + name: r.name as string, + keywords: (r.keywords as string) || '', department: r.department as string, slaHours: r.slaHours as number, menuPath: r.menuPath as string | undefined, @@ -161,7 +162,8 @@ export const mdmsService = { complaintType.serviceCode, { serviceCode: complaintType.serviceCode, - serviceName: complaintType.serviceName, + name: complaintType.name, + keywords: complaintType.keywords, department: complaintType.department, slaHours: complaintType.slaHours, menuPath: complaintType.menuPath || 'Complaint', diff --git a/utilities/crs_dataloader/ui-mockup/src/api/types.ts b/utilities/crs_dataloader/ui-mockup/src/api/types.ts index 89233c275..c90965178 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/types.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/types.ts @@ -146,7 +146,8 @@ export interface Designation { // Complaint Type / Service Definition export interface ComplaintType { serviceCode: string; - serviceName: string; + name: string; + keywords: string; department: string; slaHours: number; menuPath?: string; @@ -378,7 +379,8 @@ export interface DesignationExcelRow { export interface ComplaintTypeExcelRow { serviceCode: string; - serviceName: string; + name: string; + keywords: string; department: string; slaHours: number; active: boolean; diff --git a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx index da422a347..77c3444f6 100644 --- a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx +++ b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx @@ -171,7 +171,8 @@ export default function Phase3Page() { state.tenant, complaintTypes.map(ct => ({ serviceCode: ct.serviceCode, - serviceName: ct.serviceName, + name: ct.name, + keywords: ct.keywords, department: ct.department, slaHours: ct.slaHours, active: ct.active, @@ -184,7 +185,7 @@ export default function Phase3Page() { state.tenant, complaintTypes.map(ct => ({ serviceCode: ct.serviceCode, - serviceName: ct.serviceName, + name: ct.name, })), 'en_IN' ); @@ -491,7 +492,7 @@ export default function Phase3Page() { {type.serviceCode} - {type.serviceName} + {type.name} {type.slaHours}h {type.department} @@ -585,7 +586,7 @@ export default function Phase3Page() { ) : ( )} - {type.serviceCode} - {type.serviceName} + {type.serviceCode} - {type.name} ))} {complaintTypes.length > 5 && ( diff --git a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts index ee83b5355..f10fd46dc 100644 --- a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts +++ b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts @@ -599,7 +599,9 @@ export function parseComplaintTypeExcel(workbook: XLSX.WorkBook): { jsonData.forEach((row, index) => { const serviceCode = String(row['serviceCode'] || row['ServiceCode'] || row['code'] || '').trim(); - const serviceName = String(row['serviceName'] || row['ServiceName'] || row['name'] || '').trim(); + const name = String(row['name'] || row['Name'] || row['serviceName'] || row['ServiceName'] || '').trim(); + const keywordsRaw = String(row['keywords'] || row['Keywords'] || '').trim(); + const keywords = keywordsRaw || name.toLowerCase().replace(/\s+/g, ','); const department = String(row['department'] || row['Department'] || '').trim(); const slaHours = parseInt(String(row['slaHours'] || row['SlaHours'] || row['sla'] || '24'), 10) || 24; const activeStr = String(row['active'] || row['Active'] || row['isActive'] || 'true').trim().toLowerCase(); @@ -615,10 +617,10 @@ export function parseComplaintTypeExcel(workbook: XLSX.WorkBook): { return; } - if (!serviceName) { + if (!name) { errors.push({ row: index + 2, - field: 'serviceName', + field: 'name', message: 'Service name is required', code: 'REQUIRED_FIELD', }); @@ -635,7 +637,7 @@ export function parseComplaintTypeExcel(workbook: XLSX.WorkBook): { return; } - complaintTypes.push({ serviceCode, serviceName, department, slaHours, active }); + complaintTypes.push({ serviceCode, name, keywords, department, slaHours, active }); }); return { From dd309bdaed0790d210bde3e1b13c1c6556f0d793 Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 12:18:32 +0530 Subject: [PATCH 4/9] hrms: populate user.dob on employee create payload HRMS service rejects any _create payload without user.dob with NotNull.employeeRequest.employees[0].user.dob. Configurator's buildEmployee omitted it entirely, so every Phase 4 employee import failed on the first row. - EmployeeExcelRow: add optional dob column - buildEmployee: accept dob input; default to 1990-01-01 UTC when neither form nor sheet provided one, with a comment explaining why - parseEmployeeExcel: accept dob/DOB/dateOfBirth columns - Phase4Page: thread dob from Excel row into buildEmployee Future cleanup: surface dob as a proper form field / optional-per-employee in the Excel template generator. Fixes #4 Co-Authored-By: Claude Opus 4.7 (1M context) --- utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts | 5 +++++ utilities/crs_dataloader/ui-mockup/src/api/types.ts | 1 + utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx | 1 + utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts | 2 ++ 4 files changed, 9 insertions(+) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts index 709cf372f..d606382c1 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts @@ -101,6 +101,7 @@ export const hrmsService = { mobileNumber: string; emailId?: string; gender?: string; + dob?: number; department: string; designation: string; roles: Role[]; @@ -109,6 +110,9 @@ export const hrmsService = { password?: string; }): Employee { const now = Date.now(); + // HRMS @NotNull on user.dob. Default to 1990-01-01 UTC when the form/sheet + // didn't provide one, since HRMS otherwise rejects the whole create. + const dob = data.dob ?? Date.UTC(1990, 0, 1); const user: EmployeeUser = { userName: data.userName.toLowerCase(), @@ -117,6 +121,7 @@ export const hrmsService = { mobileNumber: data.mobileNumber, emailId: data.emailId, gender: data.gender, + dob, type: 'EMPLOYEE', active: true, tenantId: data.tenantId, diff --git a/utilities/crs_dataloader/ui-mockup/src/api/types.ts b/utilities/crs_dataloader/ui-mockup/src/api/types.ts index c90965178..5597ddb14 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/types.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/types.ts @@ -393,6 +393,7 @@ export interface EmployeeExcelRow { mobileNumber: string; emailId?: string; gender?: string; + dob?: string; department: string; designation: string; roles: string; // comma-separated diff --git a/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx b/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx index 01e474625..25e37c542 100644 --- a/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx +++ b/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx @@ -243,6 +243,7 @@ export default function Phase4Page() { mobileNumber: emp.mobileNumber, emailId: emp.emailId, gender: emp.gender, + dob: emp.dob ? new Date(emp.dob).getTime() : undefined, department: emp.department, designation: emp.designation, roles, diff --git a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts index f10fd46dc..dfecd8d80 100644 --- a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts +++ b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts @@ -694,6 +694,7 @@ export function parseEmployeeExcel(workbook: XLSX.WorkBook): { const mobileNumber = String(row['mobileNumber'] || row['MobileNumber'] || row['mobile'] || row['phone'] || '').trim(); const emailId = String(row['emailId'] || row['EmailId'] || row['email'] || '').trim() || undefined; const gender = String(row['gender'] || row['Gender'] || '').trim() || undefined; + const dob = String(row['dob'] || row['DOB'] || row['dateOfBirth'] || '').trim() || undefined; const department = String(row['department'] || row['Department'] || '').trim(); const designation = String(row['designation'] || row['Designation'] || '').trim(); const roles = String(row['roles'] || row['Roles'] || row['role'] || 'EMPLOYEE').trim(); @@ -783,6 +784,7 @@ export function parseEmployeeExcel(workbook: XLSX.WorkBook): { mobileNumber, emailId, gender, + dob, department, designation, roles, From f6fbe39a31a4d4f7e7a445dc146d7565feb37b91 Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 13:05:10 +0530 Subject: [PATCH 5/9] mdms: populate tenantId inside createTenant data payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tenant.tenants schema requires `tenantId` in the `data` object (in addition to the MDMS wrapper's tenantId). It stores the parent/root tenant the city lives under — for `ke.testzone` that's `ke`. Configurator was omitting it, so every Phase 1 upload returned: {"code":"INVALID_REQUEST_REQUIRED1","message":"required key [tenantId] not found"} Set tenantId = stateTenantId (the session tenant, which is the root). Co-Authored-By: Claude Opus 4.7 (1M context) --- utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts index ea30d5ca3..357e88a15 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts @@ -218,8 +218,11 @@ export const mdmsService = { }, async createTenant(stateTenantId: string, tenant: Tenant): Promise { - // Build the full tenant data structure matching MDMS schema + // Build the full tenant data structure matching MDMS schema. + // tenant.tenants schema requires `tenantId` inside data (in addition to the + // Mdms.tenantId wrapper) — it stores the parent/root tenant this city lives under. const tenantData = { + tenantId: stateTenantId, code: tenant.code, name: tenant.name, type: tenant.city?.ulbGrade || 'CITY', From f3e2a40d942fd070014dc41bc53967a23dcd527c Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 13:43:09 +0530 Subject: [PATCH 6/9] boundary: searchBoundaries hits /boundary-relationships/_search /boundary/_search returns boundary *entities* by code and doesn't walk the hierarchy. Phase 4 called it expecting a full tree and got 0 results even though boundaries were present. The correct endpoint is /boundary-relationships/_search with query params (tenantId, hierarchyType, includeChildren=true). Swap to it. Side effects: - flattenBoundaries: dedupe by code because the relationships endpoint returns each child twice under its parent (known backend quirk) - carry hierarchyType down from the TenantBoundary wrapper into each flattened boundary, since the children don't always have it set Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ui-mockup/src/api/services/boundary.ts | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts index c26d51b8f..320d2a7d8 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts @@ -60,7 +60,12 @@ export const boundaryService = { // Boundary Methods // ============================================ - // Search boundaries + // Search boundaries — returns the hierarchical tree flattened to a list. + // + // Uses /boundary-service/boundary-relationships/_search (not /boundary/_search) because + // the latter searches boundary *entities* by code and doesn't return children; only + // the relationships endpoint walks the hierarchy. Query params (not body) are how this + // endpoint accepts its filters. async searchBoundaries( tenantId: string, options?: { @@ -71,33 +76,45 @@ export const boundaryService = { offset?: number; } ): Promise { - const response = await apiClient.post(ENDPOINTS.BOUNDARY_SEARCH, { - RequestInfo: apiClient.buildRequestInfo(), - Boundary: { - tenantId, - hierarchyType: options?.hierarchyType, - boundaryType: options?.boundaryType, - codes: options?.codes, - limit: options?.limit || 100, - offset: options?.offset || 0, - }, - }); + const qs = new URLSearchParams({ tenantId, includeChildren: 'true' }); + if (options?.hierarchyType) qs.set('hierarchyType', options.hierarchyType); + if (options?.boundaryType) qs.set('boundaryType', options.boundaryType); + if (options?.codes?.length) qs.set('codes', options.codes.join(',')); + + const response = await apiClient.post( + `${ENDPOINTS.BOUNDARY_RELATIONSHIP_SEARCH}?${qs.toString()}`, + { RequestInfo: apiClient.buildRequestInfo() }, + ); // Flatten the nested boundary structure const tenantBoundaries = response.TenantBoundary || []; const boundaries: Boundary[] = []; + const seen = new Set(); - for (const tb of tenantBoundaries as { boundary: Boundary }[]) { - if (tb.boundary) { - this.flattenBoundaries(tb.boundary, boundaries); + for (const tb of tenantBoundaries as { boundary: Boundary | Boundary[]; hierarchyType?: string }[]) { + if (!tb.boundary) continue; + const items = Array.isArray(tb.boundary) ? tb.boundary : [tb.boundary]; + for (const root of items) { + this.flattenBoundaries(root, boundaries, seen, tb.hierarchyType); } } return boundaries; }, - // Helper to flatten nested boundary tree - flattenBoundaries(boundary: Boundary, result: Boundary[]): void { + // Helper to flatten nested boundary tree. Dedupes by code because the relationships + // endpoint duplicates children under their parent in the response payload. + flattenBoundaries( + boundary: Boundary, + result: Boundary[], + seen?: Set, + hierarchyType?: string, + ): void { + const code = boundary.code; + if (seen && code) { + if (seen.has(code)) return; + seen.add(code); + } result.push({ id: boundary.id, tenantId: boundary.tenantId, @@ -105,14 +122,14 @@ export const boundaryService = { name: boundary.name, boundaryType: boundary.boundaryType, parent: boundary.parent, - hierarchyType: boundary.hierarchyType, + hierarchyType: boundary.hierarchyType ?? hierarchyType, latitude: boundary.latitude, longitude: boundary.longitude, }); if (boundary.children) { for (const child of boundary.children) { - this.flattenBoundaries(child, result); + this.flattenBoundaries(child, result, seen, hierarchyType); } } }, From f897c97252873e31b6ff97fa1293a4fbe98baa45 Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 14:23:34 +0530 Subject: [PATCH 7/9] phase3: thread designation.description through the page-level map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix (11004e05) updated parseDesignationExcel + service + types to include description, but Phase3Page was still mapping the parsed row to a subset {code, name, department, active} — stripping description before handing to createDesignations. Result: designation creates kept silently 4xx'ing with "required key [description] not found" even on the latest bundle. Adding description to the shape sent into createDesignations. Co-Authored-By: Claude Opus 4.7 (1M context) --- utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx index 77c3444f6..2d0c58fd5 100644 --- a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx +++ b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx @@ -142,6 +142,7 @@ export default function Phase3Page() { designations.map(d => ({ code: d.code, name: d.name, + description: d.description, department: d.department, active: d.active, })) From ed5251dd69df880ae1c94507d81f6ecb7bc80db8 Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 14:28:26 +0530 Subject: [PATCH 8/9] boundary: verify before swallowing "already exists" on create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The boundary-service sometimes returns "already exists" / "DUPLICATE" errors for rows that never actually persisted (stale cache between the boundary-service and the eventually-consistent Kafka persister). The current FE catches any error string containing "already exists" and returns true, which makes Phase 2 silently claim success while leaving nothing in the DB — Phase 4 then reports "Boundaries: 0 loaded" on a tree the user thought they just built. Swap the blanket swallow for verify-then-swallow: on "already exists" errors, search for the record. If found, swallow (idempotent re-run is fine). If not, throw a clearer error so the UI can surface the real problem. Same treatment for createBoundaryEntity and createBoundaryRelationship. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ui-mockup/src/api/services/boundary.ts | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts index 320d2a7d8..a79016254 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts @@ -134,7 +134,12 @@ export const boundaryService = { } }, - // Create a boundary entity (just the entity, not the relationship) + // Create a boundary entity (just the entity, not the relationship). + // If the backend reports "already exists", verify the entity actually lives in the + // DB before swallowing the error — the boundary service occasionally returns a + // false-positive "already exists" when a prior request sits in its cache layer + // even though the row isn't persisted, which would otherwise make Phase 2 + // silently succeed and leave nothing for Phase 4 to find. async createBoundaryEntity(tenantId: string, code: string): Promise { try { await apiClient.post(ENDPOINTS.BOUNDARY_CREATE, { @@ -147,16 +152,35 @@ export const boundaryService = { }); return true; } catch (error) { - // Check if already exists (which is OK) const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.toLowerCase().includes('already exists') || errorMsg.includes('DUPLICATE')) { - return true; + // Verify it actually exists before swallowing. + const found = await this.boundaryEntityExists(tenantId, code); + if (found) return true; + throw new Error( + `Backend reported boundary entity ${code} already exists, but a search returned nothing. ` + + `Retry may be needed, or a stale cache/Kafka state is masking the real error.` + ); } throw error; } }, - // Create a boundary relationship (parent-child link in hierarchy) + async boundaryEntityExists(tenantId: string, code: string): Promise { + try { + const response = await apiClient.post( + `${ENDPOINTS.BOUNDARY_SEARCH}?tenantId=${encodeURIComponent(tenantId)}&codes=${encodeURIComponent(code)}`, + { RequestInfo: apiClient.buildRequestInfo() }, + ); + return (response.Boundary?.length ?? 0) > 0; + } catch { + return false; + } + }, + + // Create a boundary relationship (parent-child link in hierarchy). + // Same verify-before-swallow pattern as createBoundaryEntity — the backend + // sometimes returns "already exists" for relationships that never persisted. async createBoundaryRelationship( tenantId: string, hierarchyType: string, @@ -182,15 +206,33 @@ export const boundaryService = { await apiClient.post(ENDPOINTS.BOUNDARY_RELATIONSHIP_CREATE, payload); return true; } catch (error) { - // Check if already exists (which is OK) const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.toLowerCase().includes('already exists') || errorMsg.includes('DUPLICATE')) { - return true; + const found = await this.boundaryRelationshipExists(tenantId, hierarchyType, code); + if (found) return true; + throw new Error( + `Backend reported relationship ${code} (${hierarchyType}) already exists, ` + + `but a search returned nothing. Likely a stale cache — try again in a few seconds ` + + `or clean up and re-run.` + ); } throw error; } }, + async boundaryRelationshipExists(tenantId: string, hierarchyType: string, code: string): Promise { + try { + const qs = new URLSearchParams({ tenantId, hierarchyType, codes: code, includeChildren: 'false' }); + const response = await apiClient.post( + `${ENDPOINTS.BOUNDARY_RELATIONSHIP_SEARCH}?${qs.toString()}`, + { RequestInfo: apiClient.buildRequestInfo() }, + ); + return (response.TenantBoundary?.length ?? 0) > 0; + } catch { + return false; + } + }, + // Create a single boundary (entity + relationship) async createBoundary(boundary: Boundary): Promise { // Step 1: Create the boundary entity From c23b49bde68ed174fa1fe047295adcc900b00966 Mon Sep 17 00:00:00 2001 From: kanav11dwevedi Date: Tue, 21 Apr 2026 14:53:03 +0530 Subject: [PATCH 9/9] boundary/phase3: unwrap hierarchy array + add temp designation log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 never fired /boundary-relationships/_create calls in the most recent live walk — root cause is createHierarchy casting the backend response to a single BoundaryHierarchy when it's actually an array. selectedHierarchy.hierarchyType then came out undefined, and createBoundary's `if (boundary.hierarchyType && boundary.boundaryType)` guard skipped the relationship call for all 4 boundaries. Unwrap the array before returning. Phase 3 is still silently 400'ing "required key [description] not found" even with the f897c972 fix in place. The bundle has the right code path, but the live request somehow lacks the field. Add a console.log of the payload handed to createDesignations so the next failed walk surfaces exactly what the browser is sending — will remove once diagnosed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ui-mockup/src/api/services/boundary.ts | 8 +++++-- .../ui-mockup/src/pages/Phase3Page.tsx | 22 ++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts index a79016254..2ab2313d5 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts @@ -23,7 +23,9 @@ export const boundaryService = { return hierarchies as BoundaryHierarchy[]; }, - // Create a new boundary hierarchy + // Create a new boundary hierarchy. + // Note: the backend returns BoundaryHierarchy as an ARRAY even though the create + // payload sends a single object — unwrap the first element. async createHierarchy( tenantId: string, hierarchyType: string, @@ -38,7 +40,9 @@ export const boundaryService = { }, }); - return response.BoundaryHierarchy as BoundaryHierarchy; + const raw = response.BoundaryHierarchy; + if (Array.isArray(raw)) return raw[0] as BoundaryHierarchy; + return raw as BoundaryHierarchy; }, // Helper to create hierarchy from level names diff --git a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx index 2d0c58fd5..c4f23653b 100644 --- a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx +++ b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx @@ -137,16 +137,18 @@ export default function Phase3Page() { // Create designations if (designations.length > 0) { setProgressMessage('Creating designations...'); - const desigResults = await mdmsService.createDesignations( - state.tenant, - designations.map(d => ({ - code: d.code, - name: d.name, - description: d.description, - department: d.department, - active: d.active, - })) - ); + const payload = designations.map(d => ({ + code: d.code, + name: d.name, + description: d.description, + department: d.department, + active: d.active, + })); + // Temporary diagnostic while chasing the "description not found" 400s: + // logs the exact object handed to the service. Remove once the flow is stable. + // eslint-disable-next-line no-console + console.log('[configurator] designation payload ->', JSON.stringify(payload)); + const desigResults = await mdmsService.createDesignations(state.tenant, payload); setCreatedDesigs(desigResults.success.length); // Create localizations for designations