fix(ancpi): complete rewrite based on Angular source code analysis
All endpoints and payloads verified against epaymentAngularApp.js: - EpayJsonInterceptor: form-urlencoded (not JSON), uses reqType param - County IDs: internal ANCPI IDs from judeteNom (NOT 0-41 indices) - UAT lookup: reqType=nomenclatorUAT&countyId=<internal_ID> - Save metadata: reqType=saveProductMetadataForBasketItem (multipart) with productMetadataJSON using stringValues[] arrays - SearchEstate: field names are identificator/judet/uat (not identifier/countyId/uatId) - Download PDF: Content-Type: application/pdf in request header - Queue resolves county+UAT IDs dynamically via getCountyList+getUatList Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+49
-108
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { EpayClient } from "@/modules/parcel-sync/services/epay-client";
|
||||
import { resolveEpayCountyIndex } from "@/modules/parcel-sync/services/epay-counties";
|
||||
import {
|
||||
createEpaySession,
|
||||
getEpayCredentials,
|
||||
@@ -11,15 +10,10 @@ export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/ancpi/test?step=login|credits|uats|search|order
|
||||
* GET /api/ancpi/test?step=login|uats|search|order
|
||||
*
|
||||
* Temporary diagnostic endpoint to test ePay integration step-by-step.
|
||||
*
|
||||
* Steps:
|
||||
* login — test login + show credits
|
||||
* uats — resolve ePay UAT IDs for Cluj-Napoca, Feleacu, Florești
|
||||
* search — search estates for the 3 test parcels
|
||||
* order — enqueue orders for the 3 test parcels (USES 3 CREDITS!)
|
||||
* Temporary diagnostic endpoint. Uses the CORRECT ePay flow:
|
||||
* shoppingCartCompleteInformation → judeteNom → nomenclatorUAT → saveMetadata
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
@@ -29,97 +23,90 @@ export async function GET(req: Request) {
|
||||
const password = process.env.ANCPI_PASSWORD ?? "";
|
||||
|
||||
if (!username || !password) {
|
||||
return NextResponse.json({
|
||||
error: "ANCPI_USERNAME / ANCPI_PASSWORD not configured",
|
||||
});
|
||||
return NextResponse.json({ error: "ANCPI credentials not configured" });
|
||||
}
|
||||
|
||||
const normalize = (s: string) =>
|
||||
s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase().trim();
|
||||
|
||||
try {
|
||||
// ── Step: login ──
|
||||
// ── login ──
|
||||
if (step === "login") {
|
||||
const client = await EpayClient.create(username, password);
|
||||
const credits = await client.getCredits();
|
||||
createEpaySession(username, password, credits);
|
||||
return NextResponse.json({
|
||||
step: "login",
|
||||
success: true,
|
||||
credits,
|
||||
countyIdxCluj: resolveEpayCountyIndex("Cluj"),
|
||||
});
|
||||
return NextResponse.json({ step: "login", success: true, credits });
|
||||
}
|
||||
|
||||
// ── Step: uats ──
|
||||
// ── uats ── Get internal county IDs + UAT list for Cluj
|
||||
if (step === "uats") {
|
||||
const client = await EpayClient.create(username, password);
|
||||
const countyIdx = resolveEpayCountyIndex("Cluj");
|
||||
if (countyIdx === null) {
|
||||
return NextResponse.json({ error: "Could not resolve Cluj county index" });
|
||||
await client.addToCart(14200); // need cart for ShowCartItems context
|
||||
|
||||
const counties = await client.getCountyList();
|
||||
const clujCounty = counties.find((c) => normalize(c.value) === "CLUJ");
|
||||
|
||||
let uatList: { id: number; value: string }[] = [];
|
||||
if (clujCounty) {
|
||||
uatList = await client.getUatList(clujCounty.id);
|
||||
}
|
||||
|
||||
const uatList = await client.getUatList(countyIdx);
|
||||
// Find the 3 UATs
|
||||
const normalize = (s: string) =>
|
||||
s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase();
|
||||
|
||||
const clujNapoca = uatList.find((u) => normalize(u.value).includes("CLUJ-NAPOCA") || normalize(u.value).includes("CLUJNAPOCA"));
|
||||
const clujNapoca = uatList.find((u) => normalize(u.value).includes("CLUJ-NAPOCA"));
|
||||
const feleacu = uatList.find((u) => normalize(u.value).includes("FELEACU"));
|
||||
const floresti = uatList.find((u) => normalize(u.value).includes("FLORESTI"));
|
||||
|
||||
return NextResponse.json({
|
||||
step: "uats",
|
||||
countyIdx,
|
||||
totalCounties: counties.length,
|
||||
countiesFirst5: counties.slice(0, 5),
|
||||
clujCounty,
|
||||
totalUats: uatList.length,
|
||||
first5: uatList.slice(0, 5),
|
||||
clujNapoca,
|
||||
feleacu,
|
||||
floresti,
|
||||
matches: { clujNapoca, feleacu, floresti },
|
||||
});
|
||||
}
|
||||
|
||||
// ── Step: search ──
|
||||
// Test UAT lookup (JSON) + SearchEstate (with basketId)
|
||||
// ── search ── SearchEstate with correct params
|
||||
if (step === "search") {
|
||||
const client = await EpayClient.create(username, password);
|
||||
const countyIdx = resolveEpayCountyIndex("Cluj")!;
|
||||
const results: Record<string, unknown> = { countyIdx };
|
||||
await client.addToCart(14200);
|
||||
|
||||
// 1. Get UAT list (JSON body now)
|
||||
const uatList = await client.getUatList(countyIdx);
|
||||
results["uatCount"] = uatList.length;
|
||||
results["uatFirst5"] = uatList.slice(0, 5);
|
||||
const counties = await client.getCountyList();
|
||||
const clujCounty = counties.find((c) => normalize(c.value) === "CLUJ");
|
||||
if (!clujCounty) {
|
||||
return NextResponse.json({ error: "CLUJ not found", counties: counties.slice(0, 5) });
|
||||
}
|
||||
|
||||
// Find our test UATs
|
||||
const normalize = (s: string) =>
|
||||
s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase();
|
||||
const clujNapoca = uatList.find((u) => normalize(u.value).includes("CLUJ-NAPOCA") || normalize(u.value).includes("CLUJNAPOCA"));
|
||||
const uatList = await client.getUatList(clujCounty.id);
|
||||
const clujNapoca = uatList.find((u) => normalize(u.value).includes("CLUJ-NAPOCA"));
|
||||
const feleacu = uatList.find((u) => normalize(u.value).includes("FELEACU"));
|
||||
const floresti = uatList.find((u) => normalize(u.value).includes("FLORESTI"));
|
||||
results["uatMatches"] = { clujNapoca, feleacu, floresti };
|
||||
|
||||
// 2. Add to cart + SearchEstate with basketId
|
||||
const basketRowId = await client.addToCart(14200);
|
||||
results["basketRowId"] = basketRowId;
|
||||
const results: Record<string, unknown> = {
|
||||
clujCountyId: clujCounty.id,
|
||||
uats: { clujNapoca, feleacu, floresti },
|
||||
};
|
||||
|
||||
// SearchEstate with correct field names: identificator, judet (internal ID), uat (real ID)
|
||||
if (clujNapoca) {
|
||||
results["search_345295"] = await client
|
||||
.searchEstate("345295", countyIdx, clujNapoca.id, basketRowId)
|
||||
.searchEstate("345295", clujCounty.id, clujNapoca.id)
|
||||
.catch((e: Error) => ({ error: e.message }));
|
||||
}
|
||||
if (feleacu) {
|
||||
results["search_63565"] = await client
|
||||
.searchEstate("63565", countyIdx, feleacu.id, basketRowId)
|
||||
.searchEstate("63565", clujCounty.id, feleacu.id)
|
||||
.catch((e: Error) => ({ error: e.message }));
|
||||
}
|
||||
if (floresti) {
|
||||
results["search_88089"] = await client
|
||||
.searchEstate("88089", countyIdx, floresti.id, basketRowId)
|
||||
.searchEstate("88089", clujCounty.id, floresti.id)
|
||||
.catch((e: Error) => ({ error: e.message }));
|
||||
}
|
||||
|
||||
return NextResponse.json({ step: "search", results });
|
||||
}
|
||||
|
||||
// ── Step: order ── (USES 3 CREDITS!)
|
||||
// ── order ── (USES 3 CREDITS!)
|
||||
if (step === "order") {
|
||||
if (!getEpayCredentials()) {
|
||||
createEpaySession(username, password, 0);
|
||||
@@ -130,53 +117,15 @@ export async function GET(req: Request) {
|
||||
createEpaySession(username, password, credits);
|
||||
|
||||
if (credits < 3) {
|
||||
return NextResponse.json({
|
||||
error: `Doar ${credits} credite disponibile, trebuie 3.`,
|
||||
});
|
||||
}
|
||||
|
||||
const countyIdx = resolveEpayCountyIndex("Cluj")!;
|
||||
const uatList = await client.getUatList(countyIdx);
|
||||
const normalize = (s: string) =>
|
||||
s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase();
|
||||
|
||||
const findUat = (name: string) =>
|
||||
uatList.find((u) => normalize(u.value).includes(name));
|
||||
|
||||
const clujNapoca = findUat("CLUJ-NAPOCA") ?? findUat("CLUJNAPOCA");
|
||||
const feleacu = findUat("FELEACU");
|
||||
const floresti = findUat("FLORESTI");
|
||||
|
||||
if (!clujNapoca || !feleacu || !floresti) {
|
||||
return NextResponse.json({
|
||||
error: "Nu s-au găsit UAT-urile.",
|
||||
uatCount: uatList.length,
|
||||
clujNapoca, feleacu, floresti,
|
||||
});
|
||||
return NextResponse.json({ error: `Doar ${credits} credite, trebuie 3.` });
|
||||
}
|
||||
|
||||
// Resolve county + UAT IDs through the queue
|
||||
// Queue handles getCountyList + getUatList internally
|
||||
const parcels = [
|
||||
{
|
||||
nrCadastral: "345295",
|
||||
judetIndex: countyIdx,
|
||||
judetName: "CLUJ",
|
||||
uatId: clujNapoca.id,
|
||||
uatName: clujNapoca.value,
|
||||
},
|
||||
{
|
||||
nrCadastral: "63565",
|
||||
judetIndex: countyIdx,
|
||||
judetName: "CLUJ",
|
||||
uatId: feleacu.id,
|
||||
uatName: feleacu.value,
|
||||
},
|
||||
{
|
||||
nrCadastral: "88089",
|
||||
judetIndex: countyIdx,
|
||||
judetName: "CLUJ",
|
||||
uatId: floresti.id,
|
||||
uatName: floresti.value,
|
||||
},
|
||||
{ nrCadastral: "345295", judetName: "CLUJ", uatName: "Cluj-Napoca", judetIndex: 0, uatId: 0 },
|
||||
{ nrCadastral: "63565", judetName: "CLUJ", uatName: "Feleacu", judetIndex: 0, uatId: 0 },
|
||||
{ nrCadastral: "88089", judetName: "CLUJ", uatName: "Floresti", judetIndex: 0, uatId: 0 },
|
||||
];
|
||||
|
||||
const ids: string[] = [];
|
||||
@@ -188,20 +137,12 @@ export async function GET(req: Request) {
|
||||
return NextResponse.json({
|
||||
step: "order",
|
||||
credits,
|
||||
message: `Enqueued ${ids.length} orders. Processing sequentially...`,
|
||||
message: `Enqueued ${ids.length} orders. Queue resolves county/UAT IDs automatically.`,
|
||||
orderIds: ids,
|
||||
parcels: parcels.map((p, i) => ({
|
||||
nrCadastral: p.nrCadastral,
|
||||
uatName: p.uatName,
|
||||
uatId: p.uatId,
|
||||
extractId: ids[i],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
error: `Unknown step: ${step}. Use: login, uats, search, order`,
|
||||
});
|
||||
return NextResponse.json({ error: `Unknown step: ${step}` });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[ancpi-test] Step ${step} failed:`, message);
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
/**
|
||||
* ANCPI ePay client — server-side only.
|
||||
*
|
||||
* Authenticates via oassl.ancpi.ro, keeps cookie jar (iPlanetDirectoryPro + JSESSIONID),
|
||||
* and implements the full CF extract ordering workflow:
|
||||
* login → check credits → add to cart → search estate →
|
||||
* configure & submit → poll status → download PDF
|
||||
* Reverse-engineered from epaymentAngularApp.js (123KB).
|
||||
* All endpoints and payloads verified against actual Angular source.
|
||||
*
|
||||
* Modeled after eterra-client.ts: cookie jar, session cache, retry, auto-relogin.
|
||||
* Flow:
|
||||
* 1. Login (OpenAM) → AMAuthCookie
|
||||
* 2. GET ShowCartItems → Angular session context
|
||||
* 3. EpayJsonInterceptor(shoppingCartCompleteInformation) → judeteNom IDs
|
||||
* 4. EpayJsonInterceptor(nomenclatorUAT) → UAT IDs
|
||||
* 5. EpayJsonInterceptor(saveProductMetadataForBasketItem) → save (multipart)
|
||||
* 6. EditCartSubmit(goToCheckout=true) → order
|
||||
* 7. Poll ShowOrderDetails → idDocument
|
||||
* 8. DownloadFile(Content-Type: application/pdf) → PDF
|
||||
*/
|
||||
|
||||
import axios, { type AxiosError, type AxiosInstance } from "axios";
|
||||
import axios, { type AxiosInstance } from "axios";
|
||||
import crypto from "crypto";
|
||||
import { wrapper } from "axios-cookiejar-support";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
import FormData from "form-data";
|
||||
import type {
|
||||
EpayCartResponse,
|
||||
EpaySearchResult,
|
||||
EpayUatEntry,
|
||||
EpayOrderStatus,
|
||||
EpaySolutionDoc,
|
||||
OrderMetadata,
|
||||
} from "./epay-types";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -30,14 +36,13 @@ const BASE_URL = process.env.ANCPI_BASE_URL || "https://epay.ancpi.ro/epay";
|
||||
const LOGIN_URL =
|
||||
process.env.ANCPI_LOGIN_URL ||
|
||||
"https://oassl.ancpi.ro/openam/UI/Login";
|
||||
const DEFAULT_TIMEOUT_MS = 60_000; // ePay is slow
|
||||
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour (ePay sessions last longer)
|
||||
const MAX_RETRIES = 3;
|
||||
const DEFAULT_TIMEOUT_MS = 60_000;
|
||||
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||
const POLL_INTERVAL_MS = 15_000;
|
||||
const POLL_MAX_ATTEMPTS = 40; // 10 minutes
|
||||
const POLL_MAX_ATTEMPTS = 40;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Session cache (global singleton) */
|
||||
/* Session cache */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type SessionEntry = {
|
||||
@@ -57,17 +62,8 @@ globalStore.__epaySessionCache = sessionCache;
|
||||
const makeCacheKey = (u: string, p: string) =>
|
||||
crypto.createHash("sha256").update(`epay:${u}:${p}`).digest("hex");
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const isRedirectToLogin = (html: string) =>
|
||||
html.includes("IDToken1") ||
|
||||
html.includes("/openam/UI/Login") ||
|
||||
html.includes("j_security_check");
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Client */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -77,7 +73,6 @@ export class EpayClient {
|
||||
private jar: CookieJar;
|
||||
private username: string;
|
||||
private password: string;
|
||||
private reloginAttempted = false;
|
||||
|
||||
private constructor(
|
||||
client: AxiosInstance,
|
||||
@@ -108,10 +103,9 @@ export class EpayClient {
|
||||
axios.create({
|
||||
jar,
|
||||
withCredentials: true,
|
||||
maxRedirects: 5,
|
||||
maxRedirects: 10,
|
||||
headers: {
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
@@ -127,198 +121,63 @@ export class EpayClient {
|
||||
/* ── Auth ───────────────────────────────────────────────────── */
|
||||
|
||||
private async login(): Promise<void> {
|
||||
// Full login URL with module + goto params (required by OpenAM)
|
||||
const gotoUrl = "http://epay.ancpi.ro:80/epay/LogIn.action";
|
||||
const loginUrlFull = `${LOGIN_URL}?module=SelfRegistration&goto=${encodeURIComponent(gotoUrl)}`;
|
||||
|
||||
// Step 1: GET the login page first (sets initial cookies + form tokens)
|
||||
// Step 1: GET login page (sets initial cookies)
|
||||
await this.client.get(loginUrlFull, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
maxRedirects: 5,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
// Step 2: POST credentials
|
||||
const body = `IDToken1=${encodeURIComponent(this.username)}&IDToken2=${encodeURIComponent(this.password)}`;
|
||||
|
||||
const response = await this.client.post(loginUrlFull, body, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Referer: loginUrlFull,
|
||||
},
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
maxRedirects: 10,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
const finalUrl =
|
||||
response.request?.res?.responseUrl ??
|
||||
response.request?.responseURL ??
|
||||
"";
|
||||
const html = typeof response.data === "string" ? response.data : "";
|
||||
|
||||
console.log(
|
||||
`[epay] Login POST: status=${response.status}, finalUrl=${finalUrl.slice(0, 120)}`,
|
||||
);
|
||||
|
||||
// Auth failure check
|
||||
if (
|
||||
html.includes("Authentication Failed") ||
|
||||
html.includes("Autentificare esuata")
|
||||
) {
|
||||
if (html.includes("Authentication Failed") || html.includes("Autentificare esuata")) {
|
||||
throw new Error("ePay login failed (invalid credentials)");
|
||||
}
|
||||
|
||||
// Step 3: Navigate to ePay to establish JSESSIONID
|
||||
// Try the goto URL (HTTP) and HTTPS variants
|
||||
const epayUrls = [gotoUrl, `${BASE_URL}/LogIn.action`];
|
||||
let loggedIn = false;
|
||||
|
||||
for (const epayUrl of epayUrls) {
|
||||
try {
|
||||
const epayResponse = await this.client.get(epayUrl, {
|
||||
const epayResponse = await this.client.get(gotoUrl, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
maxRedirects: 10,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
const epayHtml = String(epayResponse.data ?? "");
|
||||
|
||||
if (
|
||||
epayHtml.includes("puncte de credit") ||
|
||||
epayHtml.includes("LogOut") ||
|
||||
epayHtml.includes("Istoric Comenzi")
|
||||
) {
|
||||
loggedIn = true;
|
||||
console.log(`[epay] Session established via ${epayUrl}`);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Try next
|
||||
}
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
// Log cookies for debugging
|
||||
const cookieKeys: string[] = [];
|
||||
for (const domain of [
|
||||
"https://epay.ancpi.ro",
|
||||
"https://oassl.ancpi.ro",
|
||||
"http://epay.ancpi.ro",
|
||||
]) {
|
||||
try {
|
||||
const cookies = await this.jar.getCookies(domain);
|
||||
for (const c of cookies) cookieKeys.push(`${c.key}@${domain}`);
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
console.error(`[epay] Login failed. Cookies: ${cookieKeys.join(", ")}`);
|
||||
throw new Error("ePay login failed (could not establish session)");
|
||||
if (!epayHtml.includes("puncte de credit") && !epayHtml.includes("LogOut")) {
|
||||
// Try HTTPS
|
||||
await this.client.get(`${BASE_URL}/LogIn.action`, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[epay] Login successful.");
|
||||
}
|
||||
|
||||
private async ensureSession(): Promise<void> {
|
||||
if (this.reloginAttempted) return;
|
||||
// Re-login if needed (lazy check — will be triggered on redirects)
|
||||
}
|
||||
|
||||
private async retryOnAuthFail<T>(fn: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
const html =
|
||||
typeof err?.response?.data === "string" ? err.response.data : "";
|
||||
|
||||
if (
|
||||
(err?.response?.status === 302 || isRedirectToLogin(html)) &&
|
||||
!this.reloginAttempted
|
||||
) {
|
||||
this.reloginAttempted = true;
|
||||
await this.login();
|
||||
this.reloginAttempted = false;
|
||||
return await fn();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Raw HTML fetch (for page scraping) ─────────────────── */
|
||||
|
||||
async getRawHtml(url: string): Promise<string> {
|
||||
const response = await this.client.get(url, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
maxRedirects: 5,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
return String(response.data ?? "");
|
||||
}
|
||||
|
||||
/* ── Raw POST (for endpoint discovery) ──────────────────── */
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async postRaw(url: string, body: string, extraHeaders?: Record<string, string>): Promise<any> {
|
||||
const response = await this.client.post(url, body, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
...extraHeaders,
|
||||
},
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
maxRedirects: 5,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/* ── Credits ───────────────────────────────────────────────── */
|
||||
|
||||
async getCredits(): Promise<number> {
|
||||
return this.retryOnAuthFail(async () => {
|
||||
const response = await this.client.get(`${BASE_URL}/LogIn.action`, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
});
|
||||
const html = String(response.data ?? "");
|
||||
|
||||
// Parse "Ai {N} puncte de credit"
|
||||
const match = html.match(/Ai\s+(\d+)\s+puncte?\s+de\s+credit/i);
|
||||
if (match) return parseInt(match[1] ?? "0", 10);
|
||||
|
||||
// Fallback: look for credit count in other formats
|
||||
const match2 = html.match(/(\d+)\s+puncte?\s+de\s+credit/i);
|
||||
if (match2) return parseInt(match2[1] ?? "0", 10);
|
||||
|
||||
// Broader search: any number near "credit"
|
||||
const match3 = html.match(/credit[^<]{0,30}?(\d+)|(\d+)[^<]{0,30}?credit/i);
|
||||
if (match3) {
|
||||
const n = parseInt(match3[1] ?? match3[2] ?? "0", 10);
|
||||
if (n > 0) return n;
|
||||
}
|
||||
|
||||
// Log context around "credit" or "punct" for debugging
|
||||
const creditIdx = html.toLowerCase().indexOf("credit");
|
||||
if (creditIdx >= 0) {
|
||||
console.log(
|
||||
"[epay] Credit context:",
|
||||
html.slice(Math.max(0, creditIdx - 80), creditIdx + 80),
|
||||
);
|
||||
} else {
|
||||
// Maybe the page didn't load — check if we're on the login page
|
||||
const isLoginPage = html.includes("IDToken1");
|
||||
console.warn(
|
||||
`[epay] No 'credit' found in HTML (${html.length} chars, isLogin=${isLoginPage})`,
|
||||
);
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
const match = html.match(/(\d+)\s+puncte?\s+de\s+credit/i);
|
||||
return match ? parseInt(match[1] ?? "0", 10) : 0;
|
||||
}
|
||||
|
||||
/* ── Cart ───────────────────────────────────────────────────── */
|
||||
|
||||
async addToCart(prodId = 14200): Promise<number> {
|
||||
return this.retryOnAuthFail(async () => {
|
||||
const body = new URLSearchParams();
|
||||
body.set("prodId", String(prodId));
|
||||
body.set("productQtyModif", "1");
|
||||
@@ -341,42 +200,102 @@ export class EpayClient {
|
||||
const data = response.data as EpayCartResponse;
|
||||
const item = data?.items?.[0];
|
||||
if (!item?.id) {
|
||||
throw new Error(
|
||||
`ePay addToCart failed: ${JSON.stringify(data).slice(0, 200)}`,
|
||||
);
|
||||
throw new Error(`ePay addToCart failed: ${JSON.stringify(data).slice(0, 200)}`);
|
||||
}
|
||||
|
||||
console.log(`[epay] Added to cart: basketRowId=${item.id}`);
|
||||
return item.id;
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Estate Search ─────────────────────────────────────────── */
|
||||
/* ── EpayJsonInterceptor (form-urlencoded) ─────────────────── */
|
||||
|
||||
/**
|
||||
* Search estate on ePay. Requires basketRowId in context.
|
||||
* countyIdx = ePay county index (0-41), uatId = real ePay UAT ID (NOT index).
|
||||
* Call EpayJsonInterceptor with form-urlencoded body.
|
||||
* This is how Angular's doPostAsForm works.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async interceptor(params: Record<string, string>): Promise<any> {
|
||||
const body = new URLSearchParams(params).toString();
|
||||
const response = await this.client.post(
|
||||
`${BASE_URL}/EpayJsonInterceptor.action`,
|
||||
body,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
const data = response.data;
|
||||
if (data?.jsonResult && typeof data.jsonResult === "string") {
|
||||
try {
|
||||
return JSON.parse(data.jsonResult);
|
||||
} catch { /* not JSON */ }
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/* ── Get County IDs (internal ANCPI IDs, NOT 0-41 indices) ── */
|
||||
|
||||
/**
|
||||
* Fetch shoppingCartCompleteInformation to get judeteNom
|
||||
* (internal county IDs used by all other ePay endpoints).
|
||||
*/
|
||||
async getCountyList(): Promise<EpayUatEntry[]> {
|
||||
// Must load ShowCartItems first for Angular context
|
||||
await this.client.get(`${BASE_URL}/ShowCartItems.action`, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
const data = await this.interceptor({
|
||||
reqType: "shoppingCartCompleteInformation",
|
||||
});
|
||||
|
||||
const judeteNom = data?.pNomenclatoareMap?.judeteNom;
|
||||
if (Array.isArray(judeteNom)) {
|
||||
console.log(`[epay] Got ${judeteNom.length} counties from judeteNom.`);
|
||||
return judeteNom as EpayUatEntry[];
|
||||
}
|
||||
|
||||
console.warn("[epay] Could not get judeteNom:", JSON.stringify(data).slice(0, 300));
|
||||
return [];
|
||||
}
|
||||
|
||||
/* ── UAT Lookup ────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Get UAT list for a county using its INTERNAL ID (from judeteNom).
|
||||
*/
|
||||
async getUatList(countyInternalId: number): Promise<EpayUatEntry[]> {
|
||||
const data = await this.interceptor({
|
||||
reqType: "nomenclatorUAT",
|
||||
countyId: String(countyInternalId),
|
||||
});
|
||||
|
||||
if (Array.isArray(data)) return data as EpayUatEntry[];
|
||||
console.warn(`[epay] getUatList(${countyInternalId}):`, JSON.stringify(data).slice(0, 200));
|
||||
return [];
|
||||
}
|
||||
|
||||
/* ── SearchEstate (optional) ───────────────────────────────── */
|
||||
|
||||
async searchEstate(
|
||||
identifier: string,
|
||||
countyIdx: number,
|
||||
countyInternalId: number,
|
||||
uatId: number,
|
||||
basketRowId: number,
|
||||
): Promise<EpaySearchResult[]> {
|
||||
return this.retryOnAuthFail(async () => {
|
||||
// Must include basketId for SearchEstate to return JSON
|
||||
const body = new URLSearchParams();
|
||||
body.set("identifier", identifier);
|
||||
body.set("countyId", String(countyIdx));
|
||||
body.set("uatId", String(uatId));
|
||||
body.set("basketId", String(basketRowId));
|
||||
body.set("identificator", identifier); // NOT "identifier"!
|
||||
body.set("judet", String(countyInternalId)); // NOT "countyId"!
|
||||
body.set("uat", String(uatId)); // real UAT ID
|
||||
|
||||
const response = await this.client.post(
|
||||
`${BASE_URL}/SearchEstate.action`,
|
||||
body.toString(),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
@@ -385,134 +304,140 @@ export class EpayClient {
|
||||
|
||||
const data = response.data;
|
||||
if (Array.isArray(data)) return data as EpaySearchResult[];
|
||||
|
||||
if (typeof data === "string") {
|
||||
const trimmed = data.trim();
|
||||
if (trimmed.startsWith("[")) {
|
||||
try {
|
||||
return JSON.parse(trimmed) as EpaySearchResult[];
|
||||
} catch { /* not JSON */ }
|
||||
if (typeof data === "string" && data.trim().startsWith("[")) {
|
||||
try { return JSON.parse(data) as EpaySearchResult[]; } catch { /* */ }
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[epay] SearchEstate(${identifier}): unexpected response`,
|
||||
String(typeof data === "string" ? data : JSON.stringify(data)).slice(0, 200),
|
||||
);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/* ── UAT Lookup ────────────────────────────────────────────── */
|
||||
/* ── Save Metadata (multipart/form-data) ───────────────────── */
|
||||
|
||||
/**
|
||||
* Get UAT list for a county. Uses JSON Content-Type (not form-urlencoded).
|
||||
* Save cart item metadata via EpayJsonInterceptor with
|
||||
* reqType=saveProductMetadataForBasketItem.
|
||||
* Uses multipart/form-data with productMetadataJSON field.
|
||||
*/
|
||||
async getUatList(countyIdx: number): Promise<EpayUatEntry[]> {
|
||||
return this.retryOnAuthFail(async () => {
|
||||
// EpayJsonInterceptor requires JSON body, not form-urlencoded
|
||||
async saveMetadata(
|
||||
basketRowId: number,
|
||||
countyInternalId: number,
|
||||
countyName: string,
|
||||
uatId: number,
|
||||
uatName: string,
|
||||
nrCF: string,
|
||||
nrCadastral: string,
|
||||
solicitantId: string,
|
||||
): Promise<boolean> {
|
||||
const metadata = {
|
||||
basketId: basketRowId,
|
||||
productId: 14200,
|
||||
metadate: {
|
||||
judet: {
|
||||
name: "judet",
|
||||
bigDecimalValue: countyInternalId,
|
||||
stringValue: countyName,
|
||||
stringValues: null,
|
||||
validators: { required: "required" },
|
||||
},
|
||||
uat: {
|
||||
name: "uat",
|
||||
bigDecimalValue: uatId,
|
||||
stringValue: uatName,
|
||||
stringValues: null,
|
||||
validators: { required: "required" },
|
||||
},
|
||||
CF: {
|
||||
name: "CF",
|
||||
stringValue: null,
|
||||
stringValues: [nrCF],
|
||||
validators: { required: "required" },
|
||||
},
|
||||
CAD: {
|
||||
name: "CAD",
|
||||
stringValue: null,
|
||||
stringValues: [nrCadastral],
|
||||
validators: {},
|
||||
},
|
||||
TOPO: {
|
||||
name: "TOPO",
|
||||
stringValue: null,
|
||||
stringValues: [null],
|
||||
validators: {},
|
||||
},
|
||||
metodeLivrare: {
|
||||
name: "metodeLivrare",
|
||||
stringValue: "Electronic",
|
||||
stringValues: ["Electronic"],
|
||||
},
|
||||
differentSolicitant: {
|
||||
name: "differentSolicitant",
|
||||
stringValue: solicitantId,
|
||||
stringValues: [" "],
|
||||
},
|
||||
electronicSignedDocs: {
|
||||
name: "electronicSignedDocs",
|
||||
booleanValue: true,
|
||||
},
|
||||
enableANCPIConversation: {
|
||||
name: "enableANCPIConversation",
|
||||
booleanValue: false,
|
||||
stringValue: "false",
|
||||
},
|
||||
isMandatoryCADorTOPO: {
|
||||
name: "isMandatoryCADorTOPO",
|
||||
booleanValue: true,
|
||||
},
|
||||
singleUnit: {
|
||||
name: "singleUnit",
|
||||
booleanValue: false,
|
||||
},
|
||||
requiresReexaminare: {
|
||||
name: "requiresReexaminare",
|
||||
booleanValue: false,
|
||||
},
|
||||
},
|
||||
manufacturer: "ANCPI",
|
||||
isExternalSystem: false,
|
||||
configurationUrl: "http://www.google.com",
|
||||
};
|
||||
|
||||
const form = new FormData();
|
||||
form.append("reqType", "saveProductMetadataForBasketItem");
|
||||
form.append("productMetadataJSON", JSON.stringify(metadata));
|
||||
|
||||
const response = await this.client.post(
|
||||
`${BASE_URL}/EpayJsonInterceptor.action`,
|
||||
JSON.stringify({ judet: countyIdx }),
|
||||
form,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...form.getHeaders(),
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
const result =
|
||||
typeof data?.jsonResult === "string"
|
||||
? JSON.parse(data.jsonResult)
|
||||
: data;
|
||||
|
||||
// Response: { jsonResult: "[{\"id\":55473,\"value\":\"Aghiresu\"}, ...]" }
|
||||
if (data?.jsonResult && typeof data.jsonResult === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(data.jsonResult);
|
||||
if (Array.isArray(parsed)) return parsed as EpayUatEntry[];
|
||||
} catch { /* parse failed */ }
|
||||
if (result?.code === "SUCCESS") {
|
||||
console.log(`[epay] Metadata saved for basket ${basketRowId}.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) return data as EpayUatEntry[];
|
||||
|
||||
console.warn(
|
||||
`[epay] getUatList(${countyIdx}):`,
|
||||
JSON.stringify(data).slice(0, 200),
|
||||
);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Configure Cart Item (save metadata via JSON) ──────────── */
|
||||
|
||||
/**
|
||||
* Save cart item metadata via EditCartItemJson. Uses JSON body
|
||||
* with bigDecimalValue/stringValue structure.
|
||||
*/
|
||||
async configureCartItem(
|
||||
basketRowId: number,
|
||||
countyIdx: number,
|
||||
uatId: number,
|
||||
nrCF: string,
|
||||
nrCadastral: string,
|
||||
solicitantId: string,
|
||||
): Promise<void> {
|
||||
await this.retryOnAuthFail(async () => {
|
||||
const payload = {
|
||||
basketId: basketRowId,
|
||||
metadate: {
|
||||
judet: { bigDecimalValue: countyIdx },
|
||||
uat: { bigDecimalValue: uatId },
|
||||
CF: { stringValue: nrCF },
|
||||
CAD: { stringValue: nrCadastral },
|
||||
metodeLivrare: {
|
||||
stringValue: "Electronic",
|
||||
stringValues: ["Electronic"],
|
||||
},
|
||||
differentSolicitant: { stringValue: solicitantId },
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.client.post(
|
||||
`${BASE_URL}/EditCartItemJson.action`,
|
||||
JSON.stringify(payload),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[epay] ConfigureCartItem(${basketRowId}): status=${response.status}`,
|
||||
);
|
||||
});
|
||||
console.error("[epay] saveMetadata failed:", JSON.stringify(result).slice(0, 300));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ── Order Submission ──────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Submit order after configuring cart item via EditCartItemJson.
|
||||
*/
|
||||
async submitOrder(metadata: OrderMetadata): Promise<string> {
|
||||
return this.retryOnAuthFail(async () => {
|
||||
// First configure the cart item via JSON API
|
||||
await this.configureCartItem(
|
||||
metadata.basketRowId,
|
||||
metadata.judetIndex,
|
||||
metadata.uatId,
|
||||
metadata.nrCF,
|
||||
metadata.nrCadastral,
|
||||
metadata.solicitantId,
|
||||
);
|
||||
|
||||
// Then submit checkout via form (EditCartSubmit still uses form encoding)
|
||||
async submitOrder(): Promise<string> {
|
||||
const body = new URLSearchParams();
|
||||
body.set("goToCheckout", "true");
|
||||
|
||||
const response = await this.client.post(
|
||||
await this.client.post(
|
||||
`${BASE_URL}/EditCartSubmit.action`,
|
||||
body.toString(),
|
||||
{
|
||||
@@ -523,73 +448,35 @@ export class EpayClient {
|
||||
},
|
||||
);
|
||||
|
||||
// After submit, follow to CheckoutConfirmationSubmit
|
||||
const finalUrl =
|
||||
response.request?.res?.responseUrl ?? response.config?.url ?? "";
|
||||
const html = String(response.data ?? "");
|
||||
|
||||
// Try to get the order ID from the confirmation page or redirect
|
||||
// If we're on the confirmation page, parse orderId from the recent orders
|
||||
if (
|
||||
finalUrl.includes("CheckoutConfirmation") ||
|
||||
html.includes("Vă mulțumim")
|
||||
) {
|
||||
return this.getLatestOrderId();
|
||||
}
|
||||
|
||||
// If redirected back to cart, the submission may have failed
|
||||
if (finalUrl.includes("ShowCartItems") || finalUrl.includes("ViewCart")) {
|
||||
throw new Error("ePay order submission failed — redirected to cart");
|
||||
}
|
||||
|
||||
// Try to get orderId anyway
|
||||
return this.getLatestOrderId();
|
||||
});
|
||||
}
|
||||
|
||||
/** Get the most recent order ID from the order history page */
|
||||
private async getLatestOrderId(): Promise<string> {
|
||||
const response = await this.client.get(`${BASE_URL}/LogIn.action`, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
});
|
||||
const html = String(response.data ?? "");
|
||||
|
||||
// Parse orderId from the orders table
|
||||
// Look for ShowOrderDetails.action?orderId=XXXXXXX
|
||||
const match = html.match(
|
||||
/ShowOrderDetails\.action\?orderId=(\d+)/,
|
||||
);
|
||||
const match = html.match(/ShowOrderDetails\.action\?orderId=(\d+)/);
|
||||
if (match) return match[1] ?? "";
|
||||
|
||||
// Fallback: look for order ID patterns
|
||||
const match2 = html.match(/orderId[=:][\s"]*(\d{5,})/);
|
||||
if (match2) return match2[1] ?? "";
|
||||
|
||||
throw new Error("Could not determine orderId after checkout");
|
||||
}
|
||||
|
||||
/* ── Order Status & Polling ────────────────────────────────── */
|
||||
|
||||
async getOrderStatus(orderId: string): Promise<EpayOrderStatus> {
|
||||
return this.retryOnAuthFail(async () => {
|
||||
const response = await this.client.get(
|
||||
`${BASE_URL}/ShowOrderDetails.action?orderId=${orderId}`,
|
||||
{ timeout: DEFAULT_TIMEOUT_MS },
|
||||
);
|
||||
const html = String(response.data ?? "");
|
||||
|
||||
// Parse status
|
||||
let status = "Receptionata";
|
||||
if (html.includes("Finalizata")) status = "Finalizata";
|
||||
else if (html.includes("In curs de procesare"))
|
||||
status = "In curs de procesare";
|
||||
else if (html.includes("In curs de procesare")) status = "In curs de procesare";
|
||||
else if (html.includes("Anulata")) status = "Anulata";
|
||||
else if (html.includes("Plata refuzata")) status = "Plata refuzata";
|
||||
|
||||
// Parse documents — look for JSON-like solutie data in Angular scope
|
||||
const documents: EpaySolutionDoc[] = [];
|
||||
|
||||
// Pattern: idDocument, nume, dataDocument, linkDownload
|
||||
const docPattern =
|
||||
/"idDocument"\s*:\s*(\d+)[\s\S]*?"nume"\s*:\s*"([^"]+)"[\s\S]*?"dataDocument"\s*:\s*"([^"]+)"[\s\S]*?"linkDownload"\s*:\s*"([^"]*)"/g;
|
||||
let docMatch;
|
||||
@@ -611,63 +498,62 @@ export class EpayClient {
|
||||
}
|
||||
|
||||
return { orderId, status, documents };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll order status until completed or timeout.
|
||||
* Returns the final status with document info.
|
||||
*/
|
||||
async pollUntilComplete(
|
||||
orderId: string,
|
||||
onProgress?: (attempt: number, status: string) => void,
|
||||
): Promise<EpayOrderStatus> {
|
||||
for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) {
|
||||
const status = await this.getOrderStatus(orderId);
|
||||
|
||||
if (onProgress) onProgress(attempt, status.status);
|
||||
|
||||
if (
|
||||
status.status === "Finalizata" ||
|
||||
status.status === "Anulata" ||
|
||||
status.status === "Plata refuzata"
|
||||
) {
|
||||
if (["Finalizata", "Anulata", "Plata refuzata"].includes(status.status)) {
|
||||
return status;
|
||||
}
|
||||
|
||||
await sleep(POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`ePay order ${orderId} timed out after ${POLL_MAX_ATTEMPTS} poll attempts`,
|
||||
);
|
||||
throw new Error(`ePay order ${orderId} timed out after ${POLL_MAX_ATTEMPTS} poll attempts`);
|
||||
}
|
||||
|
||||
/* ── Document Download ─────────────────────────────────────── */
|
||||
|
||||
async downloadDocument(
|
||||
idDocument: number,
|
||||
typeD = 4,
|
||||
): Promise<Buffer> {
|
||||
return this.retryOnAuthFail(async () => {
|
||||
async downloadDocument(idDocument: number, typeD = 4): Promise<Buffer> {
|
||||
const url = `${BASE_URL}/DownloadFile.action?typeD=${typeD}&id=${idDocument}&source=&browser=chrome`;
|
||||
|
||||
// Angular sends Content-Type: application/pdf in the REQUEST
|
||||
const response = await this.client.post(url, null, {
|
||||
headers: { "Content-Type": "application/pdf" },
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
if (!data || data.length < 100) {
|
||||
throw new Error(
|
||||
`ePay download returned empty/small response (${data?.length ?? 0} bytes)`,
|
||||
);
|
||||
throw new Error(`ePay download empty (${data?.length ?? 0} bytes)`);
|
||||
}
|
||||
console.log(`[epay] Downloaded document ${idDocument}: ${data.length} bytes`);
|
||||
return Buffer.from(data);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[epay] Downloaded document ${idDocument}: ${data.length} bytes`,
|
||||
);
|
||||
return Buffer.from(data);
|
||||
/* ── Utility ───────────────────────────────────────────────── */
|
||||
|
||||
async getRawHtml(url: string): Promise<string> {
|
||||
const response = await this.client.get(url, {
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
return String(response.data ?? "");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async postRaw(url: string, body: string, extraHeaders?: Record<string, string>): Promise<any> {
|
||||
const response = await this.client.post(url, body, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
...extraHeaders,
|
||||
},
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,20 +175,54 @@ async function processItem(item: QueueItem): Promise<void> {
|
||||
const basketRowId = await client.addToCart(input.prodId ?? 14200);
|
||||
await updateStatus(extractId, "cart", { basketRowId });
|
||||
|
||||
// Step 3: Configure + submit order
|
||||
// Skip SearchEstate — we already have the data from eTerra.
|
||||
// configureCartItem (via EditCartItemJson) + submitOrder (via EditCartSubmit)
|
||||
// Step 3: Resolve internal county + UAT IDs
|
||||
await updateStatus(extractId, "searching");
|
||||
const counties = await client.getCountyList();
|
||||
const normalize = (s: string) =>
|
||||
s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase().trim();
|
||||
|
||||
const countyMatch = counties.find(
|
||||
(c) => normalize(c.value) === normalize(input.judetName),
|
||||
);
|
||||
if (!countyMatch) {
|
||||
await updateStatus(extractId, "failed", {
|
||||
errorMessage: `Județul "${input.judetName}" nu a fost găsit în ePay. Disponibile: ${counties.slice(0, 5).map((c) => c.value).join(", ")}...`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const uats = await client.getUatList(countyMatch.id);
|
||||
const uatMatch = uats.find(
|
||||
(u) => normalize(u.value) === normalize(input.uatName),
|
||||
);
|
||||
if (!uatMatch) {
|
||||
await updateStatus(extractId, "failed", {
|
||||
errorMessage: `UAT "${input.uatName}" nu a fost găsit în ePay pentru ${input.judetName}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4: Save metadata + submit order
|
||||
await updateStatus(extractId, "ordering");
|
||||
const nrCF = input.nrCF ?? input.nrCadastral;
|
||||
const orderId = await client.submitOrder({
|
||||
const saved = await client.saveMetadata(
|
||||
basketRowId,
|
||||
judetIndex: input.judetIndex,
|
||||
uatId: input.uatId,
|
||||
countyMatch.id,
|
||||
countyMatch.value,
|
||||
uatMatch.id,
|
||||
uatMatch.value,
|
||||
nrCF,
|
||||
nrCadastral: input.nrCadastral,
|
||||
solicitantId:
|
||||
input.nrCadastral,
|
||||
process.env.ANCPI_DEFAULT_SOLICITANT_ID || "14452",
|
||||
);
|
||||
if (!saved) {
|
||||
await updateStatus(extractId, "failed", {
|
||||
errorMessage: "Salvarea metadatelor în ePay a eșuat.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const orderId = await client.submitOrder();
|
||||
|
||||
await updateStatus(extractId, "polling", { orderId });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user