Skip to main content

LLD: AI Audit Freeze API

1. Overview

Freezes (commits) an AI audit result for a single Transformer Centre (TC) for a given month. This is the final step of the AI Audit flow — after the user has reviewed the AI suggestions (tagging/untagging, consumption overrides) and optionally adjusted them, this endpoint writes the changes to the real collections (transformer, installation, audit-summaries).

This is the most write-heavy endpoint in the entire application. A single freeze can trigger:

  1. User overrides — the frontend sends only diffs (installations where the user changed the AI suggestion). Backend merges these with the full staging data.
  2. Target TC update — recomputes audit fields using the final tagged installation set (with consumption overrides applied)
  3. Installation tagging changes — updates tcId / tcNumber on real installation documents for newly tagged and newly untagged installations
  4. Source TC reaudits — for each source TC that lost installations (newly tagged to the target TC), recomputes audit fields from its remaining installations
  5. Adjacent month loss % updates — for ALL affected TCs (target + sources), updates previousMonthLossPercentage and previousToPreviousMonthLossPercentage on TC documents in the next 2 months
  6. Audit summary cascade — recomputes audit summaries at all 6 levels for each unique section affected
  7. Freeze flag — marks the ai-audit-result as frozen so it cannot be re-triggered
LLDRelationship
LLD-ai-audit-schemas.mdData model — defines ai-audit-result and ai-audit-installation schemas
LLD-ai-audit-run.mdThe run API that creates the staging data this endpoint freezes
LLD-audit-tc.mdStandard reaudit — reuses computeAuditFields.js and recomputeAuditSummaries.js

2. Endpoint

RouteMethod
POST /audit/transformers/:id/freezePOST

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

ParamTypeRequiredDescription
monthstringYesMonth in YYYY-MM format (e.g., 2025-10)

3.4 Request Body

The frontend sends only the diffs — installations where the user changed the AI suggestion. The backend reads the full list from ai-audit-installation and applies these overrides on top.

{
"overrides": [
{
"accountId": "ACC-001",
"action": "TAG"
},
{
"accountId": "ACC-002",
"action": "UNTAG"
},
{
"accountId": "ACC-003",
"action": "TAG",
"useAvgConsumption": false
},
{
"accountId": "ACC-004",
"action": "UNTAG",
"useAvgConsumption": true
}
]
}
FieldTypeRequiredDefaultDescription
overridesarrayNo[]Installations where the user changed the AI suggestion
overrides[].accountIdstringYesIdentifies the installation
overrides[].actionstring (enum)Yes"TAG" or "UNTAG" — the user's final decision
overrides[].useAvgConsumptionbooleanNoUse AI suggestion's valueWhether to use the 6-month average consumption for remarked installations

Important: The overrides array only contains installations where the user CHANGED something. If the user accepted all AI suggestions without modification, overrides can be empty [].


4. Response

4.1 Success (200)

{
"success": true,
"message": "AI audit frozen successfully",
"data": {
"transformer": {
"id": "TC-xxx",
"number": "11F9020",
"name": "TC 11F9020",
"month": "2025-10",
"installationConsumption": 1100,
"installationCount": 18,
"lossPercentage": 15,
"auditStatus": "AUDITED",
"auditedByAI": true,
"billingCount": {
"unbilled": 8,
"mnr": 3,
"vacant": 2,
"zeroConsumption": 6,
"doorlock": 1,
"abnormal": 1,
"subnormal": 3,
"billCancellation": 0
},
"revenueParams": {
"arr": null,
"atc": null,
"demand": 420000,
"collection": 400000,
"billingEfficiency": 94,
"collectionEfficiency": 90
},
"overloaded": false
},
"affectedSourceTCs": [
{ "id": "TC-yyy", "number": "11F9021", "lossPercentage": 8.5 },
{ "id": "TC-zzz", "number": "11F9022", "lossPercentage": 12.1 }
]
}
}

4.2 Not Found (404)

{
"success": false,
"message": "AI audit result not found"
}

4.3 Forbidden (403)

When AE requests freeze on a TC outside their section:

{
"success": false,
"message": "TC does not belong to your assigned section"
}

4.4 Bad Request (400) — Already Frozen

{
"success": false,
"message": "AI audit already frozen"
}

5. Business Logic

5.1 Validate & Fetch ai-audit-result (Step 1)

const aiAuditResult = await aiAuditResultModel
.findOne({ tcId: id, month })
.lean();

if (!aiAuditResult) {
return 404"AI audit result not found";
}

if (aiAuditResult.frozen) {
return 400"AI audit already frozen";
}

5.2 Fetch Transformer

const transformer = await transformerModel
.findOne({ id, month })
.lean();

if (!transformer) {
return 404"Transformer not found";
}

5.3 AE Section Validation (Step 2)

Same pattern as auditTC.js and aiAuditTC.js:

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 All ai-audit-installation Docs (Step 3)

const aiAuditResultId = aiAuditResult._id.toString();

const aiInstallations = await aiAuditInstallationModel
.find({ aiAuditResultId })
.lean();

5.5 Apply User Overrides (Step 4)

Build a lookup map from the overrides array, then merge with each staging installation to determine the final state:

// Build override lookup
const overrideMap = new Map();
for (const override of overrides) {
overrideMap.set(override.accountId, override);
}

// Determine final state for each installation
for (const inst of aiInstallations) {
const override = overrideMap.get(inst.accountId);

if (override) {
// User changed the AI suggestion
inst.finalAction = override.action; // "TAG" or "UNTAG"
inst.finalUseAvgConsumption = override.useAvgConsumption !== undefined
? override.useAvgConsumption
: inst.aiSuggestsConsumption;
} else {
// No override — use the AI suggestion as-is
inst.finalAction = inst.aiSuggestion; // "TAG", "UNTAG", or "NO_CHANGE"
inst.finalUseAvgConsumption = inst.aiSuggestsConsumption;
}
}

Determine the final tagged set — all installations that will be tagged to this TC after the freeze:

const finalTaggedSet = aiInstallations.filter(inst => {
if (inst.finalAction === "TAG") return true;
if (inst.finalAction === "NO_CHANGE" && inst.currentTaggingStatus === "TAGGED") return true;
return false;
});

Determine newly tagged and newly untagged sets — installations that are changing their tagging:

// Were NOT tagged to this TC, now will be tagged
const newlyTaggedInstallations = aiInstallations.filter(inst =>
inst.finalAction === "TAG" && inst.currentTaggingStatus === "UNTAGGED"
);

// Were tagged to this TC, now will be untagged
const newlyUntaggedInstallations = aiInstallations.filter(inst => {
if (inst.finalAction === "UNTAG" && inst.currentTaggingStatus === "TAGGED") return true;
// User manually overrode a NO_CHANGE to UNTAG
if (inst.finalAction === "UNTAG" && inst.currentTaggingStatus === "TAGGED") return true;
return false;
});

5.5b Fetch Previous Month Installation Docs for Retagged Installations (Step 4b)

When installations are retagged (TAG or UNTAG), the newlyTaggedInstallationCount, newlyUntaggedInstallationCount, and newRemarkInstallationCount fields on affected TCs must be updated. These fields track month-over-month deltas — not absolute counts — so we need to know which TC each retagged installation was on last month to compute the correct adjustments.

const prevMonth = getPrevMonth(month);
const retaggedAccountIds = [
...newlyTaggedInstallations.map(i => i.accountId),
...newlyUntaggedInstallations.map(i => i.accountId),
];

const prevMonthDocs = await installationModel.find(
{ accountId: { $in: retaggedAccountIds }, month: prevMonth },
{ accountId: 1, tcId: 1, unbilled: 1, mnr: 1, vacant: 1,
zeroConsumption: 1, doorLock: 1, emin: 1, emax: 1 }
).lean();

const prevMonthMap = new Map(); // accountId → prev month installation doc
for (const doc of prevMonthDocs) {
prevMonthMap.set(doc.accountId, doc);
}

5.5c Compute Tagging & Remark Deltas (Step 4c)

Using the previous month data, compute delta adjustments for the target TC and each source TC.

For each newly TAG'd installation (moving to target TC from source TC):

ConditionTarget TC effectSource TC effect
prevTcId !== targetTcIdnewlyTaggedInstallationCount += 1
prevTcId === targetTcIdNo change (was already on target last month)
prevTcId === sourceTcIdnewlyUntaggedInstallationCount += 1
prevTcId !== sourceTcIdnewlyTaggedInstallationCount -= 1 (undoes a natural new-tag)
Installation has a new remark this monthnewRemarkInstallationCount.<key> += 1newRemarkInstallationCount.<key> -= 1

For each newly UNTAG'd installation (leaving target TC, going to tcId=null):

ConditionTarget TC effect
prevTcId === targetTcIdnewlyUntaggedInstallationCount += 1
prevTcId !== targetTcIdnewlyTaggedInstallationCount -= 1 (undoes a natural new-tag)
Installation has a new remark this monthnewRemarkInstallationCount.<key> -= 1

"New remark" determination: A remark is considered "new this month" if the current installation has the remark flag set (e.g., mnr: true) but the previous month's installation doc either doesn't exist or doesn't have that flag set. This is checked per flag: unbilled, mnr, vacant, zeroConsumption, doorLock (→ doorlock), emin > 0 (→ abnormal), emax > 0 (→ subnormal).

All delta values are clamped to Math.max(0, ...) when applied to prevent negative counts.

5.6 Compute Final Audit Metrics for the Target TC (Step 5)

Using the final tagged set, compute all audit fields. This follows the same formulas as computeAuditFields.js, but with one addition: consumption overrides for remarked installations.

// Effective consumption per installation
function getEffectiveConsumption(inst) {
if (inst.hasRemark && inst.finalUseAvgConsumption && inst.avgConsumption6Months > 0) {
return inst.avgConsumption6Months;
}
return inst.consumption || 0;
}

// Installation consumption — sum of effective consumption
const installationConsumption = finalTaggedSet.reduce(
(sum, inst) => sum + getEffectiveConsumption(inst), 0
);

// Installation count
const installationCount = finalTaggedSet.length;

// Loss percentage
const effectiveTcConsumption = (transformer.tcConsumption || 0) * (transformer.meterConstant || 1);
let lossPercentage = null;
if (effectiveTcConsumption > 0) {
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 finalTaggedSet) {
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
const totalInstallationSanctionedLoadInKVA = finalTaggedSet.reduce(
(sum, inst) => sum + (inst.sanctionedLoad?.kw || 0), 0
);

// Overloaded flag
const overloaded = transformer.tcCapacity
? totalInstallationSanctionedLoadInKVA > transformer.tcCapacity
: false;

// Revenue params
const revenueParams = {
demand: finalTaggedSet.reduce((sum, inst) => sum + (inst.demand || 0), 0),
collection: finalTaggedSet.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;
}

// Audit status — always AUDITED (AI audit only runs on eligible TCs)
const auditStatus = "AUDITED";

5.7 Write Changes to Real Collections (Step 6)

This is the critical step. Multiple collections are affected.

5.7a Update the Target Transformer Document

const targetDeltaCounts = applyDeltaCounts(transformer, targetDeltas);

const updatedTransformer = await transformerModel.findOneAndUpdate(
{ id, month },
{
$set: {
installationConsumption,
installationCount,
lossPercentage,
auditStatus: "AUDITED",
auditedByAI: true, // marks this TC as AI-audited
auditedOn: new Date(),
billingCount,
revenueParams,
overloaded,
totalInstallationSanctionedLoadInKVA,
...targetDeltaCounts, // newlyTaggedInstallationCount, newlyUntaggedInstallationCount, newRemarkInstallationCount
}
},
{ new: true, projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0, activatedOn: 0, deletedOn: 0, auditedOn: 0 } }
).lean();

5.7b Update Installation Documents (THIS MONTH ONLY)

For each installation that changed its tagging, update the real installation document. Only update for the selected month — do NOT touch installation documents for other months.

The transformer id has a "TC-" prefix but installation.tcId does not:

const installationTcId = id.replace(/^TC-/, "");

Newly tagged installations (were untagged/other-TC, now tagged to this TC):

const newlyTaggedAccountIds = newlyTaggedInstallations.map(inst => inst.accountId);

if (newlyTaggedAccountIds.length > 0) {
await installationModel.updateMany(
{ accountId: { $in: newlyTaggedAccountIds }, month },
{ $set: { tcId: installationTcId, tcNumber: transformer.number } }
);
}

Newly untagged installations (were tagged to this TC, now untagged):

const newlyUntaggedAccountIds = newlyUntaggedInstallations.map(inst => inst.accountId);

if (newlyUntaggedAccountIds.length > 0) {
await installationModel.updateMany(
{ accountId: { $in: newlyUntaggedAccountIds }, month },
{ $set: { tcId: null, tcNumber: null } }
);
}

5.7c Reaudit ALL Affected Source TCs

Collect all UNIQUE sourceTcId values from installations that were newly tagged (came from other TCs). For each source TC, recompute audit fields from its remaining installations using the existing computeAuditFields helper.

const affectedSourceTcIds = [...new Set(
newlyTaggedInstallations
.filter(inst => inst.sourceTcId)
.map(inst => inst.sourceTcId)
)];

const affectedSourceTCResults = [];

for (const sourceTcId of affectedSourceTcIds) {
const sourceTransformer = await transformerModel.findOne({ id: sourceTcId, month }).lean();
if (!sourceTransformer) continue;

// Fetch all installations still tagged to the source TC (after our tagging changes)
const sourceInstallationTcId = sourceTcId.replace(/^TC-/, "");
const sourceInstallations = await installationModel
.find({ tcId: sourceInstallationTcId, month })
.lean();

const sourceAuditFields = computeAuditFields(sourceTransformer, sourceInstallations);

// Remove auditedByAI — only the target TC gets this flag
delete sourceAuditFields.auditedByAI;

// Apply tagging/remark deltas to source TC (see Section 5.5c)
const srcDeltas = sourceTcDeltaMap.get(sourceTcId);
if (srcDeltas) {
Object.assign(sourceAuditFields, applyDeltaCounts(sourceTransformer, srcDeltas));
}

await transformerModel.findOneAndUpdate(
{ id: sourceTcId, month },
{ $set: sourceAuditFields }
);

affectedSourceTCResults.push({
id: sourceTcId,
number: sourceTransformer.number,
lossPercentage: sourceAuditFields.lossPercentage,
});
}

5.7d Update Adjacent Month Loss % Fields

For ALL affected TCs (target TC + all source TCs), update the previousMonthLossPercentage and previousToPreviousMonthLossPercentage on adjacent month documents.

If the month being audited is M:

  • Find the same TC's doc for month M+1 → update previousMonthLossPercentage to the new loss %
  • Find the same TC's doc for month M+2 → update previousToPreviousMonthLossPercentage to the new loss %
function getNextMonth(month) {
const [year, mon] = month.split("-").map(Number);
const next = mon === 12
? `${year + 1}-01`
: `${year}-${String(mon + 1).padStart(2, "0")}`;
return next;
}

const allAffectedTcIds = [id, ...affectedSourceTcIds];

for (const tcId of allAffectedTcIds) {
const tcDoc = await transformerModel.findOne({ id: tcId, month }).lean();
if (!tcDoc || tcDoc.lossPercentage == null) continue;

const nextMonth = getNextMonth(month); // "2025-10" → "2025-11"
const nextNextMonth = getNextMonth(nextMonth); // "2025-11" → "2025-12"

await transformerModel.updateOne(
{ id: tcId, month: nextMonth },
{ $set: { previousMonthLossPercentage: tcDoc.lossPercentage } }
);

await transformerModel.updateOne(
{ id: tcId, month: nextNextMonth },
{ $set: { previousToPreviousMonthLossPercentage: tcDoc.lossPercentage } }
);
}

Note: Uses { id, month } to identify the TC across months. The id field is stable across months (same physical TC has the same id in every month snapshot), so { id, month } uniquely identifies a document. We do NOT use { number, location.sectionCode, month } because production data can have duplicate TC numbers within a section.

5.7e Recompute Audit Summaries (Cascade)

After all TC updates are done, recompute audit summaries for the affected month at all 6 levels using the existing recomputeAuditSummaries helper. The cascade runs for EACH unique section affected:

const affectedSections = new Set();
affectedSections.add(transformer.location.sectionCode);

for (const sourceTcId of affectedSourceTcIds) {
const sourceTc = await transformerModel.findOne({ id: sourceTcId, month }).lean();
if (sourceTc) affectedSections.add(sourceTc.location.sectionCode);
}

for (const sectionCode of affectedSections) {
// Find any TC in this section to get full location
const sampleTc = await transformerModel
.findOne({ "location.sectionCode": sectionCode, month })
.lean();
if (sampleTc) {
await recomputeAuditSummaries(sampleTc.location, month);
}
}

Each call to recomputeAuditSummaries triggers 6 upserts (SECTION, SUB_DIVISION, DIVISION, CIRCLE, ZONE, MESCOM).

5.8 Mark ai-audit-result as Frozen (Step 7)

await aiAuditResultModel.findOneAndUpdate(
{ tcId: id, month },
{ $set: { frozen: true, frozenOn: new Date() } }
);

5.9 Return Response (Step 8)

return {
statusCode: 200,
success: true,
message: "AI audit frozen successfully",
data: {
transformer: {
id: updatedTransformer.id,
number: updatedTransformer.number,
name: updatedTransformer.name,
month,
installationConsumption,
installationCount,
lossPercentage,
auditStatus: "AUDITED",
auditedByAI: true,
billingCount,
revenueParams,
overloaded,
},
affectedSourceTCs: affectedSourceTCResults,
},
};

6. Cascading Impact Diagram

POST /audit/transformers/:id/freeze


┌─────────────────────────┐
│ Read ai-audit-result │
│ + ai-audit-installation │
│ Apply user overrides │
└───────────┬─────────────┘


┌─────────────────────────────┐
│ Compute final audit metrics │
│ for target TC │
└───────────┬─────────────────┘

┌───────┼────────────────────┐
▼ ▼ ▼
┌────────┐ ┌──────────────┐ ┌──────────────────┐
│Update │ │Update │ │Update │
│target │ │installation │ │installation │
│TC doc │ │docs (newly │ │docs (newly │
│ │ │tagged) │ │untagged) │
└────────┘ └──────────────┘ └──────────────────┘


┌──────────────────────────────┐
│ For each source TC: │
│ - Fetch TC doc │
│ - Fetch remaining installs │
│ - computeAuditFields │
│ - Update source TC doc │
└───────────┬──────────────────┘


┌──────────────────────────────┐
│ For ALL affected TCs │
│ (target + sources): │
│ - Update M+1 TC doc │
│ previousMonthLossPercent │
│ - Update M+2 TC doc │
│ prevToPrevMonthLoss% │
└───────────┬──────────────────┘


┌──────────────────────────────┐
│ Cascade audit summaries │
│ at all 6 levels for each │
│ affected section + month │
└───────────┬──────────────────┘


┌──────────────────────────────┐
│ Mark ai-audit-result as │
│ frozen: true │
└──────────────────────────────┘

7. Validation Schema (AJV)

7.1 Path Parameters Schema

{
type: "object",
properties: {
id: {
type: "string",
transform: ["trim"],
}
},
required: ["id"]
}

7.2 Querystring 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" }
}
},
required: ["month"]
}

7.3 Body Schema

{
type: "object",
properties: {
overrides: {
type: "array",
items: {
type: "object",
properties: {
accountId: { type: "string" },
action: { type: "string", enum: ["TAG", "UNTAG"] },
useAvgConsumption: { type: "boolean" }
},
required: ["accountId", "action"],
additionalProperties: false
},
default: []
}
},
additionalProperties: false,
errorMessage: {
additionalProperties: "Unsupported properties in request body"
}
}

7.4 Response Serialization

const responseSerialization = {
"2xx": responseSerializer("2xx", {
type: "object",
properties: {
transformer: {
type: "object",
properties: {
id: { type: "string" },
number: { type: "string" },
name: { type: "string" },
month: { type: "string" },
installationConsumption: { type: ["number", "null"] },
installationCount: { type: "number" },
lossPercentage: { type: ["number", "null"] },
auditStatus: { type: "string" },
auditedByAI: { type: "boolean" },
billingCount: {
type: "object",
properties: {
unbilled: { type: "number" },
mnr: { type: "number" },
vacant: { type: "number" },
zeroConsumption: { type: "number" },
doorlock: { type: "number" },
abnormal: { type: "number" },
subnormal: { type: "number" },
billCancellation: { type: "number" },
},
},
revenueParams: {
type: "object",
properties: {
arr: { type: ["number", "null"] },
atc: { type: ["number", "null"] },
demand: { type: ["number", "null"] },
collection: { type: ["number", "null"] },
billingEfficiency: { type: ["number", "null"] },
collectionEfficiency: { type: ["number", "null"] },
},
},
overloaded: { type: "boolean" },
},
},
affectedSourceTCs: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
number: { type: "string" },
lossPercentage: { type: ["number", "null"] },
},
},
},
},
}),

"4xx": responseSerializer("4xx"),
"5xx": responseSerializer("5xx"),
};

8. Route Config

{
routeId: "freezeAiAudit",
authentication: {
// No excludeFrom — requires full auth (validation + verification)
},
authorization: {
action: "read",
resource: "audit"
}
}

9. File Structure

api/src/entities/audit/
audit.route.v1.js — Add new route entry
controller/
fetchAuditSummary.js — Existing
fetchTransformers.js — Existing
fetchTransformerDetail.js — Existing
fetchInstallations.js — Existing
auditTC.js — Existing
fetchConsumptionTrend.js — Existing
fetchRevenueTrend.js — Existing
aiAuditTC.js — Existing (the run API)
freezeAiAudit.js — New: handler, schema, config exports
helpers/
computeAuditFields.js — Existing (reused for source TC reaudit)
recomputeAuditSummaries.js — Existing (reused for audit summary cascade)
resolveLocationFilter.js — Existing
lossBucketRanges.js — Existing
gpsConstants.js — Existing (from ai-audit-run)
deriveRemarkType.js — Existing (from ai-audit-run)

common/src/schemas/
transformer.model.js — Existing
installation.model.js — Existing
aiAuditResult.model.js — Existing (from ai-audit-run)
aiAuditInstallation.model.js — Existing (from ai-audit-run)

10. Error Handling

ScenarioStatusError Type
Invalid month format400ValidationError (AJV)
Invalid override action (not TAG/UNTAG)400ValidationError (AJV)
AI audit already frozen400BadRequestError
Invalid/expired session401UnauthorizedError
AE accessing TC outside their section403ForbiddenError
AI audit result not found for given id + month404ResourceUnavailableError
Transformer not found for given id + month404ResourceUnavailableError
AE user doc not found500DatabaseError
DB connection failure500DatabaseError
Installation update failure500DatabaseError
Source TC reaudit failure500DatabaseError (non-blocking consideration — target TC update already succeeded)
Summary recomputation failure500DatabaseError (non-blocking — TC updates already succeeded)

11. Performance Notes

11.1 DB Operations Breakdown

OperationCollectionTypeCount
Find ai-audit-resultai-audit-resultRead1
Find transformertransformerRead1
Find user (AE only)userRead0 or 1
Find all ai-audit-installationsai-audit-installationRead1
Find prev month installations (retagged)installationRead0 or 1
Update target TCtransformerWrite1
Update newly tagged installationsinstallationWrite0 or 1 (updateMany)
Update newly untagged installationsinstallationWrite0 or 1 (updateMany)
Find source TC (per source)transformerReadS
Find source TC installations (per source)installationReadS
Update source TC (per source)transformerWriteS
Find TC for adjacent month update (per affected TC)transformerReadT
Update M+1 TC doc (per affected TC)transformerWriteT
Update M+2 TC doc (per affected TC)transformerWriteT
Find sample TC for summary (per section)transformerReadK
Audit summary upserts (6 per section)audit-summariesWrite6*K
Mark ai-audit-result frozenai-audit-resultWrite1

Where:

  • S = number of unique source TCs (typically 2-3)
  • T = total affected TCs = 1 + S (typically 3-4)
  • K = number of unique sections affected (typically 1-2)

11.2 Demo Scale Estimate

For a typical freeze (1 target TC with ~25 installations, 2-3 source TCs):

  • Reads: 3 + 1 + 3 + 3 + 4 + 2 = ~16 reads
  • Writes: 1 + 2 + 3 + 8 + 12 + 1 = ~27 writes
  • Total: ~43 DB operations

This is acceptable for demo scale. Response time estimate: 500ms-1s.

11.3 Index Usage

QueryIndex Hit
aiAuditResultModel.findOne({ tcId, month }){ tcId: 1, month: 1 } unique index
aiAuditInstallationModel.find({ aiAuditResultId }){ aiAuditResultId: 1, accountId: 1 } prefix match
transformerModel.findOne({ id, month })_id or needs { id: 1, month: 1 } index
installationModel.updateMany({ accountId: { $in: [...] }, month }){ accountId: 1, month: 1 }
installationModel.find({ tcId, month }){ tcId: 1, month: 1 }
installationModel.find({ accountId: { $in }, month: prevMonth }){ accountId: 1, month: 1 }
transformerModel.updateOne({ id, month })_id or needs { id: 1, month: 1 } index

11.4 Transaction Consideration

For data consistency, the critical writes (Steps 5.7a through 5.7e) could be wrapped in a MongoDB transaction. This ensures that if any write fails, all changes are rolled back. However, this requires a MongoDB replica set and adds latency. For demo purposes, proceeding without a transaction is acceptable — partial failures can be addressed by re-running the AI audit (delete unfrozen result + re-trigger).


12. Key Design Decisions

12.1 Why Override-Based Instead of Full State?

The frontend sends only the diffs (overrides array) rather than the full list of installations with their final states. This is simpler for the frontend (it only tracks what the user changed) and results in a smaller request payload. The backend already has the full staging data in ai-audit-installation and can merge the overrides efficiently.

12.2 Why Not Reuse computeAuditFields for the Target TC?

The existing computeAuditFields computes installationConsumption as a simple sum of inst.consumption. For the freeze endpoint, we need to apply consumption overrides (use avgConsumption6Months for remarked installations where the user toggled the checkbox). This requires a custom consumption computation that computeAuditFields does not support.

However, computeAuditFields IS reused for source TC reaudits (Step 5.7c), where no consumption overrides apply — the source TCs just need a standard reaudit with their remaining installations.

12.3 Why auditedByAI Only on the Target TC?

The auditedByAI: true flag is set only on the target TC (the one the user explicitly ran AI audit on). Source TCs are reaudited as a side effect (because they lost installations), but they were not directly AI-audited. This distinction matters for the UI — it determines which TCs show the "AI Audited" badge and prevents re-running AI audit on already-frozen TCs.

12.4 Why Update Only THIS Month's Installations?

Installation documents follow the snapshot-per-month pattern (one doc per { accountId, month }). Tagging changes from an AI audit apply only to the month being audited. Other months' installation documents retain their original tagging. This preserves historical accuracy and avoids cascading changes across multiple months.

12.5 Why Adjacent Month Updates Use { id, month }?

The transformer id field is stable across months — the same physical TC has the same id in every month snapshot. So { id, month } uniquely identifies a TC document in any month. We previously used { number, location.sectionCode, month }, but production data can have duplicate TC numbers within a section, making that filter ambiguous.