Skip to main content

LLD: Fetch AI Audit Installations & Remarks APIs

1. Overview

Two paginated GET endpoints that read from the intermediate AI audit collections created by the Run AI Audit API (POST /audit/transformers/:id/ai-audit). These power the two tables on the "Audit with AI" page:

  1. Table 1: Installation Tagging — shows all installations involved in the AI audit with their GPS-based tagging suggestions (TAG / UNTAG / NO_CHANGE)
  2. Table 2: Installations with Remarks — shows installations that have billing remarks (MNR, unbilled, vacant, etc.) with average consumption suggestions

Both endpoints read from the ai-audit-installation collection. Neither endpoint modifies any data.

LLDRelationship
LLD-ai-audit-schemas.mdData model — defines ai-audit-result and ai-audit-installation schemas
LLD-ai-audit-run.mdThe Run AI Audit API that creates the data these endpoints read
LLD-fetch-installations.mdExisting installations fetch endpoint (pagination pattern reference)

2. Endpoints

RouteMethodPurpose
GET /audit/transformers/:id/ai-audit/installationsGETTable 1 — Installation Tagging
GET /audit/transformers/:id/ai-audit/remarksGETTable 2 — Installations with Remarks

Both routes resolve user type from request.user.userType. AE users are validated against their assigned section.


3. Endpoint 1 — Fetch AI Audit Installations (Tagging Table)

3.1 Request

3.1.1 Headers

HeaderRequiredDescription
AuthorizationYesBearer <sessionToken>

3.1.2 Path Parameters

ParamTypeRequiredDescription
idstringYesThe id field of the transformer document (same as tcId in ai-audit-result)

3.1.3 Query Parameters

ParamTypeRequiredDefaultDescription
monthstringNoCurrent month (YYYY-MM)Month to fetch AI audit installations for
pageintegerNo1Page number (1-indexed)
limitintegerNo10Items per page (max 100)
filterstringNo"ALL"Filter by AI suggestion type (see mapping below)
searchstringNoSearch by RR Number (case-insensitive partial match)

Filter mapping:

Filter ValueQuery Condition
ALLNo filter — return all installations
CHANGES_SUGGESTEDaiSuggestion is TAG or UNTAG
NO_CHANGES_REQUIREDaiSuggestion is NO_CHANGE

3.2 Response

3.2.1 Success (200)

{
"success": true,
"message": "AI audit installations fetched successfully",
"data": {
"installations": [
{
"accountId": "ACC-001",
"installationId": "INST-001",
"rrNumber": "RR-12345",
"consumerName": "John Doe",
"gpsCoordinates": { "type": "Point", "coordinates": [74.8421, 12.8765] },
"distanceFromTC": 2340,
"withinRange": true,
"currentTaggingStatus": "TAGGED",
"aiSuggestion": "NO_CHANGE",
"sourceTcId": null,
"sourceTcNumber": null,
"consumption": 450,
"sanctionedLoad": { "kw": 5, "hp": 6.7 }
}
],
"pagination": {
"page": 1,
"limit": 10,
"totalDocs": 25,
"totalPages": 3
},
"filterCounts": {
"all": 25,
"changesSuggested": 8,
"noChangesRequired": 17
}
}
}

3.2.2 Not Found (404)

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

3.2.3 Forbidden (403)

When AE requests installations for a TC outside their section:

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

3.3 Business Logic

3.3.1 Default Month

IF month not provided:
month = current date formatted as "YYYY-MM"

3.3.2 Find AI Audit Result

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

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

3.3.3 AE Section Validation

IF userType === "AE":
1. Use aiAuditResult.location.sectionCode (already on the result doc)
2. Fetch user doc → extract user.location.sectionCode
3. Verify aiAuditResult.location.sectionCode === user.location.sectionCode
4. If mismatch → 403 Forbidden

Note: The ai-audit-result document contains the full location object (copied from the transformer at audit time), so there is no need to separately fetch the transformer document for section validation.

3.3.4 Build Query Filter

const aiAuditResultId = aiAuditResult._id.toString();

const filter = { aiAuditResultId };

// Apply filter based on query param
if (filterParam === "CHANGES_SUGGESTED") {
filter.aiSuggestion = { $in: ["TAG", "UNTAG"] };
} else if (filterParam === "NO_CHANGES_REQUIRED") {
filter.aiSuggestion = "NO_CHANGE";
}
// "ALL" — no additional filter on aiSuggestion

// RR number search (case-insensitive partial match)
if (search) {
filter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}

3.3.5 DB Queries

Three parallel queries — data + count + filter counts:

const skip = (page - 1) * limit;

const projection = {
_id: 0,
__v: 0,
createdAt: 0,
updatedAt: 0,
aiAuditResultId: 0,
tcId: 0,
month: 0,
// Exclude Table 2 (remarks) fields — not needed for tagging table
hasRemark: 0,
remarkType: 0,
avgConsumption6Months: 0,
aiSuggestsConsumption: 0,
// Exclude raw billing flags
unbilled: 0,
mnr: 0,
vacant: 0,
doorLock: 0,
zeroConsumption: 0,
emin: 0,
emax: 0,
// Exclude revenue fields not needed
demand: 0,
collection: 0,
};

const [installations, totalDocs, filterCounts] = await Promise.all([
aiAuditInstallationModel
.find(filter, projection)
.sort({ distanceFromTC: 1 }) // closest first
.skip(skip)
.limit(limit)
.lean(),

aiAuditInstallationModel.countDocuments(filter),

// Filter counts — compute counts for all filter tabs
computeInstallationFilterCounts(aiAuditResultId, search),
]);

3.3.6 Filter Counts

Compute counts for the filter tab badges. If a search term is active, the counts should reflect the search-filtered subset.

async function computeInstallationFilterCounts(aiAuditResultId, search) {
const baseFilter = { aiAuditResultId };
if (search) {
baseFilter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}

const [all, changesSuggested, noChangesRequired] = await Promise.all([
aiAuditInstallationModel.countDocuments(baseFilter),
aiAuditInstallationModel.countDocuments({
...baseFilter,
aiSuggestion: { $in: ["TAG", "UNTAG"] },
}),
aiAuditInstallationModel.countDocuments({
...baseFilter,
aiSuggestion: "NO_CHANGE",
}),
]);

return { all, changesSuggested, noChangesRequired };
}

Alternative — single aggregation with $facet:

async function computeInstallationFilterCounts(aiAuditResultId, search) {
const matchStage = { aiAuditResultId };
if (search) {
matchStage.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}

const [result] = await aiAuditInstallationModel.aggregate([
{ $match: matchStage },
{
$facet: {
all: [{ $count: "count" }],
changesSuggested: [
{ $match: { aiSuggestion: { $in: ["TAG", "UNTAG"] } } },
{ $count: "count" },
],
noChangesRequired: [
{ $match: { aiSuggestion: "NO_CHANGE" } },
{ $count: "count" },
],
},
},
]);

return {
all: result.all[0]?.count || 0,
changesSuggested: result.changesSuggested[0]?.count || 0,
noChangesRequired: result.noChangesRequired[0]?.count || 0,
};
}

Either approach is fine for demo scale (~200-500 docs per AI audit).

3.3.7 Pagination Response

{
page,
limit,
totalDocs,
totalPages: Math.ceil(totalDocs / limit),
}

4. Endpoint 2 — Fetch AI Audit Remarks (Remarks Table)

4.1 Request

4.1.1 Headers

HeaderRequiredDescription
AuthorizationYesBearer <sessionToken>

4.1.2 Path Parameters

Same as Endpoint 1.

ParamTypeRequiredDescription
idstringYesThe id field of the transformer document (same as tcId in ai-audit-result)

4.1.3 Query Parameters

ParamTypeRequiredDefaultDescription
monthstringNoCurrent month (YYYY-MM)Month to fetch AI audit remarks for
pageintegerNo1Page number (1-indexed)
limitintegerNo10Items per page (max 100)
filterstringNo"ALL"Filter by remark type (see mapping below)
searchstringNoSearch by RR Number (case-insensitive partial match)

Filter mapping:

Filter ValueQuery Condition
ALLhasRemark: true (all installations with any remark)
UNBILLEDremarkType: "UNBILLED"
MNRremarkType: "MNR"
VACANTremarkType: "VACANT"
ZERO_CONSUMPTIONremarkType: "ZERO_CONSUMPTION"
DOORLOCKremarkType: "DOORLOCK"
ABNORMALremarkType: "ABNORMAL"
SUBNORMALremarkType: "SUBNORMAL"

4.2 Response

4.2.1 Success (200)

{
"success": true,
"message": "AI audit remarks fetched successfully",
"data": {
"installations": [
{
"accountId": "ACC-005",
"installationId": "INST-005",
"rrNumber": "RR-67890",
"consumerName": "Jane Smith",
"remarkType": "MNR",
"avgConsumption6Months": 4564,
"aiSuggestsConsumption": true,
"consumption": 0,
"withinRange": true,
"currentTaggingStatus": "TAGGED",
"aiSuggestion": "NO_CHANGE"
}
],
"pagination": {
"page": 1,
"limit": 10,
"totalDocs": 13,
"totalPages": 2
},
"filterCounts": {
"all": 13,
"unbilled": 3,
"mnr": 2,
"vacant": 1,
"zeroConsumption": 3,
"doorlock": 1,
"abnormal": 2,
"subnormal": 1
}
}
}

Important: The remarks response includes withinRange, currentTaggingStatus, and aiSuggestion fields. The frontend needs these to determine if an installation should be grayed out (e.g., if it was suggested to be untagged in Table 1, it should appear grayed out in Table 2).

4.2.2 Not Found (404)

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

4.2.3 Forbidden (403)

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

4.3 Business Logic

4.3.1 Default Month

IF month not provided:
month = current date formatted as "YYYY-MM"

4.3.2 Find AI Audit Result

Same as Endpoint 1:

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

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

4.3.3 AE Section Validation

Same pattern as Endpoint 1 — use aiAuditResult.location.sectionCode for validation.

4.3.4 Build Query Filter

const aiAuditResultId = aiAuditResult._id.toString();

const filter = {
aiAuditResultId,
hasRemark: true, // always — this table only shows remarked installations
};

// Apply remarkType filter if not "ALL"
const remarkTypeMap = {
UNBILLED: "UNBILLED",
MNR: "MNR",
VACANT: "VACANT",
ZERO_CONSUMPTION: "ZERO_CONSUMPTION",
DOORLOCK: "DOORLOCK",
ABNORMAL: "ABNORMAL",
SUBNORMAL: "SUBNORMAL",
};

if (filterParam !== "ALL" && remarkTypeMap[filterParam]) {
filter.remarkType = remarkTypeMap[filterParam];
}

// RR number search (case-insensitive partial match)
if (search) {
filter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}

4.3.5 DB Queries

Three parallel queries — data + count + filter counts:

const skip = (page - 1) * limit;

const projection = {
_id: 0,
__v: 0,
createdAt: 0,
updatedAt: 0,
aiAuditResultId: 0,
tcId: 0,
month: 0,
// Exclude GPS coordinate details (not needed in remarks table)
gpsCoordinates: 0,
distanceFromTC: 0,
// Exclude source TC fields (not needed in remarks table)
sourceTcId: 0,
sourceTcNumber: 0,
// Exclude raw billing flags (remarkType is the derived summary)
unbilled: 0,
mnr: 0,
vacant: 0,
doorLock: 0,
zeroConsumption: 0,
emin: 0,
emax: 0,
// Exclude revenue fields
demand: 0,
collection: 0,
// Exclude hasRemark (redundant — all docs in this response have it)
hasRemark: 0,
};

const [installations, totalDocs, filterCounts] = await Promise.all([
aiAuditInstallationModel
.find(filter, projection)
.sort({ remarkType: 1, rrNumber: 1 }) // group by remark type, then by RR number
.skip(skip)
.limit(limit)
.lean(),

aiAuditInstallationModel.countDocuments(filter),

// Filter counts — compute counts for all remark type tabs
computeRemarkFilterCounts(aiAuditResultId, search),
]);

4.3.6 Filter Counts

Compute counts for each remark type tab. If a search term is active, the counts reflect the search-filtered subset.

async function computeRemarkFilterCounts(aiAuditResultId, search) {
const baseFilter = { aiAuditResultId, hasRemark: true };
if (search) {
baseFilter.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}

const [all, unbilled, mnr, vacant, zeroConsumption, doorlock, abnormal, subnormal] =
await Promise.all([
aiAuditInstallationModel.countDocuments(baseFilter),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "UNBILLED" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "MNR" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "VACANT" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "ZERO_CONSUMPTION" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "DOORLOCK" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "ABNORMAL" }),
aiAuditInstallationModel.countDocuments({ ...baseFilter, remarkType: "SUBNORMAL" }),
]);

return { all, unbilled, mnr, vacant, zeroConsumption, doorlock, abnormal, subnormal };
}

Alternative — single aggregation with $facet:

async function computeRemarkFilterCounts(aiAuditResultId, search) {
const matchStage = { aiAuditResultId, hasRemark: true };
if (search) {
matchStage.rrNumber = { $regex: escapeRegex(search), $options: "i" };
}

const [result] = await aiAuditInstallationModel.aggregate([
{ $match: matchStage },
{
$facet: {
all: [{ $count: "count" }],
unbilled: [{ $match: { remarkType: "UNBILLED" } }, { $count: "count" }],
mnr: [{ $match: { remarkType: "MNR" } }, { $count: "count" }],
vacant: [{ $match: { remarkType: "VACANT" } }, { $count: "count" }],
zeroConsumption: [{ $match: { remarkType: "ZERO_CONSUMPTION" } }, { $count: "count" }],
doorlock: [{ $match: { remarkType: "DOORLOCK" } }, { $count: "count" }],
abnormal: [{ $match: { remarkType: "ABNORMAL" } }, { $count: "count" }],
subnormal: [{ $match: { remarkType: "SUBNORMAL" } }, { $count: "count" }],
},
},
]);

return {
all: result.all[0]?.count || 0,
unbilled: result.unbilled[0]?.count || 0,
mnr: result.mnr[0]?.count || 0,
vacant: result.vacant[0]?.count || 0,
zeroConsumption: result.zeroConsumption[0]?.count || 0,
doorlock: result.doorlock[0]?.count || 0,
abnormal: result.abnormal[0]?.count || 0,
subnormal: result.subnormal[0]?.count || 0,
};
}

4.3.7 Pagination Response

{
page,
limit,
totalDocs,
totalPages: Math.ceil(totalDocs / limit),
}

5. Validation Schemas (AJV)

5.1 Endpoint 1 — Fetch AI Audit Installations

5.1.1 Path Parameters Schema

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

5.1.2 Query String 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" }
},
page: {
type: "integer",
minimum: 1,
default: 1
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
filter: {
type: "string",
enum: ["ALL", "CHANGES_SUGGESTED", "NO_CHANGES_REQUIRED"],
default: "ALL"
},
search: {
type: "string",
transform: ["trim"],
minLength: 1
}
},
additionalProperties: false,
errorMessage: {
additionalProperties: "Unsupported properties in query parameters"
}
}

5.1.3 Response Serialization

{
"2xx": responseSerializer("2xx", {
type: "object",
properties: {
installations: {
type: "array",
items: {
type: "object",
properties: {
accountId: { type: "string" },
installationId: { type: ["string", "null"] },
rrNumber: { type: ["string", "null"] },
consumerName: { type: ["string", "null"] },
gpsCoordinates: {
type: "object",
properties: {
type: { type: "string" },
coordinates: { type: "array", items: { type: "number" } },
},
},
distanceFromTC: { type: ["number", "null"] },
withinRange: { type: ["boolean", "null"] },
currentTaggingStatus: { type: ["string", "null"] },
aiSuggestion: { type: ["string", "null"] },
sourceTcId: { type: ["string", "null"] },
sourceTcNumber: { type: ["string", "null"] },
consumption: { type: ["number", "null"] },
sanctionedLoad: {
type: "object",
properties: {
kw: { type: ["number", "null"] },
hp: { type: ["number", "null"] },
},
},
},
},
},
pagination: {
type: "object",
properties: {
page: { type: "integer" },
limit: { type: "integer" },
totalDocs: { type: "integer" },
totalPages: { type: "integer" },
},
},
filterCounts: {
type: "object",
properties: {
all: { type: "integer" },
changesSuggested: { type: "integer" },
noChangesRequired: { type: "integer" },
},
},
},
}),
"4xx": responseSerializer("4xx"),
"5xx": responseSerializer("5xx"),
}

5.2 Endpoint 2 — Fetch AI Audit Remarks

5.2.1 Path Parameters Schema

Same as Endpoint 1.

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

5.2.2 Query String 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" }
},
page: {
type: "integer",
minimum: 1,
default: 1
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
filter: {
type: "string",
enum: [
"ALL", "UNBILLED", "MNR", "VACANT",
"ZERO_CONSUMPTION", "DOORLOCK",
"ABNORMAL", "SUBNORMAL"
],
default: "ALL"
},
search: {
type: "string",
transform: ["trim"],
minLength: 1
}
},
additionalProperties: false,
errorMessage: {
additionalProperties: "Unsupported properties in query parameters"
}
}

5.2.3 Response Serialization

{
"2xx": responseSerializer("2xx", {
type: "object",
properties: {
installations: {
type: "array",
items: {
type: "object",
properties: {
accountId: { type: "string" },
installationId: { type: ["string", "null"] },
rrNumber: { type: ["string", "null"] },
consumerName: { type: ["string", "null"] },
remarkType: { type: ["string", "null"] },
avgConsumption6Months: { type: ["number", "null"] },
aiSuggestsConsumption: { type: "boolean" },
consumption: { type: ["number", "null"] },
withinRange: { type: ["boolean", "null"] },
currentTaggingStatus: { type: ["string", "null"] },
aiSuggestion: { type: ["string", "null"] },
sanctionedLoad: {
type: "object",
properties: {
kw: { type: ["number", "null"] },
hp: { type: ["number", "null"] },
},
},
},
},
},
pagination: {
type: "object",
properties: {
page: { type: "integer" },
limit: { type: "integer" },
totalDocs: { type: "integer" },
totalPages: { type: "integer" },
},
},
filterCounts: {
type: "object",
properties: {
all: { type: "integer" },
unbilled: { type: "integer" },
mnr: { type: "integer" },
vacant: { type: "integer" },
zeroConsumption: { type: "integer" },
doorlock: { type: "integer" },
abnormal: { type: "integer" },
subnormal: { type: "integer" },
},
},
},
}),
"4xx": responseSerializer("4xx"),
"5xx": responseSerializer("5xx"),
}

6. Route Config

6.1 Endpoint 1

{
routeId: "fetchAiAuditInstallations",
authentication: {
// No excludeFrom — requires full auth
},
authorization: {
action: "read",
resource: "audit"
}
}

6.2 Endpoint 2

{
routeId: "fetchAiAuditRemarks",
authentication: {
// No excludeFrom — requires full auth
},
authorization: {
action: "read",
resource: "audit"
}
}

7. File Structure

api/src/entities/audit/
audit.route.v1.js — Add two new route entries
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 (Run AI Audit)
fetchAiAuditInstallations.js — New: handler, schema, config exports
fetchAiAuditRemarks.js — New: handler, schema, config exports
helpers/
resolveLocationFilter.js — Existing (reused for AE section lookup)

8. Error Handling

ScenarioStatusError Type
Invalid month format400ValidationError (AJV)
Invalid filter enum value400ValidationError (AJV)
Invalid page or limit400ValidationError (AJV)
Invalid/expired session401UnauthorizedError
AE accessing TC outside their section403ForbiddenError
AI audit result not found for TC + month404ResourceUnavailableError
AE user doc not found500DatabaseError
DB connection failure500DatabaseError

9. Performance Notes

9.1 DB Operations per Request

OperationCollectionTypeCount
Find AI audit resultai-audit-resultRead1
Find user (AE only)userRead0 or 1
Find installations (paginated)ai-audit-installationRead1
Count documents (for pagination)ai-audit-installationRead1
Count documents (filter counts)ai-audit-installationRead3 (Endpoint 1) or 8 (Endpoint 2)

Total — Endpoint 1: 5-6 reads, 0 writes Total — Endpoint 2: 10-11 reads, 0 writes

9.2 Index Usage

QueryIndex Hit
aiAuditResultModel.findOne({ tcId, month }){ tcId: 1, month: 1 } unique index
find({ aiAuditResultId }) with sort on distanceFromTC{ aiAuditResultId: 1, aiSuggestion: 1 } — prefix match for filter; in-memory sort (acceptable for ~200-500 docs)
find({ aiAuditResultId, hasRemark: true }) with sort{ aiAuditResultId: 1, hasRemark: 1 } — exact match
countDocuments({ aiAuditResultId, aiSuggestion }){ aiAuditResultId: 1, aiSuggestion: 1 } — exact match
countDocuments({ aiAuditResultId, hasRemark, remarkType }){ aiAuditResultId: 1, hasRemark: 1 } — prefix match, remarkType filtered in-memory

9.3 Why Multiple countDocuments Instead of $facet?

Both approaches are provided in the business logic sections. For the demo data scale (~200-500 docs per AI audit result), multiple countDocuments calls run in parallel via Promise.all are comparably fast to a single $facet aggregation. Choose based on code readability preference.

9.4 Pagination

Offset-based (skip/limit). Acceptable since installations per AI audit are typically in the hundreds (average ~22 per TC in the section, ~20 TCs per section = ~200-500 total). Deep pagination is unlikely.

9.5 Latency Estimate

For an AI audit with ~300 installations:

  • Find ai-audit-result: ~5ms (indexed unique lookup)
  • Paginated find: ~10-20ms (indexed + skip/limit on small collection)
  • Count queries (3 or 8 parallel): ~10-20ms total (indexed)
  • Total: ~25-45ms

10. Regex Escape Utility

The search param is user input used in a $regex query. Reuse the same escapeRegex helper from fetchTransformers / fetchInstallations:

function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}