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:
- User overrides — the frontend sends only diffs (installations where the user changed the AI suggestion). Backend merges these with the full staging data.
- Target TC update — recomputes audit fields using the final tagged installation set (with consumption overrides applied)
- Installation tagging changes — updates
tcId/tcNumberon real installation documents for newly tagged and newly untagged installations - Source TC reaudits — for each source TC that lost installations (newly tagged to the target TC), recomputes audit fields from its remaining installations
- Adjacent month loss % updates — for ALL affected TCs (target + sources), updates
previousMonthLossPercentageandpreviousToPreviousMonthLossPercentageon TC documents in the next 2 months - Audit summary cascade — recomputes audit summaries at all 6 levels for each unique section affected
- Freeze flag — marks the
ai-audit-resultas frozen so it cannot be re-triggered
Related LLDs
| LLD | Relationship |
|---|---|
LLD-ai-audit-schemas.md | Data model — defines ai-audit-result and ai-audit-installation schemas |
LLD-ai-audit-run.md | The run API that creates the staging data this endpoint freezes |
LLD-audit-tc.md | Standard reaudit — reuses computeAuditFields.js and recomputeAuditSummaries.js |
2. Endpoint
| Route | Method |
|---|---|
POST /audit/transformers/:id/freeze | POST |
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
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <sessionToken> |
3.2 Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The id field of the transformer document (e.g., TC-xxx) |
3.3 Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
month | string | Yes | Month 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
}
]
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
overrides | array | No | [] | Installations where the user changed the AI suggestion |
overrides[].accountId | string | Yes | — | Identifies the installation |
overrides[].action | string (enum) | Yes | — | "TAG" or "UNTAG" — the user's final decision |
overrides[].useAvgConsumption | boolean | No | Use AI suggestion's value | Whether 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):
| Condition | Target TC effect | Source TC effect |
|---|---|---|
prevTcId !== targetTcId | newlyTaggedInstallationCount += 1 | — |
prevTcId === targetTcId | No change (was already on target last month) | — |
prevTcId === sourceTcId | — | newlyUntaggedInstallationCount += 1 |
prevTcId !== sourceTcId | — | newlyTaggedInstallationCount -= 1 (undoes a natural new-tag) |
| Installation has a new remark this month | newRemarkInstallationCount.<key> += 1 | newRemarkInstallationCount.<key> -= 1 |
For each newly UNTAG'd installation (leaving target TC, going to tcId=null):
| Condition | Target TC effect |
|---|---|
prevTcId === targetTcId | newlyUntaggedInstallationCount += 1 |
prevTcId !== targetTcId | newlyTaggedInstallationCount -= 1 (undoes a natural new-tag) |
| Installation has a new remark this month | newRemarkInstallationCount.<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
previousMonthLossPercentageto the new loss % - Find the same TC's doc for month M+2 → update
previousToPreviousMonthLossPercentageto 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
| Scenario | Status | Error Type |
|---|---|---|
| Invalid month format | 400 | ValidationError (AJV) |
| Invalid override action (not TAG/UNTAG) | 400 | ValidationError (AJV) |
| AI audit already frozen | 400 | BadRequestError |
| Invalid/expired session | 401 | UnauthorizedError |
| AE accessing TC outside their section | 403 | ForbiddenError |
AI audit result not found for given id + month | 404 | ResourceUnavailableError |
Transformer not found for given id + month | 404 | ResourceUnavailableError |
| AE user doc not found | 500 | DatabaseError |
| DB connection failure | 500 | DatabaseError |
| Installation update failure | 500 | DatabaseError |
| Source TC reaudit failure | 500 | DatabaseError (non-blocking consideration — target TC update already succeeded) |
| Summary recomputation failure | 500 | DatabaseError (non-blocking — TC updates already succeeded) |
11. Performance Notes
11.1 DB Operations Breakdown
| Operation | Collection | Type | Count |
|---|---|---|---|
| Find ai-audit-result | ai-audit-result | Read | 1 |
| Find transformer | transformer | Read | 1 |
| Find user (AE only) | user | Read | 0 or 1 |
| Find all ai-audit-installations | ai-audit-installation | Read | 1 |
| Find prev month installations (retagged) | installation | Read | 0 or 1 |
| Update target TC | transformer | Write | 1 |
| Update newly tagged installations | installation | Write | 0 or 1 (updateMany) |
| Update newly untagged installations | installation | Write | 0 or 1 (updateMany) |
| Find source TC (per source) | transformer | Read | S |
| Find source TC installations (per source) | installation | Read | S |
| Update source TC (per source) | transformer | Write | S |
| Find TC for adjacent month update (per affected TC) | transformer | Read | T |
| Update M+1 TC doc (per affected TC) | transformer | Write | T |
| Update M+2 TC doc (per affected TC) | transformer | Write | T |
| Find sample TC for summary (per section) | transformer | Read | K |
| Audit summary upserts (6 per section) | audit-summaries | Write | 6*K |
| Mark ai-audit-result frozen | ai-audit-result | Write | 1 |
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
| Query | Index 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.