LLD: Audit TC API
1. Overview
Re-audits a single transformer centre (TC) for a given month. This is a cascading operation that can affect multiple documents:
- The target TC document — recomputes audit fields from its tagged installations
- Other TC documents —
previousMonthLossPercentage/previousToPreviousMonthLossPercentageon TCs in adjacent months reference this TC's loss %. If the current month's loss changes, the next 2 months' TC docs need updating. - Audit summary documents — section, sub-division, division, circle, zone, and MESCOM-level summaries for the affected month must be recomputed (counts, loss buckets, failed reasons all change).
- Installations (future) — AI-driven untag/retag of installations based on geospatial distance. When installations move between TCs, both source and destination TCs need reaudit, which further cascades to summaries.
Phased Implementation
| Phase | Scope | This LLD |
|---|---|---|
| Phase 1 | Reaudit single TC + update audit summaries | Yes |
| Phase 2 | Update adjacent month TC docs (previous/next month loss %) | Noted, not implemented |
| Phase 3 | AI untag/retag installations → reaudit affected TCs | Noted, not implemented |
2. Endpoint
| Route | Method |
|---|---|
POST /audit/transformers/:id/audit | POST |
Single route — user type (Admin vs AE) resolved from request.user.userType. AE users are validated against their assigned section.
3. Request
3.1 Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <sessionToken> |
3.2 Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The id field of the transformer document (e.g., TC-xxx) |
3.3 Query Parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
month | string | No | Current month (YYYY-MM) | Month to audit the TC for |
4. Response
4.1 Success (200)
{
"success": true,
"message": "Transformer audited successfully",
"data": {
"transformer": {
"id": "TC-uuid-001",
"number": "11F9020",
"name": "TC 11F9020",
"month": "2026-01",
"tcCapacity": 100,
"meterConstant": 1,
"readingDay": 15,
"readingMR": "MR-001",
"totalInstallationSanctionedLoad": 450,
"gpsCoordinates": {
"latitude": 12.8765,
"longitude": 74.8421
},
"reading": {
"initialValue": 1000,
"finalValue": 4010
},
"installationCount": 753,
"untaggedInstallationCount": 5,
"tcConsumption": 3010,
"installationConsumption": 1650,
"lossPercentage": 45.18,
"previousMonthLossPercentage": 15,
"previousToPreviousMonthLossPercentage": 25,
"auditStatus": "AUDITED",
"auditedByAI": true,
"remark": null,
"overloaded": true,
"billingCount": {
"unbilled": 10,
"mnr": 5,
"vacant": 3,
"zeroConsumption": 8,
"doorlock": 2,
"abnormal": 1,
"subnormal": 4,
"billCancellation": 0
},
"revenueParams": {
"arr": 500000,
"atc": 450000,
"demand": 480000,
"collection": 460000,
"billingEfficiency": 96,
"collectionEfficiency": 92
},
"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"
}
}
}
}
4.2 Not Found (404)
{
"success": false,
"message": "Transformer not found"
}
4.3 Forbidden (403)
When AE requests audit on a TC outside their section:
{
"success": false,
"message": "TC does not belong to your assigned section"
}
5. Business Logic
5.1 Default Month
IF month not provided:
month = current date formatted as "YYYY-MM"
5.2 Fetch Transformer
const transformer = await transformerModel
.findOne({ id, month })
.lean();
if (!transformer) {
return 404 — "Transformer not found";
}
5.3 AE Section Validation
Same as fetchTransformerDetail:
IF userType === "AE":
1. Fetch user doc → extract user.location.sectionCode
2. Verify transformer.location.sectionCode === user.location.sectionCode
3. If mismatch → 403 Forbidden
5.4 Fetch Tagged Installations
// Installation tcId doesn't have the "TC-" prefix
const installationTcId = id.replace(/^TC-/, "");
const installations = await installationModel
.find({ tcId: installationTcId, month })
.lean();
5.5 Recompute Audit Fields
// Installation counts
const installationCount = installations.length;
// Installation consumption — sum of all tagged installations
const installationConsumption = installations.reduce(
(sum, inst) => sum + (inst.consumption || 0), 0
);
// TC consumption (from meter readings — already on the document)
const tcConsumption = transformer.tcConsumption;
// Loss percentage
let lossPercentage = null;
const effectiveTcConsumption = (tcConsumption || 0) * (transformer.meterConstant || 1);
if (effectiveTcConsumption !== 0) { // handles negative tcConsumption; 0 yields lossPercentage = null (division by zero)
lossPercentage = ((effectiveTcConsumption - installationConsumption) / effectiveTcConsumption) * 100;
lossPercentage = Math.round(lossPercentage * 100) / 100; // 2 decimal places
}
// Billing counts
const billingCount = {
unbilled: 0, mnr: 0, vacant: 0, zeroConsumption: 0,
doorlock: 0, abnormal: 0, subnormal: 0, billCancellation: 0,
};
for (const inst of installations) {
if (inst.unbilled) billingCount.unbilled++;
if (inst.mnr) billingCount.mnr++;
if (inst.vacant) billingCount.vacant++;
if (inst.zeroConsumption) billingCount.zeroConsumption++;
if (inst.doorLock) billingCount.doorlock++;
// abnormal / subnormal / billCancellation — derive from consumption ranges or flags
}
// Total sanctioned load
const totalInstallationSanctionedLoadInKVA = installations.reduce(
(sum, inst) => sum + (inst.sanctionedLoad?.kw || 0), 0
);
// Overloaded flag
const overloaded = transformer.tcCapacity
? totalInstallationSanctionedLoadInKVA > transformer.tcCapacity
: false;
// Revenue params
const revenueParams = {
demand: installations.reduce((sum, inst) => sum + (inst.demand || 0), 0),
collection: installations.reduce((sum, inst) => sum + (inst.collection || 0), 0),
arr: null,
atc: null,
billingEfficiency: null,
collectionEfficiency: null,
};
if (revenueParams.demand > 0) {
revenueParams.billingEfficiency =
Math.round((installationConsumption / revenueParams.demand) * 100 * 100) / 100;
}
if (installationConsumption > 0) {
revenueParams.collectionEfficiency =
Math.round((revenueParams.collection / installationConsumption) * 100 * 100) / 100;
}
5.6 Determine Audit Status
let auditStatus;
if (transformer.remark) {
// TC has a remark (METER_NOT_RECORDING, CT_BURNT_OUT, etc.) — meter is faulty/absent
auditStatus = "AUDIT_FAILED";
} else if (!transformer.meterConstant) {
auditStatus = "AUDIT_FAILED";
} else if (tcConsumption == null) {
auditStatus = "AUDIT_FAILED";
} else if (installationCount === 0) {
auditStatus = "AUDIT_FAILED";
} else {
auditStatus = "AUDITED";
}
5.7 Update Transformer Document
const updateFields = {
installationCount,
installationConsumption,
lossPercentage,
auditStatus,
auditedByAI: true,
auditedOn: new Date(),
billingCount,
revenueParams,
overloaded,
totalInstallationSanctionedLoadInKVA,
};
const updatedTransformer = await transformerModel
.findOneAndUpdate(
{ id, month },
{ $set: updateFields },
{ new: true, projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0, activatedOn: 0, deletedOn: 0, auditedOn: 0 } }
)
.lean();
5.8 Recompute Audit Summaries (Cascade)
After updating the TC, recompute audit summaries at all 6 levels for the affected location + month. Each level aggregates from all TCs that fall under that location scope.
const { location } = transformer;
// Recompute from bottom up: SECTION → SUB_DIVISION → DIVISION → CIRCLE → ZONE → MESCOM
// Each level aggregates TC counts, loss buckets, and failed reasons
async function recomputeSummaryForLevel(level, locationFilter, month) {
// Count TCs by audit status (only two statuses: AUDITED and AUDIT_FAILED)
const allTCs = await transformerModel.find({ month, ...locationFilter }).lean();
const totalTCCount = allTCs.length;
const auditedCount = allTCs.filter(tc => tc.auditStatus === "AUDITED").length;
const failedCount = totalTCCount - auditedCount;
// Loss buckets (only AUDITED TCs)
const auditedTCs = allTCs.filter(tc => tc.auditStatus === "AUDITED");
const lossBuckets = {
belowMinus5: auditedTCs.filter(tc => tc.lossPercentage < -5).length,
minus5to0: auditedTCs.filter(tc => tc.lossPercentage >= -5 && tc.lossPercentage < 0).length,
zeroTo5: auditedTCs.filter(tc => tc.lossPercentage >= 0 && tc.lossPercentage < 5).length,
fiveTo10: auditedTCs.filter(tc => tc.lossPercentage >= 5 && tc.lossPercentage < 10).length,
tenTo15: auditedTCs.filter(tc => tc.lossPercentage >= 10 && tc.lossPercentage < 15).length,
fifteenTo20: auditedTCs.filter(tc => tc.lossPercentage >= 15 && tc.lossPercentage < 20).length,
above20: auditedTCs.filter(tc => tc.lossPercentage >= 20).length,
};
// Failed reasons (only AUDIT_FAILED TCs)
const failedTCs = allTCs.filter(tc => tc.auditStatus === "AUDIT_FAILED");
const failedReasons = {
noMeterConstant: failedTCs.filter(tc => !tc.meterConstant).length,
withoutInstallationConsumption: failedTCs.filter(tc => tc.installationConsumption == null).length,
noTcConsumptionWithRemarks: failedTCs.filter(tc => tc.tcConsumption == null && tc.remark != null).length,
noTcConsumptionNoRemarks: failedTCs.filter(tc => tc.tcConsumption == null && tc.remark == null).length,
};
// Upsert the summary document
await auditSummaryModel.findOneAndUpdate(
{ level, month, ...locationFilter },
{ $set: { totalTCCount, auditedCount, failedCount, lossBuckets, failedReasons, lastAuditedOn: new Date() } },
{ upsert: true }
);
}
// Section level
await recomputeSummaryForLevel("SECTION",
{ "location.sectionCode": location.sectionCode }, month);
// Sub-division level
await recomputeSummaryForLevel("SUB_DIVISION",
{ "location.subDivisionCode": location.subDivisionCode }, month);
// Division level
await recomputeSummaryForLevel("DIVISION",
{ "location.divisionCode": location.divisionCode }, month);
// Circle level
await recomputeSummaryForLevel("CIRCLE",
{ "location.circleCode": location.circleCode }, month);
// Zone level
await recomputeSummaryForLevel("ZONE",
{ "location.zoneCode": location.zoneCode }, month);
// MESCOM level (org-wide — no location filter)
await recomputeSummaryForLevel("MESCOM", {}, month);
Note: This is a straightforward implementation. For production scale (150K TCs), consider using MongoDB aggregation pipelines instead of loading all TCs into memory. Acceptable for demo with per-section scope (~few hundred TCs).
5.9 Return Updated Transformer
Same response shape as fetchTransformerDetail — the full TC detail with all recomputed fields.
6. Cascading Impact — Full Picture
┌─────────────────────────────────────────────────────────────────────┐
│ POST /audit/transformers/:id/audit │
└─────────────────────────┬───────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ Phase 1 (This LLD) │
└───────────┬───────────┘
│
┌───────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ Update TC │ │ Recompute │ │ Recompute │
│ document │ │ section │ │ higher-level │
│ (1 write) │ │ summary │ │ summaries │
│ │ │ (1 upsert) │ │ (5 upserts) │
└─────────────┘ └─────────────┘ └──────────────────┘
┌───────────────────────┐
│ Phase 2 (Future) │
└───────────┬───────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Update │ │ Update │ │ This TC's │
│ next month │ │ month+2 │ │ loss % is │
│ TC doc: │ │ TC doc: │ │ now stored │
│ prevMonth │ │ prevToPrev │ │ as "previous"│
│ LossPercent │ │ MonthLoss% │ │ in 2 other │
└─────────────┘ └─────────────┘ │ TC docs │
└─────────────┘
┌───────────────────────┐
│ Phase 3 (Future) │
│ AI Retag Flow │
└───────────┬───────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ AI finds │ │ Move install │ │ Reaudit both │
│ installs │────▶│ docs: update │────▶│ source and │
│ out of │ │ tcId on each │ │ destination │
│ GPS range│ │ installation │ │ TCs (recurse │
│ │ │ │ │ to Phase 1) │
└──────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────────┐
│ Both TCs' audit │
│ summaries update │
│ (cascade again) │
└──────────────────┘
7. Validation Schema (AJV)
7.1 Path Parameters Schema
{
type: "object",
properties: {
id: {
type: "string",
transform: ["trim"],
}
},
required: ["id"]
}
7.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" }
}
},
additionalProperties: false
}
7.3 Response Serialization
Same as fetchTransformerDetail response serialization — full TC object with all fields.
8. Route Config
{
routeId: "auditTC",
authentication: {
// No excludeFrom — requires full auth
},
authorization: {
action: "read",
resource: "audit"
}
}
9. File Structure
api/src/entities/audit/
audit.route.v1.js — Add new route
controller/
fetchAuditSummary.js — Existing
fetchTransformers.js — Existing
fetchTransformerDetail.js — Existing (reuse response serialization)
fetchInstallations.js — Existing
auditTC.js — New handler, schema, config exports
helpers/
resolveLocationFilter.js — Existing
lossBucketRanges.js — Existing
computeAuditFields.js — New: reusable audit computation logic
recomputeAuditSummaries.js — New: cascade summary recomputation
10. Error Handling
| Scenario | Status | Error Type |
|---|---|---|
| Invalid month format | 400 | ValidationError (AJV) |
| Invalid/expired session | 401 | UnauthorizedError |
| AE accessing TC outside their section | 403 | ForbiddenError |
TC not found for given id + month | 404 | ResourceUnavailableError |
| AE user doc not found | 500 | DatabaseError |
| DB connection failure | 500 | DatabaseError |
| Summary recomputation failure | 500 | DatabaseError (non-blocking — TC update already succeeded) |
11. Performance Notes
- Phase 1 DB operations:
- 2 reads: transformer
findOne+ installationsfind(all, no pagination) - 1 write: transformer
findOneAndUpdate - 6 writes: audit summary upserts (SECTION, SUB_DIVISION, DIVISION, CIRCLE, ZONE, MESCOM)
- Each summary recompute requires 1 read (TCs for that scope) — consider aggregation pipeline for larger scopes (DIVISION+ may have thousands of TCs)
- 2 reads: transformer
- Installations per TC: average ~22 (3.3M / 150K). Loading all into memory is acceptable.
- AE users require one extra DB call (user lookup) for section validation.
- Summary recomputation could be made async (fire-and-forget) since the TC update is the primary response. For demo, synchronous is fine.
- Index used:
{ tcId: 1, month: 1 }on installations collection covers the main query.
12. Future Considerations
Phase 2 — Adjacent Month Loss %
When a TC's lossPercentage changes for month M:
- Find the same TC's doc for month M+1 → update
previousMonthLossPercentage - Find the same TC's doc for month M+2 → update
previousToPreviousMonthLossPercentage
This is 2 additional findOneAndUpdate calls. Should be added once the core audit flow is stable.
Phase 3 — AI Untag/Retag
The full AI retag flow:
- AI identifies installations that are geospatially out of range for their current TC
- AI suggests retagging them to a nearby TC
- User accepts → API updates
tcIdon each installation document - Both source TC (lost installations) and destination TC (gained installations) need reaudit
- Each reaudit cascades to audit summaries (Phase 1 runs twice)
- If source and destination are in different sections, summaries for both sections update
This requires a separate API (POST /audit/retag) and is out of scope for this LLD.