Skip to content
Closed
2 changes: 1 addition & 1 deletion utilities/crs_dataloader/ui-mockup/src/api/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down
117 changes: 90 additions & 27 deletions utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -60,7 +64,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?: {
Expand All @@ -71,53 +80,70 @@ export const boundaryService = {
offset?: number;
}
): Promise<Boundary[]> {
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<string>();

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<string>,
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,
code: boundary.code,
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);
}
}
},

// 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<boolean> {
try {
await apiClient.post(ENDPOINTS.BOUNDARY_CREATE, {
Expand All @@ -130,16 +156,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<boolean> {
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,
Expand All @@ -165,15 +210,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<boolean> {
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<Boundary> {
// Step 1: Create the boundary entity
Expand Down
5 changes: 5 additions & 0 deletions utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const hrmsService = {
mobileNumber: string;
emailId?: string;
gender?: string;
dob?: number;
department: string;
designation: string;
roles: Role[];
Expand All @@ -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(),
Expand All @@ -117,6 +121,7 @@ export const hrmsService = {
mobileNumber: data.mobileNumber,
emailId: data.emailId,
gender: data.gender,
dob,
type: 'EMPLOYEE',
active: true,
tenantId: data.tenantId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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);
},
Expand Down
12 changes: 9 additions & 3 deletions utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -141,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,
Expand All @@ -160,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',
Expand Down Expand Up @@ -215,8 +218,11 @@ export const mdmsService = {
},

async createTenant(stateTenantId: string, tenant: Tenant): Promise<MdmsRecord> {
// 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',
Expand Down
13 changes: 9 additions & 4 deletions utilities/crs_dataloader/ui-mockup/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,17 @@ export interface Department {
export interface Designation {
code: string;
name: string;
department?: string;
description: string;
department?: string[];
active: boolean;
tenantId?: string;
}

// Complaint Type / Service Definition
export interface ComplaintType {
serviceCode: string;
serviceName: string;
name: string;
keywords: string;
department: string;
slaHours: number;
menuPath?: string;
Expand Down Expand Up @@ -370,13 +372,15 @@ export interface DepartmentExcelRow {
export interface DesignationExcelRow {
code: string;
name: string;
department?: string;
description: string;
department?: string[];
active: boolean;
}

export interface ComplaintTypeExcelRow {
serviceCode: string;
serviceName: string;
name: string;
keywords: string;
department: string;
slaHours: number;
active: boolean;
Expand All @@ -389,6 +393,7 @@ export interface EmployeeExcelRow {
mobileNumber: string;
emailId?: string;
gender?: string;
dob?: string;
department: string;
designation: string;
roles: string; // comma-separated
Expand Down
Loading