feat(faza-f): ePay/CF backend swap — /api/cf/* proxies to gis-api
Plan 003 Faza F. Pilot users (session.useGisAc=true) get their CF
extract flow routed through api.gis.ac (RLS-filtered, RLS-owned
writes); everyone else keeps the legacy /api/ancpi/* path
unchanged. Feature-flag preserves rollback.
New routes (5):
- POST /api/cf/order → gisApi.enrichment.cf.create. Forwards
409 catalog_hit verbatim.
- GET /api/cf/orders → gisApi.enrichment.cf.list (limit, offset, status).
- GET /api/cf/[id] → gisApi.enrichment.cf.get.
- PATCH /api/cf/[id] → gisApi.enrichment.cf.patch.
- GET /api/cf/[id]/pdf → streams gisApi.enrichment.cf.getPdf
through to browser. Filename from documentName via cf.get; falls
back to cf-<id>.pdf.
- GET /api/cf/catalog → gisApi.enrichment.catalog.
All use getAuthSession() → 401 on no session, forward GisApiError
status+code+body, fallback {error:"internal_error", hint} at 500.
runtime=nodejs, dynamic=force-dynamic.
Helper module `cf-api-base.ts`:
- cfApiBase(useGisAc) → "/api/cf" | "/api/ancpi"
- adaptCfRow(row) → maps gisApi.CfExtractRow into the UI shape
expected by epay-tab.tsx (CfExtractRecord). Fields not in gis-api
(siruta, judetName, uatName, errorMessage, etc.) default to
empty/zero — filter-by-judet/uat on the pilot path is reduced
until gis-api enriches the response.
- fetchCfOrdersList, fetchCfHasCompletedForCadastral, placeCfOrder,
cfDownloadUrl — used by components.
UI changes:
- epay-tab.tsx: reads session.useGisAc; list fetch, reorder, single
+ bulk download routed via helpers. UI shape unchanged.
- epay-order-button.tsx: existence check uses catalog endpoint on
gis-ac path; order placement uses placeCfOrder which treats 409
catalog_hit as a soft success ("Extras CF valid").
Known gaps (followups):
- /api/ancpi/session still serves ePay session/credits — no gis-api
equivalent today. epay-connect.tsx untouched.
- ZIP bulk download has no gis-api analog; "Descarcă tot" falls back
to N tabs on gis-ac path.
- src/app/api/geoportal/cf-status returns hardcoded /api/ancpi/download
URL — separate followup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import {
|
||||
FileText,
|
||||
Download,
|
||||
@@ -23,31 +24,12 @@ import {
|
||||
} from "@/shared/components/ui/tooltip";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import type { EpaySessionStatus } from "./epay-connect";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type CfExtractRecord = {
|
||||
id: string;
|
||||
orderId: string | null;
|
||||
nrCadastral: string;
|
||||
nrCF: string | null;
|
||||
siruta: string | null;
|
||||
judetName: string;
|
||||
uatName: string;
|
||||
status: string;
|
||||
epayStatus: string | null;
|
||||
documentName: string | null;
|
||||
documentDate: string | null;
|
||||
minioPath: string | null;
|
||||
expiresAt: string | null;
|
||||
errorMessage: string | null;
|
||||
version: number;
|
||||
creditsUsed: number;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
};
|
||||
import {
|
||||
cfDownloadUrl,
|
||||
fetchCfOrdersList,
|
||||
placeCfOrder,
|
||||
type CfExtractRecord,
|
||||
} from "./cf-api-base";
|
||||
|
||||
type GisUatResult = {
|
||||
siruta: string;
|
||||
@@ -165,6 +147,12 @@ const FILTER_OPTIONS: { value: FilterValue; label: string }[] = [
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export function EpayTab() {
|
||||
/* -- Cutover flag (Plan 003, Faza F) ----------------------------- */
|
||||
const { data: session } = useSession();
|
||||
const useGisAc = Boolean(
|
||||
(session as { useGisAc?: boolean } | null)?.useGisAc,
|
||||
);
|
||||
|
||||
/* -- ePay session ------------------------------------------------ */
|
||||
const [epayStatus, setEpayStatus] = useState<EpaySessionStatus>({
|
||||
connected: false,
|
||||
@@ -193,11 +181,24 @@ export function EpayTab() {
|
||||
if (selectedIds.length === 0) return;
|
||||
setDownloadingSelection(true);
|
||||
try {
|
||||
const ids = selectedIds.join(",");
|
||||
const a = document.createElement("a");
|
||||
a.href = `/api/ancpi/download-zip?ids=${encodeURIComponent(ids)}`;
|
||||
a.download = `Extrase_CF_selectie_${selectedIds.length}.zip`;
|
||||
a.click();
|
||||
// ZIP endpoint only exists on the legacy backend today. For pilot
|
||||
// users on the gis-ac path we fall back to triggering individual
|
||||
// PDF downloads (one-by-one) until gis-api ships a batch endpoint.
|
||||
if (useGisAc) {
|
||||
for (const id of selectedIds) {
|
||||
const a = document.createElement("a");
|
||||
a.href = cfDownloadUrl(true, id);
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.click();
|
||||
}
|
||||
} else {
|
||||
const ids = selectedIds.join(",");
|
||||
const a = document.createElement("a");
|
||||
a.href = `/api/ancpi/download-zip?ids=${encodeURIComponent(ids)}`;
|
||||
a.download = `Extrase_CF_selectie_${selectedIds.length}.zip`;
|
||||
a.click();
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => setDownloadingSelection(false), 2000);
|
||||
}
|
||||
@@ -234,11 +235,7 @@ export function EpayTab() {
|
||||
async (showRefreshing = false) => {
|
||||
if (showRefreshing) setRefreshing(true);
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/orders?limit=200");
|
||||
const data = (await res.json()) as {
|
||||
orders: CfExtractRecord[];
|
||||
total: number;
|
||||
};
|
||||
const data = await fetchCfOrdersList(useGisAc, { limit: 200 });
|
||||
setOrders(data.orders);
|
||||
setTotal(data.total);
|
||||
} catch {
|
||||
@@ -248,7 +245,7 @@ export function EpayTab() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
[useGisAc],
|
||||
);
|
||||
|
||||
/* -- Initial load ------------------------------------------------ */
|
||||
@@ -307,34 +304,18 @@ export function EpayTab() {
|
||||
|
||||
/* -- Re-order (for expired extracts) ----------------------------- */
|
||||
const handleReorder = async (order: CfExtractRecord) => {
|
||||
try {
|
||||
const res = await fetch("/api/ancpi/order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
parcels: [
|
||||
{
|
||||
nrCadastral: order.nrCadastral,
|
||||
nrCF: order.nrCF,
|
||||
siruta: order.siruta,
|
||||
judetIndex: 0,
|
||||
judetName: order.judetName,
|
||||
uatId: 0,
|
||||
uatName: order.uatName,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const data = (await res.json()) as { error?: string };
|
||||
if (!res.ok || data.error) {
|
||||
/* show inline later */
|
||||
} else {
|
||||
void fetchOrders(true);
|
||||
void fetchEpayStatus();
|
||||
}
|
||||
} catch {
|
||||
/* silent */
|
||||
const result = await placeCfOrder(useGisAc, {
|
||||
nrCadastral: order.nrCadastral,
|
||||
nrCF: order.nrCF,
|
||||
siruta: order.siruta,
|
||||
judetName: order.judetName,
|
||||
uatName: order.uatName,
|
||||
});
|
||||
if (result.ok) {
|
||||
void fetchOrders(true);
|
||||
void fetchEpayStatus();
|
||||
}
|
||||
/* errors surfaced inline via downstream polling later */
|
||||
};
|
||||
|
||||
/* -- Download all valid as ZIP ----------------------------------- */
|
||||
@@ -346,23 +327,37 @@ export function EpayTab() {
|
||||
|
||||
setDownloadingAll(true);
|
||||
try {
|
||||
const ids = validOrders.map((o) => o.id).join(",");
|
||||
const res = await fetch(`/api/ancpi/download-zip?ids=${ids}`);
|
||||
if (!res.ok) throw new Error("Eroare descarcare ZIP");
|
||||
if (useGisAc) {
|
||||
// No bulk-zip endpoint on api.gis.ac yet — trigger individual
|
||||
// PDF downloads. Browser dedup will handle these as separate tabs.
|
||||
for (const o of validOrders) {
|
||||
const a = document.createElement("a");
|
||||
a.href = cfDownloadUrl(true, o.id);
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.click();
|
||||
}
|
||||
} else {
|
||||
const ids = validOrders.map((o) => o.id).join(",");
|
||||
const res = await fetch(`/api/ancpi/download-zip?ids=${ids}`);
|
||||
if (!res.ok) throw new Error("Eroare descarcare ZIP");
|
||||
|
||||
const blob = await res.blob();
|
||||
const cd = res.headers.get("Content-Disposition") ?? "";
|
||||
const match = /filename="?([^"]+)"?/.exec(cd);
|
||||
const filename = match?.[1] ? decodeURIComponent(match[1]) : "Extrase_CF.zip";
|
||||
const blob = await res.blob();
|
||||
const cd = res.headers.get("Content-Disposition") ?? "";
|
||||
const match = /filename="?([^"]+)"?/.exec(cd);
|
||||
const filename = match?.[1]
|
||||
? decodeURIComponent(match[1])
|
||||
: "Extrase_CF.zip";
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch {
|
||||
/* silent */
|
||||
} finally {
|
||||
@@ -735,7 +730,7 @@ export function EpayTab() {
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`/api/ancpi/download?id=${order.id}`}
|
||||
href={cfDownloadUrl(useGisAc, order.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -761,7 +756,7 @@ export function EpayTab() {
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`/api/ancpi/download?id=${order.id}`}
|
||||
href={cfDownloadUrl(useGisAc, order.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user