eb8cd18210
Root cause from ePay Angular analysis:
- EpayJsonInterceptor needs Content-Type: application/json + {"judet": N}
- EditCartItemJson needs JSON with bigDecimalValue/stringValue structure
- SearchEstate needs basketId in body for JSON response
- Queue skips SearchEstate (data already from eTerra), uses
configureCartItem → submitOrder flow directly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
674 lines
22 KiB
TypeScript
674 lines
22 KiB
TypeScript
/**
|
|
* 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
|
|
*
|
|
* Modeled after eterra-client.ts: cookie jar, session cache, retry, auto-relogin.
|
|
*/
|
|
|
|
import axios, { type AxiosError, type AxiosInstance } from "axios";
|
|
import crypto from "crypto";
|
|
import { wrapper } from "axios-cookiejar-support";
|
|
import { CookieJar } from "tough-cookie";
|
|
import type {
|
|
EpayCartResponse,
|
|
EpaySearchResult,
|
|
EpayUatEntry,
|
|
EpayOrderStatus,
|
|
EpaySolutionDoc,
|
|
OrderMetadata,
|
|
} from "./epay-types";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Constants */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
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 POLL_INTERVAL_MS = 15_000;
|
|
const POLL_MAX_ATTEMPTS = 40; // 10 minutes
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Session cache (global singleton) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
type SessionEntry = {
|
|
jar: CookieJar;
|
|
client: AxiosInstance;
|
|
createdAt: number;
|
|
lastUsed: number;
|
|
};
|
|
|
|
const globalStore = globalThis as {
|
|
__epaySessionCache?: Map<string, SessionEntry>;
|
|
};
|
|
const sessionCache =
|
|
globalStore.__epaySessionCache ?? new Map<string, SessionEntry>();
|
|
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 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export class EpayClient {
|
|
private client: AxiosInstance;
|
|
private jar: CookieJar;
|
|
private username: string;
|
|
private password: string;
|
|
private reloginAttempted = false;
|
|
|
|
private constructor(
|
|
client: AxiosInstance,
|
|
jar: CookieJar,
|
|
username: string,
|
|
password: string,
|
|
) {
|
|
this.client = client;
|
|
this.jar = jar;
|
|
this.username = username;
|
|
this.password = password;
|
|
}
|
|
|
|
/* ── Factory ───────────────────────────────────────────────── */
|
|
|
|
static async create(username: string, password: string): Promise<EpayClient> {
|
|
const cacheKey = makeCacheKey(username, password);
|
|
const cached = sessionCache.get(cacheKey);
|
|
const now = Date.now();
|
|
|
|
if (cached && now - cached.lastUsed < SESSION_TTL_MS) {
|
|
cached.lastUsed = now;
|
|
return new EpayClient(cached.client, cached.jar, username, password);
|
|
}
|
|
|
|
const jar = new CookieJar();
|
|
const client = wrapper(
|
|
axios.create({
|
|
jar,
|
|
withCredentials: true,
|
|
maxRedirects: 5,
|
|
headers: {
|
|
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",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const instance = new EpayClient(client, jar, username, password);
|
|
await instance.login();
|
|
sessionCache.set(cacheKey, { jar, client, createdAt: now, lastUsed: now });
|
|
return instance;
|
|
}
|
|
|
|
/* ── 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)
|
|
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")
|
|
) {
|
|
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, {
|
|
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)");
|
|
}
|
|
|
|
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;
|
|
});
|
|
}
|
|
|
|
/* ── Cart ───────────────────────────────────────────────────── */
|
|
|
|
async addToCart(prodId = 14200): Promise<number> {
|
|
return this.retryOnAuthFail(async () => {
|
|
const body = new URLSearchParams();
|
|
body.set("prodId", String(prodId));
|
|
body.set("productQtyModif", "1");
|
|
body.set("urgencyValue", "5000");
|
|
body.set("wishListId", "-1");
|
|
body.set("basketRowId", "");
|
|
body.set("addToWishList", "");
|
|
body.set("random", String(Date.now()));
|
|
body.set("secondaryProductsJson", "");
|
|
|
|
const response = await this.client.post(
|
|
`${BASE_URL}/AddToCartOrWishListFromPost.action`,
|
|
body.toString(),
|
|
{
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
},
|
|
);
|
|
|
|
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)}`,
|
|
);
|
|
}
|
|
|
|
console.log(`[epay] Added to cart: basketRowId=${item.id}`);
|
|
return item.id;
|
|
});
|
|
}
|
|
|
|
/* ── Estate Search ─────────────────────────────────────────── */
|
|
|
|
/**
|
|
* Search estate on ePay. Requires basketRowId in context.
|
|
* countyIdx = ePay county index (0-41), uatId = real ePay UAT ID (NOT index).
|
|
*/
|
|
async searchEstate(
|
|
identifier: string,
|
|
countyIdx: 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));
|
|
|
|
const response = await this.client.post(
|
|
`${BASE_URL}/SearchEstate.action`,
|
|
body.toString(),
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
},
|
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
},
|
|
);
|
|
|
|
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 */ }
|
|
}
|
|
}
|
|
|
|
console.warn(
|
|
`[epay] SearchEstate(${identifier}): unexpected response`,
|
|
String(typeof data === "string" ? data : JSON.stringify(data)).slice(0, 200),
|
|
);
|
|
return [];
|
|
});
|
|
}
|
|
|
|
/* ── UAT Lookup ────────────────────────────────────────────── */
|
|
|
|
/**
|
|
* Get UAT list for a county. Uses JSON Content-Type (not form-urlencoded).
|
|
*/
|
|
async getUatList(countyIdx: number): Promise<EpayUatEntry[]> {
|
|
return this.retryOnAuthFail(async () => {
|
|
// EpayJsonInterceptor requires JSON body, not form-urlencoded
|
|
const response = await this.client.post(
|
|
`${BASE_URL}/EpayJsonInterceptor.action`,
|
|
JSON.stringify({ judet: countyIdx }),
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
},
|
|
);
|
|
|
|
const data = response.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 (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}`,
|
|
);
|
|
});
|
|
}
|
|
|
|
/* ── 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)
|
|
const body = new URLSearchParams();
|
|
body.set("goToCheckout", "true");
|
|
|
|
const response = await this.client.post(
|
|
`${BASE_URL}/EditCartSubmit.action`,
|
|
body.toString(),
|
|
{
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
maxRedirects: 10,
|
|
validateStatus: () => true,
|
|
},
|
|
);
|
|
|
|
// 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+)/,
|
|
);
|
|
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("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;
|
|
while ((docMatch = docPattern.exec(html)) !== null) {
|
|
documents.push({
|
|
idDocument: parseInt(docMatch[1] ?? "0", 10),
|
|
idTipDocument: null,
|
|
nume: docMatch[2] ?? "",
|
|
numar: null,
|
|
serie: null,
|
|
dataDocument: docMatch[3] ?? "",
|
|
contentType: "application/pdf",
|
|
linkDownload: docMatch[4] ?? "",
|
|
downloadValabil: true,
|
|
valabilNelimitat: true,
|
|
zileValabilitateDownload: -1,
|
|
transactionId: 0,
|
|
});
|
|
}
|
|
|
|
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"
|
|
) {
|
|
return status;
|
|
}
|
|
|
|
await sleep(POLL_INTERVAL_MS);
|
|
}
|
|
|
|
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 () => {
|
|
const url = `${BASE_URL}/DownloadFile.action?typeD=${typeD}&id=${idDocument}&source=&browser=chrome`;
|
|
|
|
const response = await this.client.post(url, null, {
|
|
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)`,
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
`[epay] Downloaded document ${idDocument}: ${data.length} bytes`,
|
|
);
|
|
return Buffer.from(data);
|
|
});
|
|
}
|
|
}
|