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.
| # | Dimension | Weight | Source Fields |
|---|---|---|---|
| 1 | Loss | 40 | lossPercentage |
| 2 | Billing Health | 25 | billingCount.*, installationCount |
| 3 | Audit Compliance | 15 | auditStatus, remark |
| 4 | Loading | 10 | totalInstallationSanctionedLoadInKVA, tcCapacity |
| 5 | Trend | 10 | lossPercentage, 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 Range | Points | Rationale |
|---|---|---|
| 0% to 3% | 40 | Ideal — minimal loss |
| 3% to 5% | 35 | Acceptable |
| -5% to 0% | 30 | Minor negative loss — meter/billing variance |
| 5% to 10% | 20 | Moderate loss — needs attention |
| 10% to 15% | 10 | High loss |
| < -5% | 5 | Excessive negative — likely metering issue |
| 15% to 20% | 5 | Very high loss |
| >= 20% | 0 | Critical loss |
null | 0 | Not 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)
unhealthyCountcan exceedinstallationCountbecause one installation can have multiple flags (e.g., bothunbilledandzeroConsumption). Themax(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.
| Condition | Points | Rationale |
|---|---|---|
auditStatus === "AUDITED" AND remark is null | 15 | Clean audit |
auditStatus === "AUDITED" AND remark is not null | 8 | Audited but has meter/hardware issue |
auditStatus === "AUDIT_FAILED" | 0 | Audit 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)
deltais 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 Range | Grade | Colour | Description |
|---|---|---|---|
| 80–100 | A | Green (#22c55e) | Excellent — no action needed |
| 60–79 | B | Light Green (#84cc16) | Good — minor improvements possible |
| 40–59 | C | Yellow (#eab308) | Fair — needs attention |
| 20–39 | D | Orange (#f97316) | Poor — action required |
| 0–19 | F | Red (#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)
| Field | Type | Required |
|---|---|---|
lossPercentage | Number or null | Yes |
previousMonthLossPercentage | Number or null | Yes |
installationCount | Number | Yes |
billingCount.unbilled | Number | Yes |
billingCount.mnr | Number | Yes |
billingCount.vacant | Number | Yes |
billingCount.zeroConsumption | Number | Yes |
billingCount.doorlock | Number | Yes |
billingCount.abnormal | Number | Yes |
billingCount.subnormal | Number | Yes |
billingCount.billCancellation | Number | Yes |
auditStatus | String | Yes |
remark | String or null | Yes |
totalInstallationSanctionedLoadInKVA | Number | Yes |
tcCapacity | Number or null | Yes |
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:
- In
computeAuditFields.js— after computing audit fields, callcomputeHealthScoreon the resulting transformer state and includehealthScore/healthGradein the returned fields. - In API response serialization — compute on-the-fly when returning TC data to the frontend.
- 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
| Scenario | Handling |
|---|---|
lossPercentage is null | Loss score = 0, trend score = 0 |
previousMonthLossPercentage is null but lossPercentage exists | Trend score = 5 (neutral) |
installationCount is 0 | Billing score = 0 |
unhealthyCount > installationCount (multi-flag) | max(0, ...) prevents negative billing score |
tcCapacity is null or 0 | Loading 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
| Operation | Data size | Expected 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)