Skip to main content

Fix lossPercentage Computation for Non-Positive tcConsumption

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

Goal: Fix a bug where TCs with negative tcConsumption are stored as AUDITED with lossPercentage: null instead of having a correctly computed loss percentage.

Architecture: The fix is a one-character change in two files (> 0!== 0). The guard preventing lossPercentage computation was too strict — it excluded negative tcConsumption values even though the formula is mathematically valid for them (only tcConsumption = 0 should yield lossPercentage = null due to division by zero). Tests are written in the script workspace (only workspace with Jest configured) using a relative import to the pure helper function.

Tech Stack: Node.js ≥22, ESM, Jest 29 with --experimental-vm-modules


Background

The Bug

lossPercentage formula:

lossPercentage = ((tcConsumption * meterConstant) - installationConsumption)
/ (tcConsumption * meterConstant) * 100

In code (computeAuditFields.js and aiAuditTC.js), the guard is:

if (effectiveTcConsumption > 0) { // ← BUG: excludes negative values
lossPercentage = ...
}

Negative tcConsumption is valid (meter read backwards / bad reading). The formula still computes correctly:

tcConsumption=-4526, installationConsumption=16487
lossPercentage = (-4526 - 16487) / -4526 * 100 = 464.3% ✓

Only tcConsumption = 0 should skip the formula (division by zero). auditStatus correctly remains AUDITED even with lossPercentage = null when tcConsumption = 0.

Files to Change

FileLineChange
api/src/entities/audit/helpers/computeAuditFields.js29> 0!== 0
api/src/entities/audit/controller/aiAuditTC.js202> 0!== 0
docs/api/LLD-audit-tc.md209same in pseudocode
docs/api/LLD-ai-audit-run.md427same in pseudocode

Task 1: Fix computeAuditFields.js

Files:

  • Modify: api/src/entities/audit/helpers/computeAuditFields.js:29

Step 1: Make the change

In api/src/entities/audit/helpers/computeAuditFields.js, line 29:

// Before
if (effectiveTcConsumption > 0) {

// After
if (effectiveTcConsumption !== 0) {

No other changes to this file.


Task 2: Fix aiAuditTC.js

Files:

  • Modify: api/src/entities/audit/controller/aiAuditTC.js:202

Step 1: Make the change

In api/src/entities/audit/controller/aiAuditTC.js, line 202:

// Before
if (effectiveTcConsumption > 0) {

// After
if (effectiveTcConsumption !== 0) {

No other changes to this file.


Task 3: Write unit tests for computeAuditFields

Files:

  • Create: script/test/computeAuditFields.test.js

computeAuditFields is a pure function with no external dependencies — perfect for unit testing. Since the api workspace has no Jest setup, we import via relative path into the script workspace which already has Jest + ESM configured.

Step 1: Write the test file

Create script/test/computeAuditFields.test.js:

import { computeAuditFields } from "../../api/src/entities/audit/helpers/computeAuditFields.js";

// Minimal transformer stub — only the fields computeAuditFields reads
function makeTransformer(overrides = {}) {
return {
tcConsumption: 1000,
meterConstant: 1,
remark: null,
tcCapacity: 100,
previousMonthLossPercentage: null,
...overrides,
};
}

// Minimal installation stub
function makeInstallation(consumption = 0) {
return {
consumption,
sanctionedLoad: { kw: 0 },
unbilled: false,
mnr: false,
vacant: false,
zeroConsumption: false,
doorLock: false,
demand: 0,
collection: 0,
};
}

describe("computeAuditFields — lossPercentage", () => {
it("computes lossPercentage correctly for positive tcConsumption", () => {
const transformer = makeTransformer({ tcConsumption: 1000 });
const installations = [makeInstallation(850)];

const result = computeAuditFields(transformer, installations);

expect(result.lossPercentage).toBe(15);
expect(result.auditStatus).toBe("AUDITED");
});

it("computes lossPercentage correctly for negative tcConsumption (bug fix)", () => {
// tcConsumption=-4526, installationConsumption=16487
// lossPercentage = (-4526 - 16487) / -4526 * 100 = 464.3
const transformer = makeTransformer({ tcConsumption: -4526, meterConstant: 1 });
const installations = [makeInstallation(16487)];

const result = computeAuditFields(transformer, installations);

expect(result.lossPercentage).toBe(464.3);
expect(result.auditStatus).toBe("AUDITED");
});

it("sets lossPercentage to null when tcConsumption is 0 (division by zero)", () => {
const transformer = makeTransformer({ tcConsumption: 0 });
const installations = [makeInstallation(1000)];

const result = computeAuditFields(transformer, installations);

expect(result.lossPercentage).toBeNull();
expect(result.auditStatus).toBe("AUDITED"); // 0 is not null — TC is still audited
});

it("sets lossPercentage to 100 when installationConsumption is 0", () => {
const transformer = makeTransformer({ tcConsumption: 1000 });
const installations = [makeInstallation(0)];

const result = computeAuditFields(transformer, installations);

expect(result.lossPercentage).toBe(100);
expect(result.auditStatus).toBe("AUDITED");
});
});

describe("computeAuditFields — auditStatus", () => {
it("returns AUDIT_FAILED when tcConsumption is null", () => {
const transformer = makeTransformer({ tcConsumption: null });
const installations = [makeInstallation(1000)];

const result = computeAuditFields(transformer, installations);

expect(result.auditStatus).toBe("AUDIT_FAILED");
});

it("returns AUDIT_FAILED when there are no installations", () => {
const transformer = makeTransformer({ tcConsumption: 1000 });

const result = computeAuditFields(transformer, []);

expect(result.auditStatus).toBe("AUDIT_FAILED");
});

it("returns AUDIT_FAILED when TC has a remark", () => {
const transformer = makeTransformer({
tcConsumption: 1000,
remark: "METER_NOT_RECORDING",
});
const installations = [makeInstallation(850)];

const result = computeAuditFields(transformer, installations);

expect(result.auditStatus).toBe("AUDIT_FAILED");
});

it("returns AUDIT_FAILED when meterConstant is 0", () => {
const transformer = makeTransformer({ tcConsumption: 1000, meterConstant: 0 });
const installations = [makeInstallation(850)];

const result = computeAuditFields(transformer, installations);

expect(result.auditStatus).toBe("AUDIT_FAILED");
});
});

Step 2: Run the tests to verify they fail before the fix

yarn workspace script test --testPathPattern="computeAuditFields"

Expected: The negative tcConsumption test FAILS (lossPercentage is null, expected 464.3). All other tests should PASS (they test existing correct behaviour).

Note: Run this step BEFORE Task 1 to confirm the test catches the bug. Then run again after Task 1 to confirm it passes.

Step 3: Run after fix to confirm all pass

yarn workspace script test --testPathPattern="computeAuditFields"

Expected output:

PASS script/test/computeAuditFields.test.js
computeAuditFields — lossPercentage
✓ computes lossPercentage correctly for positive tcConsumption
✓ computes lossPercentage correctly for negative tcConsumption (bug fix)
✓ sets lossPercentage to null when tcConsumption is 0 (division by zero)
✓ sets lossPercentage to 100 when installationConsumption is 0
computeAuditFields — auditStatus
✓ returns AUDIT_FAILED when tcConsumption is null
✓ returns AUDIT_FAILED when there are no installations
✓ returns AUDIT_FAILED when TC has a remark
✓ returns AUDIT_FAILED when meterConstant is 0

Test Suites: 1 passed, 1 total
Tests: 8 passed, 8 total

Task 3.5: Write and run one-time DB fix script

Files:

  • Create: script/src/entities/transformer/fixLossPercentageNegativeConsumption.js

This script fixes existing documents in the DB that were audited via the API before the code fix. Those TCs have auditStatus = "AUDITED" with lossPercentage = null because the buggy > 0 guard skipped computation for negative tcConsumption.

The fix recomputes lossPercentage directly from the already-stored tcConsumption, meterConstant, and installationConsumption fields — no need to re-fetch installations.

Step 1: Write the script

Create script/src/entities/transformer/fixLossPercentageNegativeConsumption.js:

"use strict";

import { log } from "../../utils/logger.js";
import { transformerModel } from "../../../../common/src/schemas/transformer.model.js";

export const tags = [];

/**
* ONE-TIME FIX SCRIPT — run once, then delete.
*
* Finds TCs that are AUDITED with lossPercentage=null caused by negative
* tcConsumption (the > 0 guard bug in computeAuditFields). Recomputes
* lossPercentage from stored tcConsumption, meterConstant, installationConsumption.
*
* Executed: 2026-04-02
* Commit: see git log for script creation + deletion commits
*/
export async function mainFunction() {
const affected = await transformerModel
.find({
auditStatus: "AUDITED",
lossPercentage: null,
tcConsumption: { $lt: 0 },
meterConstant: { $exists: true, $ne: null, $ne: 0 },
})
.select("id number month tcConsumption meterConstant installationConsumption")
.lean();

log.info(`Found ${affected.length} TCs to fix`);

if (affected.length === 0) {
log.info("Nothing to fix");
return;
}

let fixed = 0;
for (const tc of affected) {
const effectiveTcConsumption = tc.tcConsumption * (tc.meterConstant || 1);
const installationConsumption = tc.installationConsumption || 0;
const lossPercentage =
Math.round(
((effectiveTcConsumption - installationConsumption) / effectiveTcConsumption) * 100 * 100,
) / 100;

await transformerModel.updateOne(
{ id: tc.id, month: tc.month },
{ $set: { lossPercentage } },
);

log.info({ id: tc.id, number: tc.number, month: tc.month, tcConsumption: tc.tcConsumption, lossPercentage }, "Fixed TC");
fixed++;
}

log.info(`Fixed ${fixed} TCs`);
}

Step 2: Run the script

yarn script-dev
# When prompted, enter: transformer_fixLossPercentageNegativeConsumption

Verify the logs show the affected TCs and their recomputed lossPercentage values.

Step 3: Commit the script

git add script/src/entities/transformer/fixLossPercentageNegativeConsumption.js
git commit -m "fix: one-time script to recompute lossPercentage for AUDITED TCs with negative tcConsumption

Patches existing DB documents that were audited with the buggy > 0 guard.
Recomputes lossPercentage from stored fields. Run on 2026-04-02.
This script will be deleted in the next commit after execution."

Task 3.6: Delete the one-time fix script

Files:

  • Delete: script/src/entities/transformer/fixLossPercentageNegativeConsumption.js

Step 1: Delete the file

rm script/src/entities/transformer/fixLossPercentageNegativeConsumption.js

Step 2: Commit the deletion

git add -u script/src/entities/transformer/fixLossPercentageNegativeConsumption.js
git commit -m "chore: remove one-time fix script fixLossPercentageNegativeConsumption (executed 2026-04-02)

Script has been run. Record preserved in git history — see previous commit."

Task 4: Update LLD docs

Files:

  • Modify: docs/api/LLD-audit-tc.md:209
  • Modify: docs/api/LLD-ai-audit-run.md:427

Step 1: Update LLD-audit-tc.md

Find and replace at line 209:

// Before
if (effectiveTcConsumption > 0) {

// After
if (effectiveTcConsumption !== 0) { // handles negative tcConsumption; 0 yields lossPercentage = null (division by zero)

Step 2: Update LLD-ai-audit-run.md

Find and replace at line 427:

// Before
if (effectiveTcConsumption > 0) {

// After
if (effectiveTcConsumption !== 0) { // handles negative tcConsumption; 0 yields lossPercentage = null (division by zero)

Task 5: Commit

git add api/src/entities/audit/helpers/computeAuditFields.js \
api/src/entities/audit/controller/aiAuditTC.js \
script/test/computeAuditFields.test.js \
docs/api/LLD-audit-tc.md \
docs/api/LLD-ai-audit-run.md \
docs/plans/2026-04-02-fix-loss-percentage-negative-consumption.md

git commit -m "fix: compute lossPercentage for negative tcConsumption

TCs with negative tcConsumption (e.g. meter reading went backwards) were
being stored as AUDITED with lossPercentage=null because the guard was
effectiveTcConsumption > 0. Changed to !== 0 so negative values are
handled. Zero tcConsumption still yields null (division by zero) and
the TC remains AUDITED, which is the correct behaviour.

Fixed in both computeAuditFields (auditTC flow) and aiAuditTC (projected
lossPercentage calculation). Adds unit tests covering all edge cases."