feat(parcel-sync): add /api/eterra/debug-fields diagnostic endpoint

Shows all available eTerra fields for a parcel + buildings:
- GIS layer attributes (raw from ArcGIS)
- Immovable parcel details (intravilan, categories)
- Immovable list entry (address, areas)
- Documentation data (owners, registrations)
- Local DB state (enrichment, sync dates)

Usage: /api/eterra/debug-fields?siruta=161829&cadRef=77102

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-27 06:59:35 +02:00
parent acb9be8345
commit 4e67c29267
+187
View File
@@ -0,0 +1,187 @@
import { NextResponse } from "next/server";
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
/**
* GET /api/eterra/debug-fields?siruta=161829&cadRef=77102
*
* Diagnostic endpoint — shows all available fields from eTerra + local DB
* for a specific parcel and its buildings.
*/
export async function GET(request: Request) {
const url = new URL(request.url);
const siruta = url.searchParams.get("siruta") ?? "161829";
const cadRef = url.searchParams.get("cadRef") ?? "77102";
const username = process.env.ETERRA_USERNAME;
const password = process.env.ETERRA_PASSWORD;
if (!username || !password) {
return NextResponse.json({ error: "ETERRA creds missing" }, { status: 500 });
}
const result: Record<string, unknown> = {
query: { siruta, cadRef },
timestamp: new Date().toISOString(),
};
try {
const client = await EterraClient.create(username, password);
// 1. GIS layer: TERENURI_ACTIVE — raw attributes
const terenuri = await client.listLayerByWhere(
{ id: "TERENURI_ACTIVE", name: "TERENURI_ACTIVE", endpoint: "aut" },
`ADMIN_UNIT_ID=${siruta} AND IS_ACTIVE=1 AND NATIONAL_CADASTRAL_REFERENCE='${cadRef}'`,
{ limit: 1, outFields: "*" },
);
const parcelAttrs = terenuri[0]?.attributes ?? null;
result.gis_parcela = {
found: !!parcelAttrs,
fields: parcelAttrs
? Object.entries(parcelAttrs)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => ({ field: k, value: v, type: typeof v }))
: [],
};
// 2. GIS layer: CLADIRI_ACTIVE — buildings on this parcel
const cladiri = await client.listLayerByWhere(
{ id: "CLADIRI_ACTIVE", name: "CLADIRI_ACTIVE", endpoint: "aut" },
`ADMIN_UNIT_ID=${siruta} AND IS_ACTIVE=1 AND NATIONAL_CADASTRAL_REFERENCE LIKE '${cadRef}-%'`,
{ limit: 20, outFields: "*" },
);
result.gis_cladiri = {
count: cladiri.length,
buildings: cladiri.map((c) => {
const a = c.attributes;
return {
cadastralRef: a.NATIONAL_CADASTRAL_REFERENCE,
fields: Object.entries(a)
.sort(([x], [y]) => x.localeCompare(y))
.filter(([, v]) => v != null && v !== "" && v !== 0)
.map(([k, v]) => ({ field: k, value: v, type: typeof v })),
};
}),
};
// 3. Immovable details (enrichment source)
const immId = parcelAttrs?.IMMOVABLE_ID;
const wsId = parcelAttrs?.WORKSPACE_ID;
if (immId && wsId) {
try {
const details = await client.fetchImmovableParcelDetails(
wsId as string | number,
immId as string | number,
);
result.immovable_parcel_details = {
count: details.length,
items: details,
};
} catch (e) {
result.immovable_parcel_details = {
error: e instanceof Error ? e.message : String(e),
};
}
// 4. Immovable list entry (address source)
try {
const listResponse = await client.fetchImmovableListByAdminUnit(
wsId as number,
siruta,
0,
5,
true,
);
const items = (listResponse?.content ?? []) as Record<string, unknown>[];
// Find our specific immovable
const match = items.find(
(item) => String(item.immovablePk) === String(immId) ||
String(item.identifierDetails ?? "").includes(cadRef),
);
result.immovable_list_entry = {
totalInUat: listResponse?.totalElements ?? "?",
matchFound: !!match,
entry: match ?? null,
note: "Acest obiect contine campul immovableAddresses cu adresa completa",
};
} catch (e) {
result.immovable_list_entry = {
error: e instanceof Error ? e.message : String(e),
};
}
// 5. Documentation data (owner source)
try {
const docResponse = await client.fetchDocumentationData(
wsId as number,
[String(immId)],
);
const immovables = docResponse?.immovables ?? [];
const regs = docResponse?.partTwoRegs ?? [];
result.documentation_data = {
immovablesCount: immovables.length,
immovables: immovables.slice(0, 3),
registrationsCount: regs.length,
registrations: regs.slice(0, 10),
note: "partTwoRegs contine proprietarii (nodeType=P, nodeStatus=-1=radiat)",
};
} catch (e) {
result.documentation_data = {
error: e instanceof Error ? e.message : String(e),
};
}
}
// 6. Local DB data (what we have stored)
const dbParcel = await prisma.gisFeature.findFirst({
where: { layerId: "TERENURI_ACTIVE", siruta, cadastralRef: cadRef },
select: {
objectId: true,
cadastralRef: true,
areaValue: true,
isActive: true,
enrichment: true,
enrichedAt: true,
geometrySource: true,
},
});
const dbBuildings = await prisma.gisFeature.findMany({
where: {
layerId: "CLADIRI_ACTIVE",
siruta,
cadastralRef: { startsWith: `${cadRef}-` },
},
select: {
objectId: true,
cadastralRef: true,
areaValue: true,
attributes: true,
},
});
result.local_db = {
parcel: dbParcel
? {
objectId: dbParcel.objectId,
cadastralRef: dbParcel.cadastralRef,
areaValue: dbParcel.areaValue,
enrichedAt: dbParcel.enrichedAt,
geometrySource: dbParcel.geometrySource,
enrichment: dbParcel.enrichment,
}
: null,
buildings: dbBuildings.map((b) => ({
objectId: b.objectId,
cadastralRef: b.cadastralRef,
areaValue: b.areaValue,
is_legal: (b.attributes as Record<string, unknown>)?.IS_LEGAL,
})),
};
} catch (e) {
result.error = e instanceof Error ? e.message : String(e);
}
return NextResponse.json(result, {
headers: { "Content-Type": "application/json; charset=utf-8" },
});
}