Skip to main content

LLD: TC Health Score

1. Objective

Compute a composite health score (0–100) for each Transformer Centre (TC) based on five dimensions: energy loss, billing health, audit compliance, loading, and loss trend. The score provides a single, intuitive metric for quickly assessing TC condition in the AI Audit views.


2. Health Score Dimensions

The score is a weighted sum of five sub-scores. Each sub-score is computed independently from fields already present on the transformer document — no additional DB queries required.

#DimensionWeightSource Fields
1Loss40lossPercentage
2Billing Health25billingCount.*, installationCount
3Audit Compliance15auditStatus, remark
4Loading10totalInstallationSanctionedLoadInKVA, tcCapacity
5Trend10lossPercentage, previousMonthLossPercentage

Total: 100 points


3. Sub-Score Computation

3.1 Loss Score (max 40 points)

The primary metric. Ideal range is 0–3% loss. Both high loss and excessive negative loss (energy appearing from nowhere) are penalised.

lossPercentage RangePointsRationale
0% to 3%40Ideal — minimal loss
3% to 5%35Acceptable
-5% to 0%30Minor negative loss — meter/billing variance
5% to 10%20Moderate loss — needs attention
10% to 15%10High loss
< -5%5Excessive negative — likely metering issue
15% to 20%5Very high loss
>= 20%0Critical loss
null0Not computable (audit failed)

Logic (pseudocode):

IF lossPercentage is null → 0
IF 0 <= loss < 3 → 40
IF 3 <= loss < 5 → 35
IF -5 <= loss < 0 → 30
IF 5 <= loss < 10 → 20
IF 10 <= loss < 15 → 10
IF loss < -5 → 5
IF 15 <= loss < 20 → 5
IF loss >= 20 → 0

The ordering above is intentional — check the most common/ideal ranges first for early return.

3.2 Billing Health Score (max 25 points)

Measures the proportion of installations that are in a healthy billing state. An installation is "unhealthy" if it has any billing flag set (unbilled, MNR, vacant, zero consumption, doorlock, abnormal, subnormal, bill cancellation).

Formula:

unhealthyCount = billingCount.unbilled + billingCount.mnr + billingCount.vacant
+ billingCount.zeroConsumption + billingCount.doorlock
+ billingCount.abnormal + billingCount.subnormal
+ billingCount.billCancellation

IF installationCount === 0 → 0 points (no installations tagged)

healthyRatio = max(0, (installationCount - unhealthyCount)) / installationCount
billingScore = round(healthyRatio * 25, 2)

unhealthyCount can exceed installationCount because one installation can have multiple flags (e.g., both unbilled and zeroConsumption). The max(0, ...) guard prevents negative scores.

3.3 Audit Compliance Score (max 15 points)

Rewards TCs that pass audit and penalises those with hardware/meter remarks.

ConditionPointsRationale
auditStatus === "AUDITED" AND remark is null15Clean audit
auditStatus === "AUDITED" AND remark is not null8Audited but has meter/hardware issue
auditStatus === "AUDIT_FAILED"0Audit failed

3.4 Loading Score (max 10 points)

Based on the ratio of total installation sanctioned load to TC capacity. Overloaded TCs are at risk of failure.

Formula:

IF tcCapacity is null or 0 → 0 points (can't assess loading)

loadRatio = totalInstallationSanctionedLoadInKVA / tcCapacity

IF loadRatio <= 0.8 → 10 (healthy — under 80% capacity)
IF loadRatio <= 1.0 → 7 (near capacity — watch zone)
IF loadRatio <= 1.2 → 3 (overloaded — needs action)
IF loadRatio > 1.2 → 0 (heavily overloaded — critical)

3.5 Trend Score (max 10 points)

Compares current month's loss with the previous month's loss. Improving trend is rewarded.

Formula:

IF previousMonthLossPercentage is null → 5 (no history — neutral)
IF lossPercentage is null → 0 (can't compare)

delta = lossPercentage - previousMonthLossPercentage

IF delta <= -2 → 10 (improving — loss decreased by >= 2 pp)
IF delta <= 2 → 7 (stable — within ±2 pp)
IF delta > 2 → 2 (worsening — loss increased by > 2 pp)

delta is signed: negative means loss decreased (good), positive means loss increased (bad).


4. Health Grade

The numeric score maps to a grade for display in the UI.

Score RangeGradeColourDescription
80–100AGreen (#22c55e)Excellent — no action needed
60–79BLight Green (#84cc16)Good — minor improvements possible
40–59CYellow (#eab308)Fair — needs attention
20–39DOrange (#f97316)Poor — action required
0–19FRed (#ef4444)Critical — immediate action needed

5. Chain of Dependencies

lossPercentage ──────────────────────────────────→ lossScore (40)

billingCount.* + installationCount ──────────────→ billingScore (25)

auditStatus + remark ────────────────────────────→ auditScore (15)

totalInstallationSanctionedLoadInKVA + tcCapacity → loadingScore (10)

lossPercentage + previousMonthLossPercentage ────→ trendScore (10)


healthScore (0–100)


healthGrade (A–F)

All source fields are already present on the transformer document. No additional DB queries needed.


6. Function Signature

File location

api/src/entities/audit/helpers/computeHealthScore.js

Placed alongside computeAuditFields.js — both are pure computation helpers for transformer audit data.

Exports

/**
* Computes a composite health score (0–100) and grade for a TC.
*
* @param {Object} transformer — the TC document (lean or plain object)
* @returns {{ healthScore: number, healthGrade: string }}
*/
export function computeHealthScore(transformer) { ... }

Input (transformer fields used)

FieldTypeRequired
lossPercentageNumber or nullYes
previousMonthLossPercentageNumber or nullYes
installationCountNumberYes
billingCount.unbilledNumberYes
billingCount.mnrNumberYes
billingCount.vacantNumberYes
billingCount.zeroConsumptionNumberYes
billingCount.doorlockNumberYes
billingCount.abnormalNumberYes
billingCount.subnormalNumberYes
billingCount.billCancellationNumberYes
auditStatusStringYes
remarkString or nullYes
totalInstallationSanctionedLoadInKVANumberYes
tcCapacityNumber or nullYes

Output

{
"healthScore": 72,
"healthGrade": "B"
}

healthScore is rounded to nearest integer (0–100).


7. Usage Context

This function is a pure computation helper (no side-effects, no DB access). It can be called:

  1. In computeAuditFields.js — after computing audit fields, call computeHealthScore on the resulting transformer state and include healthScore/healthGrade in the returned fields.
  2. In API response serialization — compute on-the-fly when returning TC data to the frontend.
  3. In seed scripts — when generating test data.

The recommended approach is (1) — compute and store the score alongside other audit fields, so it's available for filtering and sorting without re-computation.


8. Example Calculations

Example 1: Healthy TC

lossPercentage: 2.5 → lossScore: 40
installationCount: 50
unhealthyCount: 3 → billingScore: round((47/50) * 25) = 23.5
auditStatus: "AUDITED"
remark: null → auditScore: 15
tcCapacity: 100
sanctionedLoad: 65 → loadRatio: 0.65 → loadingScore: 10
previousMonthLossPercentage: 3.1
delta: 2.5 - 3.1 = -0.6 → trendScore: 7 (stable, within ±2)

healthScore = 40 + 23.5 + 15 + 10 + 7 = 96
healthGrade = "A" (Excellent)

Example 2: Problematic TC

lossPercentage: 18.5 → lossScore: 5
installationCount: 30
unhealthyCount: 15 → billingScore: round((15/30) * 25) = 12.5
auditStatus: "AUDITED"
remark: "CT_BURNT_OUT" → auditScore: 8
tcCapacity: 63
sanctionedLoad: 80 → loadRatio: 1.27 → loadingScore: 0
previousMonthLossPercentage: 12.0
delta: 18.5 - 12.0 = 6.5 → trendScore: 2 (worsening)

healthScore = 5 + 12.5 + 8 + 0 + 2 = 28
healthGrade = "D" (Poor)

Example 3: Failed TC

lossPercentage: null → lossScore: 0
installationCount: 0 → billingScore: 0
auditStatus: "AUDIT_FAILED" → auditScore: 0
tcCapacity: 100
sanctionedLoad: 0 → loadRatio: 0 → loadingScore: 10
previousMonthLossPercentage: null → trendScore: 0 (loss is null)

healthScore = 0 + 0 + 0 + 10 + 0 = 10
healthGrade = "F" (Critical)

9. Edge Cases

ScenarioHandling
lossPercentage is nullLoss score = 0, trend score = 0
previousMonthLossPercentage is null but lossPercentage existsTrend score = 5 (neutral)
installationCount is 0Billing score = 0
unhealthyCount > installationCount (multi-flag)max(0, ...) prevents negative billing score
tcCapacity is null or 0Loading score = 0
All fields are null/zero (completely broken TC)Score = 0, Grade = "F"
lossPercentage is exactly on a boundary (e.g., 3.0)Uses >= lower bound — 3.0 falls in 3–5% range (35 points)

10. Storage & Schema

Two new fields added to transformer.model.js:

healthScore: {
type: Number, // 0–100 composite score
},
healthGrade: {
type: String, // A, B, C, D, F
enum: ["A", "B", "C", "D", "F"],
},

These are stored on the transformer document (not computed on-the-fly) so they can be used for filtering and sorting in list views.


11. Backfill Script

File location

script/src/entities/transformer/backfillHealthScore.js

Invocation

Config name: transformer_backfillHealthScore

In config.json:

{
"scriptRunArray": ["transformer_backfillHealthScore"],
"input": {
"transformer": {
"backfillHealthScore": {
"months": ["2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"],
"batchSize": 500
}
}
}
}

If months is omitted, the script auto-discovers all distinct months from the transformer collection.

Algorithm

Phase 1: Resolve target months
Input: config.months (optional)
If not provided → query transformer.distinct("month")

Phase 2: For each month
Open a cursor on transformer collection (lean, projected to health-relevant fields only)
For each TC:
Call computeHealthScore(tc) → { healthScore, healthGrade }
Add to bulkWrite batch
Flush batch every 500 docs via bulkWrite({ ordered: false })

Phase 3: Log summary per month

Performance Estimate

OperationData sizeExpected time
Cursor read + compute per month~150K TCs~5–10 seconds
bulkWrite per month~300 batches of 500~3–5 seconds
Total (7 months)~1M TCs~60–100 seconds

12. Re-audit Integration

Health score is recalculated during the freeze flow for both target and source TCs.

Target TC (in freezeAiAudit.js)

The target TC computes its audit fields inline. After computing lossPercentage, billingCount, etc., computeHealthScore is called directly and the result is included in the $set update:

const { healthScore, healthGrade } = computeHealthScore({
lossPercentage,
previousMonthLossPercentage: transformer.previousMonthLossPercentage,
installationCount,
billingCount,
auditStatus: "AUDITED",
remark: transformer.remark,
totalInstallationSanctionedLoadInKVA,
tcCapacity: transformer.tcCapacity,
});

// Included in the $set alongside other audit fields

Source TCs (via computeAuditFields.js)

Source TCs (that lost installations) are reaudited via computeAuditFields(). This function now calls computeHealthScore internally and includes healthScore/healthGrade in its return value. No changes needed in freezeAiAudit.js for source TCs — they get health scores automatically.

Flow Diagram

Freeze AI Audit
├── Target TC
│ ├── Compute audit fields inline
│ ├── computeHealthScore() ← NEW
│ └── $set { ...auditFields, healthScore, healthGrade }

├── Source TCs (for each)
│ ├── computeAuditFields(sourceTransformer, sourceInstallations)
│ │ └── internally calls computeHealthScore() ← NEW
│ └── $set { ...sourceAuditFields } (now includes healthScore, healthGrade)

└── Cascade audit summaries (unchanged)