LLD: Fetch Transformers (TC List) API
1. Overview
Returns a paginated list of transformer centres (TCs) for a given month and location, with support for filtering by audit status, loss bucket range, failed reason, and TC name search. Used to populate the TC table on the AI Audit page.
2. Endpoint
| Route | Method |
|---|---|
GET /audit/transformers | GET |
Single route for all user types. Behaviour is determined from request.user.userType:
- AE — auto-scoped to their section (location params ignored)
- Admin (CIO / SUPER_ADMIN) — can query any location level via query params
3. Request
3.1 Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <sessionToken> |
3.2 Query Parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
month | string | No | Current month (YYYY-MM) | Month to fetch TCs for |
page | integer | No | 1 | Page number (1-based) |
limit | integer | No | 10 | Items per page |
auditStatus | string | No | — | AUDITED or AUDIT_FAILED |
lossBucket | string | No | — | One of: belowMinus5, minus5to0, zeroTo5, fiveTo10, tenTo15, fifteenTo20, above20 |
failedReason | string | No | — | One of: noMeterConstant, withoutInstallationConsumption, noTcConsumptionWithRemarks, noTcConsumptionNoRemarks |
search | string | No | — | TC name prefix search (starts with, case-insensitive) |
sectionCode | string | No | — | Filter by section (admin only) |
subDivisionCode | string | No | — | Filter by sub-division (admin only) |
divisionCode | string | No | — | Filter by division (admin only) |
circleCode | string | No | — | Filter by circle (admin only) |
zoneCode | string | No | — | Filter by zone (admin only) |
Filter combinations:
lossBucketimpliesauditStatus: "AUDITED"(loss buckets only apply to audited TCs)failedReasonimpliesauditStatus: "AUDIT_FAILED"- If both
lossBucketandfailedReasonare sent, return 400 (mutually exclusive)
For AE users: all location params are ignored. Location is auto-resolved from the user's profile.
4. Response
4.1 Success (200)
{
"success": true,
"message": "Transformers fetched successfully",
"data": {
"transformers": [
{
"id": "tc-uuid-001",
"number": "TC856896541556325623",
"name": "TC856896541556325623...",
"month": "2026-01",
"tcCapacity": 100,
"meterConstant": 1,
"installationCount": 753,
"untaggedInstallationCount": 5,
"tcConsumption": 3010,
"installationConsumption": 1650,
"lossPercentage": 20,
"previousMonthLossPercentage": 15,
"previousToPreviousMonthLossPercentage": 25,
"auditStatus": "AUDITED",
"auditedByAI": true,
"remark": null,
"overloaded": true,
"healthScore": 72,
"healthGrade": "B",
"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"
}
}
],
"pagination": {
"page": 1,
"limit": 10,
"totalCount": 1245,
"totalPages": 125
}
}
}
4.2 Bad Request (400)
{
"success": false,
"message": ["lossBucket and failedReason are mutually exclusive filters"]
}
5. Business Logic
5.1 Location Resolution
Uses the same resolveLocationFilter helper as the summary API.
For AE users: fetches user doc → extracts sectionCode → filters by location.sectionCode.
For Admin users:
sectionCode→ filter bylocation.sectionCodesubDivisionCode→ filter bylocation.subDivisionCodedivisionCode→ filter bylocation.divisionCodecircleCode→ filter bylocation.circleCodezoneCode→ filter bylocation.zoneCode- No location param → no location filter (all TCs for that month)
5.2 Default Month
IF month not provided:
month = current date formatted as "YYYY-MM"
5.3 Build Query Filter
const filter = { month };
// Location filter (from resolveLocationFilter — excludes 'level' key)
// e.g., { "location.divisionCode": "DIV-001" }
Object.assign(filter, locationFilter);
// Audit status filter
if (lossBucket) {
filter.auditStatus = "AUDITED";
} else if (failedReason) {
filter.auditStatus = "AUDIT_FAILED";
} else if (auditStatus) {
filter.auditStatus = auditStatus;
}
// Loss bucket range filter (only for AUDITED)
if (lossBucket) {
const ranges = {
belowMinus5: { $lt: -5 },
minus5to0: { $gte: -5, $lt: 0 },
zeroTo5: { $gte: 0, $lt: 5 },
fiveTo10: { $gte: 5, $lt: 10 },
tenTo15: { $gte: 10, $lt: 15 },
fifteenTo20: { $gte: 15, $lt: 20 },
above20: { $gte: 20 },
};
filter.lossPercentage = ranges[lossBucket];
}
// Failed reason filter (only for AUDIT_FAILED)
if (failedReason) {
switch (failedReason) {
case "noMeterConstant":
filter.meterConstant = null;
break;
case "withoutInstallationConsumption":
filter.installationConsumption = null;
break;
case "noTcConsumptionWithRemarks":
filter.tcConsumption = null;
filter.remark = { $ne: null };
break;
case "noTcConsumptionNoRemarks":
filter.tcConsumption = null;
filter.remark = null;
break;
}
}
// TC name prefix search (case-insensitive, starts with)
if (search) {
filter.name = { $regex: `^${escapeRegex(search)}`, $options: "i" };
}
5.4 DB Queries
Two parallel queries — data + count:
const skip = (page - 1) * limit;
const [transformers, totalCount] = await Promise.all([
transformerModel
.find(filter, projection)
.skip(skip)
.limit(limit)
.lean(),
transformerModel.countDocuments(filter),
]);
5.5 Projection
Only return fields needed by the UI:
const projection = {
_id: 0,
__v: 0,
createdAt: 0,
updatedAt: 0,
reading: 0,
billingCount: 0,
revenueParams: 0,
totalInstallationSanctionedLoad: 0,
readingDay: 0,
readingMR: 0,
gpsCoordinates: 0,
activatedOn: 0,
deletedOn: 0,
auditedOn: 0,
};
Fields returned per TC:
id,number,name,monthtcCapacity,meterConstantinstallationCount,untaggedInstallationCounttcConsumption,installationConsumptionlossPercentage,previousMonthLossPercentage,previousToPreviousMonthLossPercentageauditStatus,auditedByAI,remark,overloadedhealthScore,healthGradelocation(all fields)
5.6 Pagination Response
{
page,
limit,
totalCount,
totalPages: Math.ceil(totalCount / limit)
}
6. Validation Schema (AJV)
6.1 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
},
auditStatus: {
type: "string",
enum: ["AUDITED", "AUDIT_FAILED"]
},
lossBucket: {
type: "string",
enum: [
"belowMinus5", "minus5to0", "zeroTo5",
"fiveTo10", "tenTo15", "fifteenTo20", "above20"
]
},
failedReason: {
type: "string",
enum: [
"noMeterConstant",
"withoutInstallationConsumption",
"noTcConsumptionWithRemarks",
"noTcConsumptionNoRemarks"
]
},
search: {
type: "string",
transform: ["trim"],
minLength: 1
},
sectionCode: { type: "string", transform: ["trim"] },
subDivisionCode: { type: "string", transform: ["trim"] },
divisionCode: { type: "string", transform: ["trim"] },
circleCode: { type: "string", transform: ["trim"] },
zoneCode: { type: "string", transform: ["trim"] }
},
additionalProperties: false
}
6.2 Response Serialization
{
"2xx": responseSerializer("2xx", {
type: "object",
properties: {
transformers: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
number: { type: "string" },
name: { type: "string" },
month: { type: "string" },
tcCapacity: { type: ["number", "null"] },
meterConstant: { type: ["number", "null"] },
installationCount: { type: ["number", "null"] },
untaggedInstallationCount: { type: ["number", "null"] },
tcConsumption: { type: ["number", "null"] },
installationConsumption: { type: ["number", "null"] },
lossPercentage: { type: ["number", "null"] },
previousMonthLossPercentage: { type: ["number", "null"] },
previousToPreviousMonthLossPercentage: { type: ["number", "null"] },
auditStatus: { type: ["string", "null"], enum: ["AUDITED", "AUDIT_FAILED", null] },
auditedByAI: { type: "boolean" },
remark: { type: ["string", "null"] },
overloaded: { type: "boolean" },
healthScore: { type: ["number", "null"] },
healthGrade: { type: ["string", "null"], enum: ["A", "B", "C", "D", "F", 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" }
}
}
}
}
},
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: "fetchTransformers",
authentication: {
// No excludeFrom — requires full auth
},
authorization: {
resource: "transformer",
action: "view",
id: "idNotApplicable"
}
}
8. File Structure
api/src/entities/audit/
audit.route.v1.js — Route definitions
controller/
fetchAuditSummary.js — Summary API handler
fetchTransformers.js — TC list API handler
helpers/
resolveLocationFilter.js — Shared location resolution logic
lossBucketRanges.js — Loss bucket → { $gte, $lt } mapping
9. Error Handling
| Scenario | Status | Error Type |
|---|---|---|
| Invalid month format | 400 | ValidationError (AJV) |
| Invalid auditStatus/lossBucket/failedReason enum | 400 | ValidationError (AJV) |
lossBucket + failedReason both sent | 400 | ValidationError (custom) |
| Invalid/expired session | 401 | UnauthorizedError |
| AE user doc not found | 500 | DatabaseError |
| DB connection failure | 500 | DatabaseError |
10. Performance Notes
- Indexes used:
- With
auditStatus+ location:{ month, location.sectionCode, auditStatus }compound index - With
lossBucket(loss range):{ month, auditStatus, lossPercentage }compound index - With
search: regex onname— no index support for prefix regex with case-insensitive. For 150K TCs, consider adding a separate lowercasenameLowerfield with a regular index if performance is an issue.
- With
countDocumentsruns in parallel withfindto avoid sequential latency.- AE users require one extra DB call (user lookup) — same as summary API.
- Pagination: offset-based (skip/limit). Acceptable for this dataset size (~150K TCs). If deep pagination becomes slow, consider cursor-based pagination as a future optimization.
11. Regex Escape Utility
The search param is user input used in a $regex query. Special regex characters must be escaped to prevent regex injection:
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}