feat(geoportal-v2): swap refresh path to /parcel/enrich (deep-enrich)
gis-api session shipped PR3 (gis-api 09f1ab8 + gis-sync-orchestrator
0371d81): new POST /api/v1/parcel/enrich does the full eTerra
round-trip (searchImmovableByIdentifier → fetchDocumentationData
→ fetchImmovableParcelDetails) and merges NR_CF / ADRESA / PROPRIETARI
+ 20-plus fields into gis_core.GisFeature.enrichment with a 30-day
cache. Verified on 266888 + 328607 → 27 keys with full PII.
Wired in three places:
1. src/lib/gis-api-client.ts — gisApi.parcel.enrich({siruta,
cadastralRef, force?}) thin wrapper.
2. src/app/api/gis/parcel/enrich/route.ts — architots-side proxy,
matches the parcel/tech pattern (auth check → forward → bubble up
GisApiError status codes).
3. src/modules/geoportal/v2/feature-info-panel.tsx — refreshFromAncpi
now POSTs to /api/gis/parcel/enrich instead of /api/gis/parcel/tech.
After the orchestrator returns, the panel re-fetches the canonical
record via parcela.get (when uuid known) or parcela.find (when
not), so it sees exactly what gis_core stores rather than the
orchestrator response shape.
The existing auto-trigger (fires when detail has no NR_CF/ADRESA/
PROPRIETARI) now actually fills those fields. Subsequent clicks on the
same parcel hit gis-api's 30-day cache (5ms vs 1-2s live fetch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAuthSession } from "@/core/auth/require-auth";
|
||||
import { gisApi, GisApiError, type ParcelRefBody } from "@/lib/gis-api-client";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Proxy to gis-api POST /api/v1/parcel/enrich (PR3, 2026-05-19).
|
||||
// Orchestrator does the full eTerra round-trip and populates NR_CF /
|
||||
// ADRESA / PROPRIETARI + tech fields in gis_core. 30-day cache by
|
||||
// default; force=true bypasses. After this returns, the panel does a
|
||||
// fresh parcela.get / parcela.find to read the canonical record.
|
||||
export async function POST(request: Request) {
|
||||
const session = await getAuthSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
let body: ParcelRefBody;
|
||||
try {
|
||||
body = (await request.json()) as ParcelRefBody;
|
||||
} catch {
|
||||
return NextResponse.json({ error: "invalid_body" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!body?.siruta || !body?.cadastralRef) {
|
||||
return NextResponse.json(
|
||||
{ error: "missing_fields", required: ["siruta", "cadastralRef"] },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return NextResponse.json(await gisApi.parcel.enrich(body));
|
||||
} catch (err) {
|
||||
if (err instanceof GisApiError) {
|
||||
return NextResponse.json(
|
||||
{ error: err.code, status: err.status, body: err.body },
|
||||
{ status: err.status },
|
||||
);
|
||||
}
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[gis-parcel-enrich] internal:", msg);
|
||||
return NextResponse.json(
|
||||
{ error: "internal_error", hint: msg.slice(0, 200) },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -270,6 +270,17 @@ export const gisApi = {
|
||||
body: JSON.stringify(body),
|
||||
accessToken: opts.accessToken,
|
||||
}),
|
||||
// Deep-enrich (PR3 / gis-api 09f1ab8): orchestrator looks up eTerra
|
||||
// immovable, fetches documentation + parcel details, parses NR_CF +
|
||||
// ADRESA + PROPRIETARI + 20+ more fields and merges into gis_core.
|
||||
// Cache TTL ~30d; pass force=true to bypass.
|
||||
enrich: (body: ParcelRefBody, opts: GisApiCallOpts = {}) =>
|
||||
request<unknown>("/api/v1/parcel/enrich", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
accessToken: opts.accessToken,
|
||||
}),
|
||||
unitsFetch: (body: ParcelRefBody, opts: GisApiCallOpts = {}) =>
|
||||
request<unknown>("/api/v1/parcel/units/fetch", {
|
||||
method: "POST",
|
||||
|
||||
@@ -417,7 +417,12 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
||||
setRefreshing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/gis/parcel/tech", {
|
||||
// PR3 deep-enrich path: gis-api orchestrates the eTerra round-trip
|
||||
// and persists NR_CF / ADRESA / PROPRIETARI + tech fields in gis_core
|
||||
// (30-day cache; force=true bypasses). After this returns the
|
||||
// central record is canonical — we re-fetch it via parcela.get or
|
||||
// parcela.find so the panel sees what's actually in gis_core.
|
||||
const enrichResp = await fetch("/api/gis/parcel/enrich", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -426,16 +431,17 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
||||
force: true,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
setError(body.error || "refresh_failed");
|
||||
if (!enrichResp.ok) {
|
||||
const body = await enrichResp.json().catch(() => ({}));
|
||||
setError(body.error || `enrich_failed_${enrichResp.status}`);
|
||||
return;
|
||||
}
|
||||
const techData = await res.json().catch(() => null);
|
||||
// After orchestrator updates the central DB, re-fetch via the
|
||||
// server-side find/get path so we land on the canonical shape
|
||||
// (and pick up rich enrichment that the tech response itself
|
||||
// doesn't carry).
|
||||
const enriched = (await enrichResp.json().catch(() => null)) as
|
||||
| { siruta?: string; cadastralRef?: string; enrichment?: Record<string, unknown>; enrichedAt?: string }
|
||||
| null;
|
||||
|
||||
// Re-fetch canonical record so the panel matches what other clients
|
||||
// would see (and so we get isActive / layerId / etc.).
|
||||
const id = detail?.id ?? feature.id;
|
||||
let updated: Response | null = null;
|
||||
if (id) {
|
||||
@@ -447,19 +453,18 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
||||
`&layerId=${encodeURIComponent(feature.layerId)}`,
|
||||
);
|
||||
}
|
||||
if (updated.ok) {
|
||||
if (updated && updated.ok) {
|
||||
setDetail(await updated.json());
|
||||
} else if (techData) {
|
||||
// Final fallback — project the orchestrator response if we
|
||||
// can't re-fetch the canonical record.
|
||||
const inner =
|
||||
(techData?.data as Record<string, unknown> | undefined) ?? techData;
|
||||
} else if (enriched?.enrichment) {
|
||||
// Fallback: project the enrich response directly when the
|
||||
// canonical re-fetch can't run.
|
||||
setDetail({
|
||||
siruta: feature.siruta,
|
||||
cadastralRef: feature.cadastralRef,
|
||||
siruta: enriched.siruta ?? feature.siruta,
|
||||
cadastralRef: enriched.cadastralRef ?? feature.cadastralRef,
|
||||
areaValue: feature.areaValue,
|
||||
layerId: feature.layerId,
|
||||
enrichment: inner as Record<string, unknown>,
|
||||
enrichment: enriched.enrichment,
|
||||
enrichedAt: enriched.enrichedAt,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user