Skip to main content

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:

  1. The target TC document — recomputes audit fields from its tagged installations
  2. Other TC documentspreviousMonthLossPercentage / previousToPreviousMonthLossPercentage on TCs in adjacent months reference this TC's loss %. If the current month's loss changes, the next 2 months' TC docs need updating.
  3. 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).
  4. 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

PhaseScopeThis LLD
Phase 1Reaudit single TC + update audit summariesYes
Phase 2Update adjacent month TC docs (previous/next month loss %)Noted, not implemented
Phase 3AI untag/retag installations → reaudit affected TCsNoted, not implemented

2. Endpoint

RouteMethod
POST /audit/transformers/:id/auditPOST

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

HeaderRequiredDescription
AuthorizationYesBearer <sessionToken>

3.2 Path Parameters

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

3.3 Query Parameters

ParamTypeRequiredDefaultDescription
monthstringNoCurrent 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

ScenarioStatusError Type
Invalid month format400ValidationError (AJV)
Invalid/expired session401UnauthorizedError
AE accessing TC outside their section403ForbiddenError
TC not found for given id + month404ResourceUnavailableError
AE user doc not found500DatabaseError
DB connection failure500DatabaseError
Summary recomputation failure500DatabaseError (non-blocking — TC update already succeeded)

11. Performance Notes

  • Phase 1 DB operations:
    • 2 reads: transformer findOne + installations find (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)
  • 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:

  1. AI identifies installations that are geospatially out of range for their current TC
  2. AI suggests retagging them to a nearby TC
  3. User accepts → API updates tcId on each installation document
  4. Both source TC (lost installations) and destination TC (gained installations) need reaudit
  5. Each reaudit cascades to audit summaries (Phase 1 runs twice)
  6. 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.