Skip to main content

LLD: Fetch All Installations API

1. Overview

Returns a paginated list of installations for a given TC and month, with support for filtering by billing status and searching by RR number. Used to populate the installation table within the TC Detail view on the AI Audit page.


2. Endpoint

RouteMethod
GET /audit/transformers/:tcId/installationsGET

Single route for all user types. The tcId path parameter specifies which TC's installations to fetch. Behaviour is determined from request.user.userType:

  • AE — validates that the TC belongs to the user's assigned section
  • Admin (CIO / SUPER_ADMIN) — can query any TC's installations

3. Request

3.1 Headers

HeaderRequiredDescription
AuthorizationYesBearer <sessionToken>

3.2 Path Parameters

ParamTypeRequiredDescription
tcIdstringYesThe id field of the transformer document (e.g., TC-xxx)

3.3 Query Parameters

ParamTypeRequiredDefaultDescription
monthstringNoCurrent month (YYYY-MM)Month to fetch installations for
pageintegerNo1Page number (1-based)
limitintegerNo10Items per page
billingStatusstringNoFilter by billing status (see enum below)
searchstringNoRR number prefix search (starts with, case-insensitive)

billingStatus enum values: BILLED, UNBILLED, VACANT, MNR, ZERO_CONSUMPTION, DOOR_LOCKED, ABNORMAL, SUBNORMAL, BILL_CANCELLATION

Note: billingStatus is a single string in the query filter, but the response field billingStatus is an array of strings (an installation can have multiple statuses).

For AE users: the handler verifies that the requested TC belongs to the AE's assigned section.


4. Response

4.1 Success (200)

{
"success": true,
"message": "Installations fetched successfully",
"data": {
"installations": [
{
"id": "inst-uuid-001",
"accountId": "ACC-123456",
"rrNumber": "RR-789012",
"month": "2026-01",
"mrCode": "MR-001",
"readingDay": 15,
"consumerName": "John Doe",
"tariff": "LT4",
"sanctionedLoad": {
"kw": 5,
"hp": 6.7
},
"consumption": 250,
"billAmount": 1500,
"billingStatus": ["BILLED"],
"tcId": "tc-uuid-001",
"tcNumber": "TC856896541556325623",
"location": {
"sectionCode": "SEC-001",
"sectionName": "Section 1",
"subDivisionCode": "SD-001",
"subDivisionName": "Sub Division 1",
"divisionCode": "DIV-001",
"divisionName": "Division 1",
"circleCode": "CIR-001",
"circleName": "Circle 1",
"zoneCode": "ZN-001",
"zoneName": "Zone 1"
},
"gpsCoordinates": {
"latitude": 12.8765,
"longitude": 74.8421
}
}
],
"pagination": {
"page": 1,
"limit": 10,
"totalCount": 753,
"totalPages": 76
}
}
}

4.2 Bad Request (400)

{
"success": false,
"message": ["tcId is required"]
}

4.3 Forbidden (403)

When AE requests installations for a TC outside their section:

{
"success": false,
"message": "TC does not belong to your assigned section"
}

5. Business Logic

5.1 AE Section Validation

The handler verifies the TC belongs to the user's section:

IF userType === "AE":
1. Fetch user doc from `users` collection using request.user.userId
2. Extract user.location.sectionCode
3. Fetch transformer doc for { id: tcId, month }
4. Verify transformer.location.sectionCode === user.location.sectionCode
5. If mismatch → 403 Forbidden

For Admin users: no section validation — can access any TC.

5.2 Default Month

IF month not provided:
month = current date formatted as "YYYY-MM"

5.3 Build Query Filter

// tcId from path params — strip TC- prefix to match installation tcId format
const installationTcId = params.tcId.replace(/^TC-/, "");

const filter = {
tcId: installationTcId,
month,
};

// Billing status filter
if (billingStatus) {
filter.billingStatus = billingStatus;
}

// RR number prefix search (case-insensitive, starts with)
if (search) {
filter.rrNumber = { $regex: `^${escapeRegex(search)}`, $options: "i" };
}

5.4 DB Queries

Two parallel queries — data + count:

const skip = (page - 1) * limit;

const [installations, totalCount] = await Promise.all([
installationModel
.find(filter, projection)
.skip(skip)
.limit(limit)
.lean(),

installationModel.countDocuments(filter),
]);

5.5 Projection

Only return fields needed by the UI:

const projection = {
_id: 0,
__v: 0,
createdAt: 0,
updatedAt: 0,
demand: 0,
collection: 0,
status: 0,
};

Fields returned per installation:

  • id, accountId, rrNumber, month
  • mrCode, readingDay
  • consumerName, tariff
  • sanctionedLoad (kw, hp)
  • consumption, billAmount, billingStatus
  • tcId, tcNumber
  • location (all fields)
  • gpsCoordinates (latitude, longitude) — Note: DB stores GeoJSON format ({ type: "Point", coordinates: [longitude, latitude] }). The API transforms this to { latitude, longitude } in the response.

5.6 Pagination Response

{
page,
limit,
totalCount,
totalPages: Math.ceil(totalCount / limit)
}

6. Validation Schema (AJV)

6.1 Path Parameters Schema

{
type: "object",
properties: {
tcId: {
type: "string",
transform: ["trim"],
}
},
required: ["tcId"]
}

6.2 Query String Schema

{
type: "object",
properties: {
month: {
type: "string",
pattern: "^\\d{4}-(0[1-9]|1[0-2])$",
errorMessage: { pattern: "month must be in YYYY-MM format" }
},
page: {
type: "integer",
minimum: 1,
default: 1
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
billingStatus: {
type: "string",
enum: [
"BILLED", "UNBILLED", "VACANT", "MNR",
"ZERO_CONSUMPTION", "DOOR_LOCKED",
"ABNORMAL", "SUBNORMAL", "BILL_CANCELLATION"
]
},
search: {
type: "string",
transform: ["trim"],
minLength: 1
}
},
additionalProperties: false
}

6.3 Response Serialization

{
"2xx": responseSerializer("2xx", {
type: "object",
properties: {
installations: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
accountId: { type: "string" },
rrNumber: { type: ["string", "null"] },
month: { type: "string" },
mrCode: { type: ["string", "null"] },
readingDay: { type: ["number", "null"] },
consumerName: { type: ["string", "null"] },
tariff: { type: ["string", "null"] },
sanctionedLoad: {
type: "object",
properties: {
kw: { type: ["number", "null"] },
hp: { type: ["number", "null"] }
}
},
consumption: { type: ["number", "null"] },
billAmount: { type: ["number", "null"] },
billingStatus: {
type: "array",
items: { type: "string" }
},
tcId: { type: ["string", "null"] },
tcNumber: { type: ["string", "null"] },
location: {
type: "object",
properties: {
sectionCode: { type: "string" },
sectionName: { type: "string" },
subDivisionCode: { type: "string" },
subDivisionName: { type: "string" },
divisionCode: { type: "string" },
divisionName: { type: "string" },
circleCode: { type: "string" },
circleName: { type: "string" },
zoneCode: { type: "string" },
zoneName: { type: "string" }
}
},
gpsCoordinates: {
type: "object",
properties: {
latitude: { type: ["number", "null"] },
longitude: { type: ["number", "null"] }
}
}
}
}
},
pagination: {
type: "object",
properties: {
page: { type: "integer" },
limit: { type: "integer" },
totalCount: { type: "integer" },
totalPages: { type: "integer" }
}
}
}
}),
"4xx": responseSerializer("4xx"),
"5xx": responseSerializer("5xx")
}

7. Route Config

{
routeId: "fetchInstallations",
authentication: {
// No excludeFrom — requires full auth
},
authorization: {
resource: "installation",
action: "view",
id: "idNotApplicable"
}
}

8. File Structure

api/src/entities/audit/
audit.route.v1.js — Route definitions
controller/
fetchAuditSummary.js — Existing
fetchTransformers.js — Existing
fetchInstallations.js — Handler, schema, config exports
helpers/
resolveLocationFilter.js — Existing (reused for AE section lookup)

9. Error Handling

ScenarioStatusError Type
Missing tcId400ValidationError (AJV — required)
Invalid month format400ValidationError (AJV)
Invalid billingStatus enum400ValidationError (AJV)
Invalid/expired session401UnauthorizedError
AE accessing TC outside their section403UnauthorizedError
AE user doc not found500DatabaseError
DB connection failure500DatabaseError

10. Performance Notes

  • Index used for main query: { tcId: 1, month: 1 } — covers the base filter. When billingStatus is added, the { tcId: 1, month: 1, billingStatus: 1 } compound index covers the filter exactly.
  • RR number search: { rrNumber: 1, month: 1 } index covers standalone RR searches. When combined with tcId filter, MongoDB will use the { tcId: 1, month: 1 } index and apply the regex as an in-memory filter, which is acceptable since a single TC typically has a few hundred installations at most.
  • countDocuments runs in parallel with find to avoid sequential latency.
  • AE users require one extra DB call (user lookup via resolveLocationFilter) plus one TC lookup for section validation.
  • Pagination: offset-based (skip/limit). Acceptable since installations per TC are typically in the hundreds, not thousands.
  • Average installations per TC: ~22 (3.3M installations / 150K TCs). Deep pagination is unlikely.

11. Regex Escape Utility

The search param is user input used in a $regex query. Reuse the same escapeRegex helper from fetchTransformers:

function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}