fix(epay-ui): show localitate + judet on intern extracts; hide cancelled rows

The intern (and gis-api-sourced) rows showed an empty "jud." with no UAT name
or county, and a few dead cancelled/test rows cluttered the list.

- gis-api returns siruta + uatName but judetName is null there, and the
  CfExtractRow type didn't even declare those fields so adaptCfRow blanked
  them. Added the fields to the type; adaptCfRow now surfaces uatName + siruta.
- New enrichCfLocations(rows) fills missing uatName/judetName from SIRUTA via
  the local GisUat table (batched, one query). Applied in both list proxies
  (/api/cf/orders for gis rows, /api/ancpi/orders for old legacy intern rows
  whose judetName was stored empty). So intern rows now read "LOCALITATE,
  jud. X".
- Hide status='cancelled' rows from the Extrase CF list (dead — payment
  refused / cleaned-up bad orders, e.g. the old 354686 test). failed/review
  stay (actionable via Reincearca).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-06-05 21:25:23 +03:00
parent 9b66dd6452
commit aa246c2d91
5 changed files with 87 additions and 19 deletions
+5
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { prisma } from "@/core/storage/prisma";
import { requireCfAccess } from "@/core/auth/cf-access";
import { enrichCfLocations } from "@/modules/parcel-sync/services/cf-enrich-location";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
@@ -57,6 +58,10 @@ export async function GET(req: Request) {
prisma.cfExtract.count({ where }),
]);
// Fill missing uatName/judetName from SIRUTA (old intern rows stored an
// empty judetName) so the list shows localitate + judet for them too.
await enrichCfLocations(orders);
// Build statusMap for multi-cadastral queries (or single if requested)
if (cadastralNumbers.length > 0) {
const now = new Date();
+9 -3
View File
@@ -10,6 +10,7 @@
import { NextResponse } from "next/server";
import { getAuthSession } from "@/core/auth/require-auth";
import { gisApi, GisApiError } from "@/lib/gis-api-client";
import { enrichCfLocations } from "@/modules/parcel-sync/services/cf-enrich-location";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
@@ -30,9 +31,14 @@ export async function GET(request: Request) {
const offset = Number.isFinite(offsetRaw) && offsetRaw >= 0 ? offsetRaw : 0;
try {
return NextResponse.json(
await gisApi.enrichment.cf.list({ limit, offset, status }),
);
const data = await gisApi.enrichment.cf.list({ limit, offset, status });
// gis-api returns uatName + siruta but judetName is null there — fill the
// county (and any missing UAT name) from the local GisUat table so the UI
// can show "localitate, jud. X" on intern rows too.
if (Array.isArray(data?.rows)) {
await enrichCfLocations(data.rows);
}
return NextResponse.json(data);
} catch (err) {
if (err instanceof GisApiError) {
return NextResponse.json(
+4
View File
@@ -55,6 +55,10 @@ export interface CfExtractRow {
userId: string;
nrCadastral: string;
nrCF?: string;
type?: string;
siruta?: string | null;
uatName?: string | null;
judetName?: string | null;
status:
| "pending"
| "queued"
@@ -99,10 +99,10 @@ function adaptLegacyRow(row: LegacyCfExtract): CfExtractRecord {
}
// Convert a gisApi CfExtractRow → the UI-side CfExtractRecord shape.
// gis-api currently doesn't surface uatName/siruta/judetName on the list
// endpoint, so we leave them blank; the row type defaults to "intern"
// because gis_core's CfExtract is the cf-intern store (the cutover plan
// hasn't yet moved ePay writes here).
// gis-api returns siruta + uatName (judetName is null there, but the
// /api/cf/orders proxy fills it from the local GisUat by SIRUTA — see
// enrichCfLocations). The row type defaults to "intern" because gis's
// CfExtract is primarily the cf-intern store.
export function adaptCfRow(row: CfExtractRow & { type?: string }): CfExtractRecord {
return {
id: row.id,
@@ -110,9 +110,9 @@ export function adaptCfRow(row: CfExtractRow & { type?: string }): CfExtractReco
orderId: row.orderId ?? null,
nrCadastral: row.nrCadastral,
nrCF: row.nrCF ?? null,
siruta: null,
judetName: "",
uatName: "",
siruta: row.siruta ?? null,
judetName: row.judetName ?? "",
uatName: row.uatName ?? "",
status: row.status,
epayStatus: row.epayStatus ?? null,
documentName: row.documentName ?? null,
@@ -167,12 +167,18 @@ async function fetchGisAc(
// single timeline shows ePay + intern history together. Sort newest-
// first; dedupe by id (in case the same record ever lands in both
// stores during the cutover migration).
// Cancelled rows are dead (payment refused / cleaned-up bad orders) and just
// clutter the list — hide them. failed/review stay (they're actionable).
const isListable = (r: CfExtractRecord): boolean => r.status !== "cancelled";
export async function fetchCfOrdersList(
useGisAc: boolean,
params: { limit?: number; nrCadastral?: string; status?: string } = {},
): Promise<{ orders: CfExtractRecord[]; total: number }> {
if (!useGisAc) {
return fetchLegacy(params);
const r = await fetchLegacy(params);
const orders = r.orders.filter(isListable);
return { orders, total: orders.length };
}
// Pull more rows from each side than the caller asked for so that the
@@ -191,6 +197,7 @@ export async function fetchCfOrdersList(
const seen = new Set<string>();
const dedup = merged.filter((r) => {
if (!isListable(r)) return false;
if (seen.has(r.id)) return false;
seen.add(r.id);
return true;
@@ -198,14 +205,8 @@ export async function fetchCfOrdersList(
dedup.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
const total =
(legacy.status === "fulfilled" ? legacy.value.total : 0) +
(gisac.status === "fulfilled" ? gisac.value.total : 0);
return {
orders: params.limit ? dedup.slice(0, params.limit) : dedup,
total,
};
const orders = params.limit ? dedup.slice(0, params.limit) : dedup;
return { orders, total: orders.length };
}
// Existence check used by the per-parcel order button. We check both
@@ -0,0 +1,52 @@
// Fill in uatName + judetName on CF extract rows from their SIRUTA.
//
// Intern (cf-intern) extracts — and ePay rows on the gis-api side — often
// arrive without a judetName (it's null in gis_enrichment) and sometimes
// without a uatName. Both are derivable from `siruta` via the local GisUat
// table. This batches one query for the whole page instead of N lookups.
import { prisma } from "@/core/storage/prisma";
type LocatableRow = {
siruta?: string | null;
uatName?: string | null;
judetName?: string | null;
};
/**
* Mutates rows in place: for any row with a SIRUTA whose uatName/judetName is
* blank, fill it from GisUat. Best-effort — a missing SIRUTA or a DB error
* leaves the row unchanged. Returns the same array for convenience.
*/
export async function enrichCfLocations<T extends LocatableRow>(
rows: T[],
): Promise<T[]> {
const sirutas = Array.from(
new Set(
rows
.map((r) => (r.siruta ? String(r.siruta).trim() : ""))
.filter(Boolean),
),
);
if (sirutas.length === 0) return rows;
try {
const uats = await prisma.gisUat.findMany({
where: { siruta: { in: sirutas } },
select: { siruta: true, name: true, county: true },
});
const bySiruta = new Map(uats.map((u) => [u.siruta, u]));
for (const row of rows) {
const s = row.siruta ? String(row.siruta).trim() : "";
if (!s) continue;
const uat = bySiruta.get(s);
if (!uat) continue;
if (!row.uatName && uat.name) row.uatName = uat.name;
if (!row.judetName && uat.county) row.judetName = uat.county;
}
} catch (error) {
console.warn("[cf-enrich-location] lookup failed:", error);
}
return rows;
}