LLD: Seed AI Audit Test Data
1. Objective
Generate a small, self-contained test dataset for a single section that exercises the entire AI Audit flow end-to-end. The seeded data creates 15 Transformer Centres with ~150 installations across 7 months (2025-09 to 2026-03), designed so that running the 4 AI Audit APIs (Run, Fetch Installations, Fetch Remarks, Freeze) produces meaningful results covering all code paths, GPS classification sets, remark types, and edge cases.
2. Background
The AI Audit feature depends on realistic data relationships between transformers and installations:
-
GPS distance classification -- the Run API uses
$geoNearto classify installations into 4 sets (A: tagged+in-range, B: tagged+out-range, C: untagged+in-range, Remaining: untagged+out-range). This requires installations with valid GPS coordinates at varying distances from the target TC. -
Remark-based consumption analysis -- the Run API derives
remarkTypeandaiSuggestsConsumptionfrom billing flags. This requires installations with all 7 remark types and pre-computedavgConsumption6Monthsvalues. -
Freeze cascade -- the Freeze API updates real transformer/installation docs, reaudits source TCs, updates adjacent month loss percentages, and recomputes audit summaries. This requires multiple TCs with cross-TC installation references.
-
Edge cases -- AUDIT_FAILED TCs (meterConstant=0, no consumption), already-AI-audited TCs, overloaded TCs, all loss bucket ranges, and TC-level remarks.
Without this seed data, manual testing of the AI Audit flow requires either migrating production data (3.3M installations, slow and unwieldy) or manually inserting documents (error-prone and incomplete). This script creates a minimal dataset that covers all testing scenarios.
3. API Endpoint
The seed script was migrated to an API endpoint. Invoke it with:
POST /dtcea-mumbai-demo/v1/seed/ai-audit-test-data
Body: { "clearExisting": true }
If clearExisting is false or omitted, Phase 1 (cleanup) is skipped and data is inserted directly. Set to true for idempotent re-runs.
Implementation: api/src/entities/seed/controller/seedAiAuditTestData.js
4. Location Hierarchy
One complete chain from Zone to Section:
| Level | Code | Name |
|---|---|---|
| Zone | Z01 | Mysuru Zone |
| Circle | C01 | Mysuru Circle |
| Division | D01 | Mysuru Division |
| SubDivision | SD01 | Mysuru SubDivision |
| Section | S01 | Mysuru Section |
All transformers, installations, and the AE user share this location:
const LOCATION = {
sectionCode: "S01",
sectionName: "Mysuru Section",
subDivisionCode: "SD01",
subDivisionName: "Mysuru SubDivision",
divisionCode: "D01",
divisionName: "Mysuru Division",
circleCode: "C01",
circleName: "Mysuru Circle",
zoneCode: "Z01",
zoneName: "Mysuru Zone",
};
5. Users
Two users matching the demo app's simplified role model (Admin + AE):
| Field | USR-ADMIN | USR-AE-01 |
|---|---|---|
userId | USR-ADMIN | USR-AE-01 |
userType | CIO | AE |
loginUserName | admin | ae01 |
name | Admin User | AE Mysuru |
loginPassword | argon2.hash("password123") | argon2.hash("password123") |
location | (none -- CIO is org-wide) | Full S01 chain (sectionCode through zoneCode) |
createdBy | { userId: "SYSTEM", userType: "SUPER_ADMIN" } | { userId: "USR-ADMIN", userType: "CIO" } |
lastUpdatedBy | { userId: "SYSTEM", userType: "SUPER_ADMIN" } | { userId: "USR-ADMIN", userType: "CIO" } |
Password hashing is done at runtime using argon2.hash("password123") (not hardcoded hashes).
6. Transformer Definitions
6.1 Months
const MONTHS = ["2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"];
Total documents: 15 TCs x 7 months = 105 transformer docs.
6.2 TC Definitions (15 TCs)
GPS center point: Mysuru area (latitude: 12.3100, longitude: 76.6500).
| # | id | number | name | GPS [lng, lat] | tcCapacity | meterConstant | Purpose |
|---|---|---|---|---|---|---|---|
| 1 | TC-01 | MYS-TC-001 | TC MYS-TC-001 | [76.6500, 12.3100] | 100 | 1.0 | Primary AI audit target -- center TC with mixed installations |
| 2 | TC-02 | MYS-TC-002 | TC MYS-TC-002 | [76.6520, 12.3120] | 63 | 1.0 | Nearby (~250m) -- source for Set C |
| 3 | TC-03 | MYS-TC-003 | TC MYS-TC-003 | [76.6540, 12.3080] | 100 | 0.1 | Nearby (~450m) -- another source for Set C |
| 4 | TC-04 | MYS-TC-004 | TC MYS-TC-004 | [76.7100, 12.3100] | 200 | 1.0 | Far TC (~6.5km) -- source for Set B |
| 5 | TC-05 | MYS-TC-005 | TC MYS-TC-005 | [76.7200, 12.3200] | 200 | 1.0 | Far TC (~8km) -- installations far from TC-01 |
| 6 | TC-06 | MYS-TC-006 | TC MYS-TC-006 | [76.6510, 12.3090] | 63 | 1.0 | Near (~150m) -- cross-TC freeze conflict |
| 7 | TC-07 | MYS-TC-007 | TC MYS-TC-007 | [76.6505, 12.3105] | 25 | 1.0 | Near -- overloaded (sanctionedLoad > capacity) |
| 8 | TC-08 | MYS-TC-008 | TC MYS-TC-008 | [76.6530, 12.3110] | 100 | 0 | AUDIT_FAILED -- meterConstant = 0 |
| 9 | TC-09 | MYS-TC-009 | TC MYS-TC-009 | [76.6515, 12.3095] | 63 | 1.0 | AUDIT_FAILED -- installationConsumption = 0 |
| 10 | TC-10 | MYS-TC-010 | TC MYS-TC-010 | [76.6525, 12.3115] | 100 | 10 | High loss (>20%) TC |
| 11 | TC-11 | MYS-TC-011 | TC MYS-TC-011 | [76.6495, 12.3090] | 63 | 1.0 | Negative loss (<-5%) TC |
| 12 | TC-12 | MYS-TC-012 | TC MYS-TC-012 | [76.6480, 12.3080] | 100 | 1.0 | Already AI-audited (auditedByAI: true) -- should block re-audit |
| 13 | TC-13 | MYS-TC-013 | TC MYS-TC-013 | [76.6510, 12.3100] | 63 | 1.0 | TC with remark METER_NOT_RECORDING |
| 14 | TC-14 | MYS-TC-014 | TC MYS-TC-014 | [76.6520, 12.3090] | 100 | 1.0 | All normal installations (no remarks, all in-range) |
| 15 | TC-15 | MYS-TC-015 | TC MYS-TC-015 | [76.6535, 12.3105] | 63 | 1.0 | Zero TC consumption (AUDIT_FAILED with remark NO_DATA) |
6.3 TC Field Values -- First Month (2025-09)
All TCs share these base fields:
const baseTCFields = {
location: LOCATION,
readingDay: 15,
readingMR: "MR-S01-01",
// New TC detail fields
tcMake: "Kirloskar",
serialNumber: "KR-2024-XXXX", // XXXX replaced per TC
timsCode: "TIMS-MYS-XXXX",
dtlms: "DTLMS-MYS-001",
dtr: "DTR-MYS-001",
feeder: "MYS-FDR-01",
executionType: "Open Billing",
meterMake: "Secure Meters",
meterSerial: "SM-2024-XXXX",
ctRatio: "200/5",
};
Detailed first-month values per TC:
| TC | reading.initial | reading.final | tcConsumption | instConsumption | instCount | lossPercentage | auditStatus | auditedByAI | overloaded | remark | totalInstSanctionedLoadInKVA |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-01 | 1000 | 1720 | 720 | 600 | 11 | 16.67 | AUDITED | false | false | null | 55 |
| TC-02 | 2000 | 2300 | 300 | 540 | 8 | -80.00 | AUDITED | false | false | null | 40 |
| TC-03 | 5000 | 5400 | 400 | 530 | 7 | -1225.00 | AUDITED | false | false | null | 35 |
| TC-04 | 3000 | 3800 | 800 | 770 | 15 | 3.75 | AUDITED | false | false | null | 75 |
| TC-05 | 4000 | 4600 | 600 | 570 | 12 | 5.00 | AUDITED | false | false | null | 60 |
| TC-06 | 1500 | 1970 | 470 | 475 | 7 | -1.06 | AUDITED | false | false | null | 35 |
| TC-07 | 800 | 1050 | 250 | 220 | 8 | 12.00 | AUDITED | false | true | null | 30 |
| TC-08 | 6000 | 6500 | 500 | 400 | 10 | null | AUDIT_FAILED | false | false | null | 50 |
| TC-09 | 900 | 1200 | 300 | 0 | 5 | null | AUDIT_FAILED | false | false | null | 25 |
| TC-10 | 100 | 110 | 10 | 60 | 8 | 40.00 | AUDITED | false | false | null | 40 |
| TC-11 | 200 | 350 | 150 | 200 | 6 | -33.33 | AUDITED | false | false | null | 30 |
| TC-12 | 3000 | 3400 | 400 | 360 | 9 | 10.00 | AUDITED | true | false | null | 45 |
| TC-13 | 0 | 0 | 0 | 180 | 6 | null | AUDIT_FAILED | false | false | METER_NOT_RECORDING | 30 |
| TC-14 | 2500 | 2900 | 400 | 390 | 10 | 2.50 | AUDITED | false | false | null | 50 |
| TC-15 | 0 | 0 | 0 | 0 | 3 | null | AUDIT_FAILED | false | false | NO_DATA | 15 |
Notes:
- TC-01
lossPercentage:((720 * 1.0) - 600) / (720 * 1.0) * 100 = 16.67(15% to 20% bucket) - TC-02
lossPercentage:((300 * 1.0) - 540) / (300 * 1.0) * 100 = -80.00(below -5% bucket) - TC-03
lossPercentage:((400 * 0.1) - 530) / (400 * 0.1) * 100 = -1225.00(below -5% bucket, amplified by meterConstant=0.1) - TC-04
lossPercentage:((800 * 1.0) - 770) / (800 * 1.0) * 100 = 3.75(0% to 5% bucket) - TC-05
lossPercentage:((600 * 1.0) - 570) / (600 * 1.0) * 100 = 5.00(5% to 10% bucket) - TC-06
lossPercentage:((470 * 1.0) - 475) / (470 * 1.0) * 100 = -1.06(negative loss, -5% to 0% bucket) - TC-07 is
overloaded: truebecausetotalInstSanctionedLoadInKVA (30) > tcCapacity (25) - TC-08 has
meterConstant: 0, solossPercentageis null andauditStatus: AUDIT_FAILED - TC-09 has
installationConsumption: 0(all tagged installations have zero consumption),auditStatus: AUDIT_FAILED - TC-10
lossPercentage:((10 * 10) - 60) / (10 * 10) * 100 = 40.00(high loss >20%) - TC-11
lossPercentage:((150 * 1.0) - 200) / (150 * 1.0) * 100 = -33.33(negative loss <-5%) - TC-12
lossPercentage:((400 * 1.0) - 360) / (400 * 1.0) * 100 = 10.00(10% to 15% bucket) - TC-13 has
remark: "METER_NOT_RECORDING", TC-15 hasremark: "NO_DATA" - TC-14
lossPercentage:((400 * 1.0) - 390) / (400 * 1.0) * 100 = 2.50(0% to 5% bucket) - The script uses
baseConsumptionfor month 0 (2025-09) andconsumptionPattern[monthIndex]for months 1-6
6.4 Loss Bucket Distribution
The 15 TCs are designed to cover all 7 loss buckets (for the AUDITED TCs):
| Loss Bucket | TCs | Loss % |
|---|---|---|
| Below -5% | TC-02, TC-03, TC-11 | -80.00, -1225.00, -33.33 |
| -5% to 0% | TC-06 | -1.06 |
| 0% to 5% | TC-04, TC-14 | 3.75, 2.50 |
| 5% to 10% | TC-05 | 5.00 |
| 10% to 15% | TC-07, TC-12 | 12.00, 10.00 |
| 15% to 20% | TC-01 | 16.67 |
| Above 20% | TC-10 | 40.00 |
Note on bucket boundaries: Per computeAuditSummary.js, bucket boundaries use [lower, upper) -- e.g., 0 <= loss < 5 for the "0 to 5%" bucket, 5 <= loss < 10 for the "5 to 10%" bucket, etc. See Section 6.3 for exact values.
6.5 Month-to-Month Variation Pattern
For months 2025-10 through 2026-03, vary TC data from the base (2025-09) values as follows:
// For each TC, each month after 2025-09:
// reading.initialValue = previous month's finalValue
// reading.finalValue = initialValue + tcConsumption * variationFactor
// variationFactor cycles: [1.0, 1.05, 0.95, 1.10, 0.90, 1.02]
// installationConsumption recalculated from actual tagged installations
const VARIATION_FACTORS = [1.0, 1.05, 0.95, 1.10, 0.90, 1.02];
This creates natural month-to-month variation in:
tcConsumption(varies by +/- 10%)lossPercentage(changes as consumption varies)previousMonthLossPercentage(set to prior month'slossPercentage)previousToPreviousMonthLossPercentage(set to 2 months prior,nullfor 2025-09 and 2025-10)
The variation factors ensure that across 7 months, some TCs will naturally drift into currently-empty loss buckets (e.g., -5% to 0%, 0% to 3%, 3% to 5%).
6.6 billingCount Derivation
For each TC, billingCount is computed from the actual tagged installations for that month:
const billingCount = {
unbilled: 0, mnr: 0, vacant: 0, zeroConsumption: 0,
doorlock: 0, abnormal: 0, subnormal: 0, billCancellation: 0,
};
for (const inst of taggedInstallations) {
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++;
if (inst.emin != null && inst.emin > 0) billingCount.abnormal++;
if (inst.emax != null && inst.emax > 0) billingCount.subnormal++;
}
6.7 revenueParams Derivation
const revenueParams = {
demand: taggedInstallations.reduce((sum, i) => sum + (i.demand || 0), 0),
collection: taggedInstallations.reduce((sum, i) => sum + (i.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;
}
7. Installation Definitions
7.1 Total Installations
~150 installations per month, 7 months = ~1050 installation docs.
All installations share:
const baseInstallationFields = {
location: LOCATION,
mrCode: "MR-S01-01",
status: "ACTIVE",
avgConsumption6Months: null, // populated by backfill script, not by this seed
};
7.2 GPS Set Design for TC-01
When the AI Audit Run API is executed on TC-01 (center GPS: [76.6500, 12.3100]), the installations should classify into all 4 sets:
Set A -- Tagged to TC-01 + Within 5km (NO_CHANGE)
8 installations, GPS clustered near TC-01:
| id | accountId | rrNumber | consumerName | tcId | tcNumber | GPS [lng, lat] | ~Distance | tariff | sanctionedLoad (kw) | consumption | Remark Flags |
|---|---|---|---|---|---|---|---|---|---|---|---|
INST-001 | ACC-001 | RR-001 | Consumer 001 | 01 | MYS-TC-001 | [76.6502, 12.3102] | ~30m | LT2 | 5 | 150 | (none) |
INST-002 | ACC-002 | RR-002 | Consumer 002 | 01 | MYS-TC-001 | [76.6498, 12.3098] | ~30m | LT3 | 5 | 120 | (none) |
INST-003 | ACC-003 | RR-003 | Consumer 003 | 01 | MYS-TC-001 | [76.6505, 12.3095] | ~70m | LT1 | 5 | 0 | unbilled: true |
INST-004 | ACC-004 | RR-004 | Consumer 004 | 01 | MYS-TC-001 | [76.6495, 12.3105] | ~70m | LT4 | 5 | 0 | mnr: true |
INST-005 | ACC-005 | RR-005 | Consumer 005 | 01 | MYS-TC-001 | [76.6510, 12.3100] | ~100m | HT1 | 10 | 0 | vacant: true |
INST-006 | ACC-006 | RR-006 | Consumer 006 | 01 | MYS-TC-001 | [76.6490, 12.3100] | ~100m | LT5 | 5 | 50 | emin: 150 (abnormal) |
INST-007 | ACC-007 | RR-007 | Consumer 007 | 01 | MYS-TC-001 | [76.6503, 12.3097] | ~45m | LT2 | 5 | 50 | emax: 200 (subnormal) |
INST-008 | ACC-008 | RR-008 | Consumer 008 | 01 | MYS-TC-001 | [76.6497, 12.3103] | ~45m | LT1 | 5 | 50 | (none) |
Set A totals for TC-01 (2025-09):
installationCount: 8 (from Set A only, before AI audit adjusts with Set B/C)- Normal consumption sum: 150 + 120 + 0 + 0 + 0 + 50 + 50 + 50 = 420
Note: installation.tcId does NOT include the "TC-" prefix. When transformer.id = "TC-01", the corresponding installation.tcId = "01". The prefix is stripped during comparison (id.replace(/^TC-/, "")).
Set B -- Tagged to TC-01 + Outside 5km (UNTAG)
3 installations, GPS far from TC-01 but currently tagged to TC-01:
| id | accountId | rrNumber | consumerName | tcId | tcNumber | GPS [lng, lat] | ~Distance | tariff | sanctionedLoad (kw) | consumption | Remark Flags |
|---|---|---|---|---|---|---|---|---|---|---|---|
INST-009 | ACC-009 | RR-009 | Consumer 009 | 01 | MYS-TC-001 | [76.7100, 12.3100] | ~6.5km | LT2 | 5 | 80 | (none) |
INST-010 | ACC-010 | RR-010 | Consumer 010 | 01 | MYS-TC-001 | [76.7150, 12.3120] | ~7km | LT3 | 5 | 60 | doorLock: true |
INST-011 | ACC-011 | RR-011 | Consumer 011 | 01 | MYS-TC-001 | [76.7200, 12.3080] | ~7.5km | LT1 | 5 | 40 | zeroConsumption: true |
These are currently tagged to TC-01 (tcId: "01") but their GPS is near TC-04/TC-05. The AI audit should suggest UNTAG for all three.
Note: INST-009/010/011 are part of TC-01's installationCount (11 total = 8 from Set A + 3 from Set B) in the pre-AI-audit state.
Set C -- NOT tagged to TC-01 + Within 5km (TAG)
5 installations, GPS near TC-01 but tagged to other TCs:
| id | accountId | rrNumber | consumerName | tcId | tcNumber | GPS [lng, lat] | ~Distance | tariff | sanctionedLoad (kw) | consumption | Source TC |
|---|---|---|---|---|---|---|---|---|---|---|---|
INST-012 | ACC-012 | RR-012 | Consumer 012 | 02 | MYS-TC-002 | [76.6505, 12.3098] | ~55m from TC-01 | LT2 | 5 | 100 | TC-02 |
INST-013 | ACC-013 | RR-013 | Consumer 013 | 02 | MYS-TC-002 | [76.6508, 12.3102] | ~90m from TC-01 | LT6 | 3 | 80 | TC-02 |
INST-014 | ACC-014 | RR-014 | Consumer 014 | 03 | MYS-TC-003 | [76.6496, 12.3096] | ~55m from TC-01 | LT1 | 5 | 70 | TC-03 |
INST-015 | ACC-015 | RR-015 | Consumer 015 | 06 | MYS-TC-006 | [76.6503, 12.3103] | ~45m from TC-01 | HT2 | 8 | 130 | TC-06 |
INST-016 | ACC-016 | RR-016 | Consumer 016 | 06 | MYS-TC-006 | [76.6499, 12.3097] | ~35m from TC-01 | LT2 | 4 | 90 | TC-06 |
These are within 5km of TC-01 but tagged to TC-02, TC-03, or TC-06. The AI audit should suggest TAG for all five. During freeze, TC-02, TC-03, and TC-06 are the "source TCs" that need reauditing.
Remaining -- NOT tagged to TC-01 + Outside 5km (NO_CHANGE)
All other installations in the section that are tagged to TCs other than TC-01 and are outside 5km from TC-01. These include installations tagged to TC-04 and TC-05, which are ~6.5-8km away from TC-01's GPS. The AI audit should suggest NO_CHANGE for all of these.
7.3 Installations per TC (Full Distribution)
Each TC has its own set of tagged installations. Below are the installation allocations (in addition to the ones above that overlap with TC-01's audit sets):
| TC | Tagged Installation IDs | Count | Notes |
|---|---|---|---|
| TC-01 | INST-001..008 (Set A), INST-009..011 (Set B) | 11 | 8 near + 3 far-away |
| TC-02 | INST-012..013 (Set C from TC-01) + INST-017..022 | 8 | 2 shared with TC-01 audit + 6 own |
| TC-03 | INST-014 (Set C from TC-01) + INST-023..028 | 7 | 1 shared + 6 own |
| TC-04 | INST-029..043 | 15 | All far from TC-01 (near TC-04 GPS) |
| TC-05 | INST-044..055 | 12 | All far from TC-01 (near TC-05 GPS) |
| TC-06 | INST-015..016 (Set C from TC-01) + INST-056..060 | 7 | 2 shared + 5 own |
| TC-07 | INST-061..068 | 8 | Near TC-01; overloaded TC |
| TC-08 | INST-069..078 | 10 | Near TC-01; AUDIT_FAILED (meterConstant=0) |
| TC-09 | INST-079..083 | 5 | Near TC-01; all installations have consumption=0 (patternD) |
| TC-10 | INST-084..091 | 8 | Near TC-01; high loss |
| TC-11 | INST-092..097 | 6 | Near TC-01; negative loss |
| TC-12 | INST-098..106 | 9 | Near TC-01; already AI-audited |
| TC-13 | INST-107..112 | 6 | Near TC-01; has remark |
| TC-14 | INST-113..122 | 10 | Near TC-01; all normal |
| TC-15 | INST-123..125 | 3 | Near TC-01; zero TC consumption; all installations have consumption=0 (patternD) |
| Untagged | INST-126..135 | 10 | tcId: null, tcNumber: null -- scattered GPS |
| Total | 145 |
7.4 Remark Distribution
~30% of installations (~44) have remark flags. Remarks are CONSISTENT across months (the same installation always has the same remark type). Remark assignments:
| Remark Type | Flag(s) | Installation IDs | Count |
|---|---|---|---|
unbilled | unbilled: true | INST-003, INST-029, INST-044, INST-069, INST-126 | 5 |
mnr | mnr: true | INST-004, INST-030, INST-045, INST-079, INST-127 | 5 |
vacant | vacant: true | INST-005, INST-031, INST-080, INST-128 | 4 |
doorLock | doorLock: true | INST-010, INST-032, INST-046, INST-129 | 4 |
zeroConsumption | zeroConsumption: true | INST-011, INST-033, INST-047, INST-081 | 4 |
abnormal | emin: 150 | INST-006, INST-034, INST-048, INST-070 | 4 |
subnormal | emax: 200 | INST-007, INST-035, INST-049, INST-130 | 4 |
| (none) | All flags false/null | All remaining | ~115 |
Total remarked: 30 installations
7.5 Consumption Variation for Remarked Installations
Remarked installations have varying consumption across months to test avgConsumption6Months computation:
// Pattern for remarked installations' consumption across 7 months:
// [monthIndex 0..6] -> consumption value
const REMARK_CONSUMPTION_PATTERNS = {
// Pattern A: Some months with data, some zero
patternA: [0, 120, 0, 150, 0, 130, 0],
// Pattern B: Mostly zero, occasional spike
patternB: [0, 0, 200, 0, 0, 180, 0],
// Pattern C: Increasing trend with gaps
patternC: [80, 0, 100, 0, 120, 0, 140],
// Pattern D: All zero (tests avgConsumption6Months = 0 case)
patternD: [0, 0, 0, 0, 0, 0, 0],
};
// Assignment:
// INST-003, INST-029, INST-069: patternA
// INST-004, INST-030, INST-079: patternB
// INST-005, INST-031, INST-080: patternC
// INST-011, INST-033, INST-081: patternD (all zero -- avg will be 0)
// INST-082, INST-083 (TC-09): patternD (all zero -- ensures TC-09 installationConsumption = 0)
// INST-123, INST-124, INST-125 (TC-15): patternD (all zero -- ensures TC-15 installationConsumption = 0)
// Others: patternA or patternC (alternating)
This ensures that:
- Some remarked installations have
avgConsumption6Months > 0(triggeringaiSuggestsConsumption: true) - Some have
avgConsumption6Months = 0(no suggestion possible) - The average varies per installation based on which months had data
Note: The seed script sets avgConsumption6Months as dummy hardcoded values directly on 16 remarked installations (no separate backfillAvgConsumption step needed). Installations with zeroConsumption flags and Pattern D remain null. The script uses baseConsumption for month 0 (2025-09) and consumptionPattern[monthIndex] for months 1-6.
7.6 Normal Installation Consumption
Non-remarked installations have stable consumption that varies slightly month-to-month:
// Base consumption per installation, varied by month factor
function getConsumption(baseConsumption, monthIndex) {
const factors = [1.0, 1.03, 0.97, 1.05, 0.98, 1.02, 1.01];
return Math.round(baseConsumption * factors[monthIndex]);
}
Base consumption values range from 50 to 300 depending on tariff and sanctioned load.
7.7 Untagged Installations (INST-126 through INST-135)
10 installations with tcId: null and tcNumber: null. These represent installations not yet tagged to any TC. Their GPS coordinates are scattered across the section:
| id | GPS [lng, lat] | ~Distance from TC-01 | Notes |
|---|---|---|---|
| INST-126 | [76.6502, 12.3098] | ~30m | Within range of TC-01 (Set C candidate) |
| INST-127 | [76.6510, 12.3110] | ~140m | Within range of TC-01 |
| INST-128 | [76.6540, 12.3085] | ~450m | Within range of TC-01 |
| INST-129 | [76.7100, 12.3095] | ~6.5km | Near TC-04 (far from TC-01) |
| INST-130 | [76.7200, 12.3190] | ~7.6km | Near TC-05 (far from TC-01) |
| INST-131 | [76.6480, 12.3070] | ~400m | Within range of TC-01 |
| INST-132 | [76.6520, 12.3120] | ~280m | Within range of TC-01 |
| INST-133 | [76.7150, 12.3050] | ~7.1km | Far from TC-01 |
| INST-134 | [76.6495, 12.3095] | ~70m | Within range of TC-01 |
| INST-135 | [76.6505, 12.3105] | ~70m | Within range of TC-01 |
Untagged installations within 5km of TC-01 are also Set C candidates (in addition to the ones tagged to other TCs). This tests the case where installation.tcId === null and currentTaggingStatus = "UNTAGGED" with sourceTcId: null.
7.8 Installation Field Template
Every installation document follows this structure:
{
id: "INST-NNN",
month: "YYYY-MM",
mrCode: "MR-S01-01",
readingDay: 15, // varies 1-28 for some
accountId: "ACC-NNN",
rrNumber: "RR-NNN",
tariff: { short: "LT2", long: "LT 2 - Domestic" },
consumerName: "Consumer NNN",
sanctionedLoad: { kw: 5, hp: 6.7 },
tcId: "NN", // without "TC-" prefix, or null if untagged
tcNumber: "MYS-TC-NNN", // or null if untagged
consumption: 150, // varies by month
demand: 5000, // consumption * 10 (approx)
collection: 4500, // demand * 0.9 (approx)
location: LOCATION,
gpsCoordinates: { type: "Point", coordinates: [lng, lat] },
status: "ACTIVE",
// Remark flags (all false unless specified in Section 7.4)
unbilled: false,
mnr: false,
vacant: false,
doorLock: false,
zeroConsumption: false,
emin: null,
emax: null,
avgConsumption6Months: null,
}
8. Algorithm
Phase 1: Clear Existing Test Data
If clearExisting: true in config:
const deleteFilter = { "location.sectionCode": "S01" };
const [userResult, tcResult, instResult, summaryResult, aiResultDocs] = await Promise.all([
baseUserModel.deleteMany({
$or: [
{ "location.sectionCode": "S01" },
{ userId: "USR-ADMIN" },
],
}),
transformerModel.deleteMany(deleteFilter),
installationModel.deleteMany(deleteFilter),
auditSummaryModel.deleteMany(deleteFilter),
aiAuditResultModel.find(deleteFilter, { _id: 1 }).lean(),
]);
// Delete ai-audit-installation docs linked to the ai-audit-result docs
if (aiResultDocs.length > 0) {
const aiResultIds = aiResultDocs.map((d) => d._id.toString());
await aiAuditInstallationModel.deleteMany({
aiAuditResultId: { $in: aiResultIds },
});
await aiAuditResultModel.deleteMany(deleteFilter);
}
log.info(`Phase 1: Cleared existing data — users: ${userResult.deletedCount}, `
+ `transformers: ${tcResult.deletedCount}, installations: ${instResult.deletedCount}, `
+ `summaries: ${summaryResult.deletedCount}, ai-audit-results: ${aiResultDocs.length}`);
Phase 2: Insert Users
import argon2 from "argon2";
const hashedPassword = await argon2.hash("password123");
const users = [
{
userId: "USR-ADMIN",
userType: "CIO",
loginUserName: "admin",
loginPassword: hashedPassword,
name: "Admin User",
createdBy: { userId: "SYSTEM", userType: "SUPER_ADMIN" },
lastUpdatedBy: { userId: "SYSTEM", userType: "SUPER_ADMIN" },
},
{
userId: "USR-AE-01",
userType: "AE",
loginUserName: "ae01",
loginPassword: hashedPassword,
name: "AE Mysuru",
location: LOCATION,
createdBy: { userId: "USR-ADMIN", userType: "CIO" },
lastUpdatedBy: { userId: "USR-ADMIN", userType: "CIO" },
},
];
// Insert using discriminator models for proper userType handling
await CIO.create(users[0]);
await AE.create(users[1]);
log.info("Phase 2: Inserted 2 users (CIO + AE)");
Phase 3: Generate and Insert Transformers
const TC_DEFINITIONS = [
// { id, number, gps, tcCapacity, meterConstant, baseReading, baseTcConsumption,
// auditedByAI, overloaded, remark, readingDay }
// ... 15 TC definitions as per Section 6.2 and 6.3
];
const MONTHS = ["2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"];
const allTransformers = [];
for (const tcDef of TC_DEFINITIONS) {
let prevInitial = tcDef.baseReading.initialValue;
let prevFinal = tcDef.baseReading.finalValue;
let prevLoss = null;
let prevPrevLoss = null;
for (let mi = 0; mi < MONTHS.length; mi++) {
const month = MONTHS[mi];
const factor = mi === 0 ? 1.0 : VARIATION_FACTORS[(mi - 1) % VARIATION_FACTORS.length];
const initialValue = mi === 0 ? tcDef.baseReading.initialValue : prevFinal;
const baseTcConsumption = tcDef.baseTcConsumption;
const tcConsumption = mi === 0
? baseTcConsumption
: Math.round(baseTcConsumption * factor);
const finalValue = initialValue + tcConsumption;
// Installation consumption and counts are computed in Phase 4
// after installations are generated. Use placeholder values here.
// A second pass updates these fields after Phase 4.
const tcDoc = {
id: tcDef.id,
number: tcDef.number,
name: `TC ${tcDef.number}`,
month,
location: LOCATION,
readingDay: tcDef.readingDay || 15,
readingMR: "MR-S01-01",
tcCapacity: tcDef.tcCapacity,
meterConstant: tcDef.meterConstant,
meterConstantChanged: false,
gpsCoordinates: { type: "Point", coordinates: tcDef.gps },
reading: { initialValue, finalValue },
tcConsumption,
// Placeholder -- updated after Phase 4
installationConsumption: 0,
installationCount: 0,
lossPercentage: null,
previousMonthLossPercentage: mi >= 1 ? prevLoss : null,
previousToPreviousMonthLossPercentage: mi >= 2 ? prevPrevLoss : null,
auditStatus: null, // computed after
auditedByAI: tcDef.auditedByAI || false,
remark: tcDef.remark || null,
overloaded: false, // computed after
billingCount: { unbilled: 0, mnr: 0, vacant: 0, zeroConsumption: 0,
doorlock: 0, abnormal: 0, subnormal: 0, billCancellation: 0 },
revenueParams: { arr: null, atc: null, demand: 0, collection: 0,
billingEfficiency: null, collectionEfficiency: null },
totalInstallationSanctionedLoadInKVA: 0,
// TC Detail + Meter Detail fields
tcMake: "Kirloskar",
serialNumber: `KR-2024-${tcDef.number}`,
timsCode: `TIMS-${tcDef.number}`,
dtlms: "DTLMS-MYS-001",
dtr: "DTR-MYS-001",
feeder: "MYS-FDR-01",
executionType: "Open Billing",
meterMake: "Secure Meters",
meterSerial: `SM-2024-${tcDef.number}`,
ctRatio: "200/5",
};
allTransformers.push(tcDoc);
prevPrevLoss = prevLoss;
prevLoss = tcDoc.lossPercentage; // updated later in Phase 5
prevFinal = finalValue;
}
}
await transformerModel.insertMany(allTransformers, { ordered: false });
log.info(`Phase 3: Inserted ${allTransformers.length} transformer documents`);
Phase 4: Generate and Insert Installations
const INSTALLATION_DEFINITIONS = [
// { id, accountId, rrNumber, consumerName, tcId, tcNumber, gps,
// tariff, sanctionedLoad, baseConsumption, remarkFlags, consumptionPattern }
// ... 145 installation definitions as per Section 7
];
const allInstallations = [];
for (const instDef of INSTALLATION_DEFINITIONS) {
for (let mi = 0; mi < MONTHS.length; mi++) {
const month = MONTHS[mi];
// Determine consumption for this month
let consumption;
if (mi === 0) {
// Base month (2025-09) -- always use baseConsumption
consumption = instDef.baseConsumption;
} else if (instDef.consumptionPattern) {
// Remarked installation, months 1-6 -- use pattern
consumption = instDef.consumptionPattern[mi];
} else {
// Normal installation -- vary from base
consumption = getConsumption(instDef.baseConsumption, mi);
}
const instDoc = {
id: instDef.id,
month,
mrCode: "MR-S01-01",
readingDay: instDef.readingDay || 15,
accountId: instDef.accountId,
rrNumber: instDef.rrNumber,
tariff: instDef.tariff,
consumerName: instDef.consumerName,
sanctionedLoad: instDef.sanctionedLoad,
tcId: instDef.tcId,
tcNumber: instDef.tcNumber,
consumption,
demand: consumption > 0 ? consumption * 10 : 0,
collection: consumption > 0 ? Math.round(consumption * 9) : 0,
location: LOCATION,
gpsCoordinates: { type: "Point", coordinates: instDef.gps },
status: "ACTIVE",
unbilled: instDef.remarkFlags?.unbilled || false,
mnr: instDef.remarkFlags?.mnr || false,
vacant: instDef.remarkFlags?.vacant || false,
doorLock: instDef.remarkFlags?.doorLock || false,
zeroConsumption: instDef.remarkFlags?.zeroConsumption || false,
emin: instDef.remarkFlags?.emin || null,
emax: instDef.remarkFlags?.emax || null,
avgConsumption6Months: null,
};
allInstallations.push(instDoc);
}
}
await installationModel.insertMany(allInstallations, { ordered: false });
log.info(`Phase 4: Inserted ${allInstallations.length} installation documents`);
Phase 5: Compute and Update Transformer Derived Fields
After installations are inserted, update each transformer document with fields derived from its tagged installations:
for (const month of MONTHS) {
for (const tcDef of TC_DEFINITIONS) {
// Fetch all installations tagged to this TC for this month
const tcIdWithoutPrefix = tcDef.id.replace(/^TC-/, "");
const taggedInstallations = allInstallations.filter(
(i) => i.tcId === tcIdWithoutPrefix && i.month === month
);
const installationCount = taggedInstallations.length;
const installationConsumption = taggedInstallations.reduce(
(sum, i) => sum + (i.consumption || 0), 0
);
// Loss percentage
const effectiveTcConsumption =
(tcDef.baseTcConsumption > 0 ? /* month-adjusted value */ : 0) * tcDef.meterConstant;
let lossPercentage = null;
if (effectiveTcConsumption > 0 && installationConsumption >= 0) {
lossPercentage =
Math.round(
((effectiveTcConsumption - installationConsumption) / effectiveTcConsumption) * 100 * 100
) / 100;
}
// Audit status
let auditStatus;
if (tcDef.meterConstant === 0 || tcDef.remark != null) {
auditStatus = "AUDIT_FAILED";
lossPercentage = null;
} else if (lossPercentage != null) {
auditStatus = "AUDITED";
} else {
auditStatus = "AUDIT_FAILED";
}
// Billing counts
const billingCount = { unbilled: 0, mnr: 0, vacant: 0, zeroConsumption: 0,
doorlock: 0, abnormal: 0, subnormal: 0, billCancellation: 0 };
for (const inst of taggedInstallations) {
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++;
if (inst.emin != null && inst.emin > 0) billingCount.abnormal++;
if (inst.emax != null && inst.emax > 0) billingCount.subnormal++;
}
// Total sanctioned load and overloaded flag
const totalInstallationSanctionedLoadInKVA = taggedInstallations.reduce(
(sum, i) => sum + (i.sanctionedLoad?.kw || 0), 0
);
const overloaded = tcDef.tcCapacity
? totalInstallationSanctionedLoadInKVA > tcDef.tcCapacity
: false;
// Revenue params
const demand = taggedInstallations.reduce((sum, i) => sum + (i.demand || 0), 0);
const collection = taggedInstallations.reduce((sum, i) => sum + (i.collection || 0), 0);
const revenueParams = {
demand, collection, arr: null, atc: null,
billingEfficiency: demand > 0
? Math.round((installationConsumption / demand) * 100 * 100) / 100
: null,
collectionEfficiency: installationConsumption > 0
? Math.round((collection / installationConsumption) * 100 * 100) / 100
: null,
};
await transformerModel.updateOne(
{ id: tcDef.id, month },
{
$set: {
installationConsumption,
installationCount,
lossPercentage,
auditStatus,
overloaded,
billingCount,
revenueParams,
totalInstallationSanctionedLoadInKVA,
},
}
);
}
}
// Second pass: update previousMonthLossPercentage and previousToPreviousMonthLossPercentage
for (const tcDef of TC_DEFINITIONS) {
for (let mi = 0; mi < MONTHS.length; mi++) {
const updates = {};
if (mi >= 1) {
const prevDoc = await transformerModel
.findOne({ id: tcDef.id, month: MONTHS[mi - 1] }, { lossPercentage: 1 })
.lean();
updates.previousMonthLossPercentage = prevDoc?.lossPercentage ?? null;
}
if (mi >= 2) {
const prevPrevDoc = await transformerModel
.findOne({ id: tcDef.id, month: MONTHS[mi - 2] }, { lossPercentage: 1 })
.lean();
updates.previousToPreviousMonthLossPercentage = prevPrevDoc?.lossPercentage ?? null;
}
if (Object.keys(updates).length > 0) {
await transformerModel.updateOne({ id: tcDef.id, month: MONTHS[mi] }, { $set: updates });
}
}
}
log.info("Phase 5: Updated all transformer derived fields");
Phase 6: Recompute Audit Summaries
Calls recomputeAuditSummaries(LOCATION, month) in parallel for all 7 months. This upserts audit summary documents at all 6 hierarchy levels (SECTION → MESCOM) so the summary cards are immediately usable after seeding.
await Promise.all(MONTHS.map((month) => recomputeAuditSummaries(LOCATION, month)));
log.info(`Phase 6: Recomputed audit summaries for ${MONTHS.length} months`);
Phase 7: Log Summary Statistics
const tcCount = await transformerModel.countDocuments({ "location.sectionCode": "S01" });
const instCount = await installationModel.countDocuments({ "location.sectionCode": "S01" });
const userCount = await baseUserModel.countDocuments({
$or: [{ "location.sectionCode": "S01" }, { userId: "USR-ADMIN" }],
});
const auditedCount = await transformerModel.countDocuments({
"location.sectionCode": "S01",
auditStatus: "AUDITED",
});
const failedCount = await transformerModel.countDocuments({
"location.sectionCode": "S01",
auditStatus: "AUDIT_FAILED",
});
log.info("Phase 7: Seed complete");
log.info(` Users: ${userCount}`);
log.info(` Transformers: ${tcCount} (${auditedCount} AUDITED, ${failedCount} AUDIT_FAILED)`);
log.info(` Installations: ${instCount}`);
9. Internal Structure
mainFunction(input)
├── Phase 1: clearExistingData(clearExisting)
│ ├── deleteUsers()
│ ├── deleteTransformers()
│ ├── deleteInstallations()
│ ├── deleteAuditSummaries()
│ └── deleteAiAuditData()
├── Phase 2: insertUsers()
│ ├── hashPassword()
│ ├── CIO.create()
│ └── AE.create()
├── Phase 3: generateAndInsertTransformers()
│ ├── for each TC_DEFINITION:
│ │ └── for each MONTH:
│ │ └── buildTransformerDoc(tcDef, month, monthIndex)
│ └── transformerModel.insertMany()
├── Phase 4: generateAndInsertInstallations()
│ ├── for each INSTALLATION_DEFINITION:
│ │ └── for each MONTH:
│ │ └── buildInstallationDoc(instDef, month, monthIndex)
│ └── installationModel.insertMany()
├── Phase 5: computeAndUpdateDerivedFields()
│ ├── for each MONTH:
│ │ └── for each TC_DEFINITION:
│ │ ├── aggregateTaggedInstallations()
│ │ ├── computeLossPercentage()
│ │ ├── computeAuditStatus()
│ │ ├── computeBillingCounts()
│ │ ├── computeRevenueParams()
│ │ ├── computeOverloadedFlag()
│ │ └── transformerModel.updateOne()
│ └── updatePreviousMonthLossPercentages()
├── Phase 6: recomputeAuditSummaries() [parallel across all MONTHS]
│ └── recomputeAuditSummaries(LOCATION, month) × 7
└── Phase 7: logSummaryStatistics()
Key Functions
buildTransformerDoc(tcDef, month, monthIndex) --> Object
Generates a single transformer document for one TC for one month. Applies month-to-month variation to readings and consumption. Returns a plain object ready for insertMany.
buildInstallationDoc(instDef, month, monthIndex) --> Object
Generates a single installation document for one installation for one month. Uses the consumption pattern (for remarked) or base consumption with variation factor (for normal). Returns a plain object ready for insertMany.
getConsumption(baseConsumption, monthIndex) --> number
Applies a month-specific variation factor to a base consumption value. Returns a rounded integer.
computeLossPercentage(tcConsumption, meterConstant, installationConsumption) --> number | null
Computes ((tcConsumption * meterConstant) - installationConsumption) / (tcConsumption * meterConstant) * 100, rounded to 2 decimal places. Returns null if tcConsumption * meterConstant === 0.
deriveAuditStatus(tcDef, lossPercentage) --> string
Returns "AUDIT_FAILED" if meterConstant is 0, remark is present, or lossPercentage is null. Otherwise returns "AUDITED".
10. Data Relationships
How transformer fields derive from installations -- these invariants must hold for every TC + month combination:
| Transformer Field | Derivation | Invariant |
|---|---|---|
installationCount | COUNT(installations WHERE tcId = thisTC AND month = M) | Must equal exact count of tagged installations |
installationConsumption | SUM(installation.consumption WHERE tcId = thisTC AND month = M) | Must equal exact sum |
lossPercentage | ((tcConsumption * meterConstant) - installationConsumption) / (tcConsumption * meterConstant) * 100 | Must be mathematically correct |
billingCount.unbilled | COUNT(installations WHERE tcId = thisTC AND month = M AND unbilled = true) | Must match |
billingCount.mnr | COUNT(... AND mnr = true) | Must match |
billingCount.vacant | COUNT(... AND vacant = true) | Must match |
billingCount.zeroConsumption | COUNT(... AND zeroConsumption = true) | Must match |
billingCount.doorlock | COUNT(... AND doorLock = true) | Must match |
billingCount.abnormal | COUNT(... AND emin > 0) | Must match |
billingCount.subnormal | COUNT(... AND emax > 0) | Must match |
totalInstallationSanctionedLoadInKVA | SUM(installation.sanctionedLoad.kw) | Must equal exact sum |
overloaded | totalInstallationSanctionedLoadInKVA > tcCapacity | Boolean check |
revenueParams.demand | SUM(installation.demand) | Must equal exact sum |
revenueParams.collection | SUM(installation.collection) | Must equal exact sum |
previousMonthLossPercentage | lossPercentage from same TC in month M-1 | Must reference correct prior month |
previousToPreviousMonthLossPercentage | lossPercentage from same TC in month M-2 | Must reference correct prior month |
11. Edge Cases
| Scenario | How the Seed Data Handles It |
|---|---|
| TC with meterConstant = 0 | TC-08 has meterConstant: 0, resulting in AUDIT_FAILED and lossPercentage: null |
| TC with zero installationConsumption | TC-09 has all tagged installations with consumption: 0 |
| TC with zero tcConsumption | TC-15 has reading.initialValue = reading.finalValue = 0 and remark: "NO_DATA" |
| Already AI-audited TC | TC-12 has auditedByAI: true -- Run API should return 400 |
| TC with remark | TC-13 has remark: "METER_NOT_RECORDING" -- Run API should return 400 (ineligible) |
| Overloaded TC | TC-07 has totalInstallationSanctionedLoadInKVA > tcCapacity |
| Negative loss percentage | TC-11 has installationConsumption > tcConsumption * meterConstant |
| High loss percentage (>20%) | TC-10 achieves 40% loss via high meterConstant (10) with low TC consumption |
| Untagged installations (tcId = null) | INST-126..135 have tcId: null -- these are Set C candidates with sourceTcId: null |
| Cross-TC installation conflict | TC-06 shares nearby installations (INST-015, INST-016) that are Set C for TC-01 |
| First month has no prior data | 2025-09 has previousMonthLossPercentage: null and previousToPreviousMonthLossPercentage: null |
| Remarked installation with all-zero consumption | INST-011, 033, 081 use patternD (all zeros) -- avgConsumption6Months will be 0 |
| Script re-run (idempotent) | clearExisting: true deletes all S01 data before re-inserting |
12. Post-Seed Steps
Audit summaries are now computed automatically in Phase 6 of the seed — no separate step required.
Note: avgConsumption6Months is set as dummy data directly by the seed script (no separate backfillAvgConsumption step needed).
Verify AI Audit Data
After seeding, run the verification queries from Section 13 to confirm data integrity.
13. Testing Strategy
After running the seed script and post-seed step (audit summary), execute the following verification queries. All tests must pass.
Connection:
mongosh "<MONGO_CONNECTION_STRING>" --quiet
Test 1: Correct document counts
var tcCount = db.transformer.countDocuments({ "location.sectionCode": "S01" });
var instCount = db.installation.countDocuments({ "location.sectionCode": "S01" });
var userCount = db.user.countDocuments({
$or: [{ "location.sectionCode": "S01" }, { userId: "USR-ADMIN" }]
});
print("Transformers:", tcCount, "(expected: 105)");
print("Installations:", instCount, "(expected: ~1015)");
print("Users:", userCount, "(expected: 2)");
print(tcCount === 105 && userCount === 2 ? "PASS" : "FAIL");
Pass criteria: 105 transformers (15 TCs x 7 months), ~1015 installations (145 x 7 months), 2 users.
Test 2: All 15 TCs present in every month
var months = ["2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"];
var pass = true;
months.forEach(function(m) {
var count = db.transformer.countDocuments({ "location.sectionCode": "S01", month: m });
if (count !== 15) {
print("FAIL:", m, "has", count, "TCs (expected 15)");
pass = false;
}
});
print(pass ? "PASS" : "FAIL");
Pass criteria: Exactly 15 TCs per month.
Test 3: installationCount matches actual tagged installations
var pass = true;
var months = ["2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"];
months.forEach(function(m) {
db.transformer.find({ "location.sectionCode": "S01", month: m }).forEach(function(tc) {
var tcIdWithoutPrefix = tc.id.replace(/^TC-/, "");
var actualCount = db.installation.countDocuments({ tcId: tcIdWithoutPrefix, month: m });
if (actualCount !== tc.installationCount) {
print("FAIL:", tc.id, m, "installationCount:", tc.installationCount, "actual:", actualCount);
pass = false;
}
});
});
print(pass ? "PASS" : "FAIL");
Pass criteria: For every TC in every month, installationCount equals the count of installations with matching tcId and month.
Test 4: installationConsumption matches sum of tagged installations
var pass = true;
var months = ["2025-09"]; // Spot-check first month for speed
months.forEach(function(m) {
db.transformer.find({ "location.sectionCode": "S01", month: m }).forEach(function(tc) {
var tcIdWithoutPrefix = tc.id.replace(/^TC-/, "");
var pipeline = [
{ $match: { tcId: tcIdWithoutPrefix, month: m } },
{ $group: { _id: null, total: { $sum: "$consumption" } } }
];
var result = db.installation.aggregate(pipeline).toArray();
var actualSum = result.length > 0 ? result[0].total : 0;
if (actualSum !== tc.installationConsumption) {
print("FAIL:", tc.id, m, "instConsumption:", tc.installationConsumption, "actual:", actualSum);
pass = false;
}
});
});
print(pass ? "PASS" : "FAIL");
Pass criteria: For every TC, installationConsumption equals the sum of tagged installations' consumption.
Test 5: Loss percentage formula is correct
var pass = true;
db.transformer.find({
"location.sectionCode": "S01",
month: "2025-09",
auditStatus: "AUDITED"
}).forEach(function(tc) {
var effectiveTcConsumption = (tc.tcConsumption || 0) * (tc.meterConstant || 1);
if (effectiveTcConsumption === 0) return;
var expectedLoss = Math.round(
((effectiveTcConsumption - tc.installationConsumption) / effectiveTcConsumption) * 100 * 100
) / 100;
if (Math.abs(expectedLoss - tc.lossPercentage) > 0.01) {
print("FAIL:", tc.id, "expected:", expectedLoss, "actual:", tc.lossPercentage);
pass = false;
}
});
print(pass ? "PASS" : "FAIL");
Pass criteria: Every AUDITED TC's lossPercentage matches the formula.
Test 6: AUDIT_FAILED TCs have null lossPercentage
var failedWithLoss = db.transformer.countDocuments({
"location.sectionCode": "S01",
auditStatus: "AUDIT_FAILED",
lossPercentage: { $ne: null }
});
print("AUDIT_FAILED TCs with non-null lossPercentage:", failedWithLoss);
print(failedWithLoss === 0 ? "PASS" : "FAIL");
Pass criteria: failedWithLoss === 0.
Test 7: TC-12 has auditedByAI: true
var tc12 = db.transformer.findOne({ id: "TC-12", month: "2025-09" });
print("TC-12 auditedByAI:", tc12.auditedByAI);
print(tc12.auditedByAI === true ? "PASS" : "FAIL");
Pass criteria: TC-12 has auditedByAI: true.
Test 8: TC-07 is overloaded
var tc07 = db.transformer.findOne({ id: "TC-07", month: "2025-09" });
print("TC-07 overloaded:", tc07.overloaded);
print("TC-07 sanctionedLoad:", tc07.totalInstallationSanctionedLoadInKVA, "capacity:", tc07.tcCapacity);
print(tc07.overloaded === true ? "PASS" : "FAIL");
Pass criteria: TC-07 has overloaded: true and totalInstallationSanctionedLoadInKVA > tcCapacity.
Test 9: All 7 remark types present in installations
var remarkCounts = {
unbilled: db.installation.countDocuments({ "location.sectionCode": "S01", month: "2025-09", unbilled: true }),
mnr: db.installation.countDocuments({ "location.sectionCode": "S01", month: "2025-09", mnr: true }),
vacant: db.installation.countDocuments({ "location.sectionCode": "S01", month: "2025-09", vacant: true }),
doorLock: db.installation.countDocuments({ "location.sectionCode": "S01", month: "2025-09", doorLock: true }),
zeroConsumption: db.installation.countDocuments({ "location.sectionCode": "S01", month: "2025-09", zeroConsumption: true }),
abnormal: db.installation.countDocuments({ "location.sectionCode": "S01", month: "2025-09", emin: { $gt: 0 } }),
subnormal: db.installation.countDocuments({ "location.sectionCode": "S01", month: "2025-09", emax: { $gt: 0 } }),
};
var pass = true;
Object.keys(remarkCounts).forEach(function(key) {
print(key + ":", remarkCounts[key]);
if (remarkCounts[key] === 0) {
print("FAIL: no installations with", key);
pass = false;
}
});
print(pass ? "PASS" : "FAIL");
Pass criteria: Every remark type has at least one installation.
Test 10: GPS coordinates are valid GeoJSON
var invalidGPS = db.transformer.countDocuments({
"location.sectionCode": "S01",
$or: [
{ "gpsCoordinates.type": { $ne: "Point" } },
{ "gpsCoordinates.coordinates": { $exists: false } },
{ "gpsCoordinates.coordinates": { $size: 0 } },
]
});
var invalidInstGPS = db.installation.countDocuments({
"location.sectionCode": "S01",
$or: [
{ "gpsCoordinates.type": { $ne: "Point" } },
{ "gpsCoordinates.coordinates": { $exists: false } },
{ "gpsCoordinates.coordinates": { $size: 0 } },
]
});
print("Invalid TC GPS:", invalidGPS, "Invalid installation GPS:", invalidInstGPS);
print(invalidGPS === 0 && invalidInstGPS === 0 ? "PASS" : "FAIL");
Pass criteria: All GPS coordinates have type: "Point" and non-empty coordinates array.
Test 11: Audit summaries generated after computeAuditSummary
var sectionSummaries = db.getCollection("audit-summaries").countDocuments({
level: "SECTION",
"location.sectionCode": "S01"
});
var mescomSummaries = db.getCollection("audit-summaries").countDocuments({
level: "MESCOM"
});
print("SECTION summaries:", sectionSummaries, "(expected: 7)");
print("MESCOM summaries:", mescomSummaries, "(expected: >= 7)");
print(sectionSummaries === 7 ? "PASS" : "FAIL");
Pass criteria: 7 SECTION-level summaries (one per month) for section S01.
Test 12: previousMonthLossPercentage chain is correct
var pass = true;
var months = ["2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03"];
// Check TC-01 across months
for (var i = 1; i < months.length; i++) {
var currentTC = db.transformer.findOne({ id: "TC-01", month: months[i] });
var prevTC = db.transformer.findOne({ id: "TC-01", month: months[i - 1] });
if (currentTC.previousMonthLossPercentage !== prevTC.lossPercentage) {
print("FAIL: TC-01", months[i], "prevMonthLoss:", currentTC.previousMonthLossPercentage,
"expected:", prevTC.lossPercentage);
pass = false;
}
}
// First month should be null
var firstTC = db.transformer.findOne({ id: "TC-01", month: "2025-09" });
if (firstTC.previousMonthLossPercentage !== null) {
print("FAIL: TC-01 2025-09 should have null previousMonthLossPercentage");
pass = false;
}
print(pass ? "PASS" : "FAIL");
Pass criteria: Each month's previousMonthLossPercentage equals the prior month's lossPercentage. First month is null.
Test 13: Set B installations are tagged to TC-01 but far from it
Verify that the GPS distance setup is correct for AI Audit Set B detection:
// TC-01 GPS
var tc01 = db.transformer.findOne({ id: "TC-01", month: "2025-09" });
var tc01Coords = tc01.gpsCoordinates.coordinates;
// Check Set B candidate: INST-009 should be >5km from TC-01
var inst009 = db.installation.findOne({ accountId: "ACC-009", month: "2025-09" });
print("INST-009 tcId:", inst009.tcId, "(expected: 01)");
print("INST-009 GPS:", JSON.stringify(inst009.gpsCoordinates.coordinates));
// Approximate distance check using Haversine
var R = 6371000; // Earth radius in meters
var lat1 = tc01Coords[1] * Math.PI / 180;
var lat2 = inst009.gpsCoordinates.coordinates[1] * Math.PI / 180;
var dLat = (inst009.gpsCoordinates.coordinates[1] - tc01Coords[1]) * Math.PI / 180;
var dLon = (inst009.gpsCoordinates.coordinates[0] - tc01Coords[0]) * Math.PI / 180;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon/2) * Math.sin(dLon/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var distance = R * c;
print("Approximate distance from TC-01:", Math.round(distance), "meters");
print(inst009.tcId === "01" && distance > 5000 ? "PASS" : "FAIL");
Pass criteria: INST-009 is tagged to TC-01 (tcId: "01") and is more than 5000 meters away.
Test Summary
| # | Test | What it validates |
|---|---|---|
| 1 | Correct document counts | 105 TCs, ~1015 installations, 2 users |
| 2 | All 15 TCs in every month | No missing TC-month combinations |
| 3 | installationCount matches | TC field matches actual count of tagged installations |
| 4 | installationConsumption matches | TC field matches actual sum of tagged installations |
| 5 | Loss percentage formula | Computed correctly from the standard formula |
| 6 | AUDIT_FAILED have null loss | Failed TCs do not have lossPercentage |
| 7 | TC-12 auditedByAI | Pre-flagged TC blocks AI audit re-run |
| 8 | TC-07 overloaded | Overloaded flag is set correctly |
| 9 | All 7 remark types present | Full coverage for AI audit remarks table |
| 10 | Valid GPS coordinates | All GeoJSON is well-formed |
| 11 | Audit summaries generated | Post-seed script produces expected summaries |
| 12 | Previous month loss chain | Month-to-month references are correctly linked |
| 13 | Set B GPS distance | Installations tagged to TC-01 but far away are detectable |
All 13 tests must print PASS for the seed to be considered successful.
14. Key Testing Scenarios the Data Must Support
These are the end-to-end scenarios that the seeded data enables:
| # | Scenario | How to Test | Expected Outcome |
|---|---|---|---|
| 1 | Run AI Audit on TC-01 | POST /audit/transformers/TC-01/ai-audit?month=2025-10 | Produces all 4 GPS sets: Set A (8 tagged+near), Set B (3 tagged+far=UNTAG), Set C (5+untagged near=TAG), Remaining |
| 2 | Run AI Audit on TC-12 | POST /audit/transformers/TC-12/ai-audit?month=2025-10 | 400 -- "AI audit already completed and frozen for this TC" (auditedByAI: true) |
| 3 | Run AI Audit on TC-08 | POST /audit/transformers/TC-08/ai-audit?month=2025-10 | 400 -- "TC is not eligible for AI audit" (meterConstant: 0, AUDIT_FAILED) |
| 4 | Fetch installations for TC-01 | GET /audit/transformers/TC-01/ai-audit/installations?month=2025-10&filter=CHANGES_SUGGESTED | Returns Set B (UNTAG) + Set C (TAG) installations with distances |
| 5 | Fetch remarks for TC-01 | GET /audit/transformers/TC-01/ai-audit/remarks?month=2025-10&filter=ALL | Returns installations with remark flags, filterable by each of the 7 remark types |
| 6 | Freeze TC-01 | POST /audit/transformers/TC-01/freeze with month 2025-10 | Updates real TC-01 doc, retags installations, reaudits source TCs (TC-02, TC-03, TC-06 lose Set C installations; Set B installations get untagged from TC-01) |
| 7 | Freeze cascade -- adjacent months | After freezing TC-01 for 2025-10, check TC-01's doc for 2025-11 | previousMonthLossPercentage updated to TC-01's new 2025-10 loss %; TC-01's 2025-12 doc has previousToPreviousMonthLossPercentage updated |
| 8 | Overloaded TC | Query TC-07 in any month | overloaded: true because totalInstallationSanctionedLoadInKVA > tcCapacity (25 kVA) |
| 9 | Loss bucket coverage | Run computeAuditSummary then check lossBuckets | At least 5 of 7 buckets populated in base month; all 7 covered across all months |
| 10 | Remark type coverage | Fetch remarks for any TC with remarked installations | All 7 types (UNBILLED, MNR, VACANT, ZERO_CONSUMPTION, DOORLOCK, ABNORMAL, SUBNORMAL) present |
| 11 | avgConsumption6Months | After backfill script, check remarked installations in 2026-02 | Installations with consumption patterns A/B/C have avgConsumption6Months > 0; pattern D has avgConsumption6Months = 0 |
| 12 | Cross-TC conflict | Run AI audit on TC-06, then compare with TC-01 audit | TC-06 shares nearby installations (INST-015, INST-016) that appear in both TCs' Set A/C sets. Freezing one affects the other's data |
15. Dependencies
| Dependency | Import path |
|---|---|
transformerModel | ../../../../common/src/schemas/transformer.model.js |
installationModel | ../../../../common/src/schemas/installation.model.js |
auditSummaryModel | ../../../../common/src/schemas/auditSummary.model.js |
aiAuditResultModel | ../../../../common/src/schemas/aiAuditResult.model.js |
aiAuditInstallationModel | ../../../../common/src/schemas/aiAuditInstallation.model.js |
baseUserModel, CIO, AE | ../../../../common/src/schemas/user.model.js |
argon2 | argon2 (already in project dependencies) |
log | ../../utils/logger.js |
assignDefaults | ../../utils/functions.js |
No new dependencies required.