Skip to main content

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

RouteMethod
GET /audit/transformersGET

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

HeaderRequiredDescription
AuthorizationYesBearer <sessionToken>

3.2 Query Parameters

ParamTypeRequiredDefaultDescription
monthstringNoCurrent month (YYYY-MM)Month to fetch TCs for
pageintegerNo1Page number (1-based)
limitintegerNo10Items per page
auditStatusstringNoAUDITED or AUDIT_FAILED
lossBucketstringNoOne of: belowMinus5, minus5to0, zeroTo5, fiveTo10, tenTo15, fifteenTo20, above20
failedReasonstringNoOne of: noMeterConstant, withoutInstallationConsumption, noTcConsumptionWithRemarks, noTcConsumptionNoRemarks
searchstringNoTC name prefix search (starts with, case-insensitive)
sectionCodestringNoFilter by section (admin only)
subDivisionCodestringNoFilter by sub-division (admin only)
divisionCodestringNoFilter by division (admin only)
circleCodestringNoFilter by circle (admin only)
zoneCodestringNoFilter by zone (admin only)

Filter combinations:

  • lossBucket implies auditStatus: "AUDITED" (loss buckets only apply to audited TCs)
  • failedReason implies auditStatus: "AUDIT_FAILED"
  • If both lossBucket and failedReason are 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 by location.sectionCode
  • subDivisionCode → filter by location.subDivisionCode
  • divisionCode → filter by location.divisionCode
  • circleCode → filter by location.circleCode
  • zoneCode → filter by location.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, month
  • tcCapacity, meterConstant
  • installationCount, untaggedInstallationCount
  • tcConsumption, installationConsumption
  • lossPercentage, previousMonthLossPercentage, previousToPreviousMonthLossPercentage
  • auditStatus, auditedByAI, remark, overloaded
  • healthScore, healthGrade
  • location (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

ScenarioStatusError Type
Invalid month format400ValidationError (AJV)
Invalid auditStatus/lossBucket/failedReason enum400ValidationError (AJV)
lossBucket + failedReason both sent400ValidationError (custom)
Invalid/expired session401UnauthorizedError
AE user doc not found500DatabaseError
DB connection failure500DatabaseError

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 on name — no index support for prefix regex with case-insensitive. For 150K TCs, consider adding a separate lowercase nameLower field with a regular index if performance is an issue.
  • countDocuments runs in parallel with find to 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, "\\$&");
}