From 665a51d794424b7a0b8cf2adba4127e7216b0fe3 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Mon, 23 Mar 2026 01:37:45 +0200 Subject: [PATCH] feat(ancpi): extract Angular AJAX endpoints from ShowCartItems page --- src/app/api/ancpi/test/route.ts | 68 ++++++++++++++----- .../parcel-sync/services/epay-client.ts | 11 +++ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/app/api/ancpi/test/route.ts b/src/app/api/ancpi/test/route.ts index d813b74..52fc4f9 100644 --- a/src/app/api/ancpi/test/route.ts +++ b/src/app/api/ancpi/test/route.ts @@ -77,36 +77,68 @@ export async function GET(req: Request) { } // ── Step: search ── + // SearchEstate.action returns HTML (Angular app), not JSON. + // Need to find the real AJAX endpoints from the Angular JS code. if (step === "search") { const client = await EpayClient.create(username, password); - const countyIdx = resolveEpayCountyIndex("Cluj")!; - const results: Record = {}; + // Add to cart first + const basketRowId = await client.addToCart(14200); - // First add to cart — SearchEstate might only work with active cart - let basketRowId: number | null = null; - try { - basketRowId = await client.addToCart(14200); - results["_basketRowId"] = basketRowId; - } catch (e) { - results["_cartError"] = (e as Error).message; + // Load ShowCartItems.action — the Angular form page + const cartPageHtml = await client.getRawHtml( + `${process.env.ANCPI_BASE_URL || "https://epay.ancpi.ro/epay"}/ShowCartItems.action`, + ); + + // Extract Angular AJAX endpoints and form structure + // Look for: SearchEstate, EpayJsonInterceptor, ng-controller, $http + const patterns = [ + /SearchEstate[^"'\s]*/g, + /EpayJsonInterceptor[^"'\s]*/g, + /EditCartItem[^"'\s]*/g, + /\$http\.(post|get)\s*\(\s*['"]([^'"]+)['"]/g, + /action\s*[:=]\s*['"]([^'"]*)['"]/g, + /url\s*[:=]\s*['"]([^'"]*?\.action[^'"]*)['"]/g, + /ng-controller\s*=\s*["']([^"']+)["']/g, + /searchEstate|SearchEstate|cautaImobil/gi, + ]; + + const found: Record = {}; + for (const pat of patterns) { + const matches = cartPageHtml.match(pat); + if (matches && matches.length > 0) { + found[pat.source.slice(0, 40)] = [...new Set(matches)].slice(0, 10); + } } - // Test SearchEstate: without uatId (optional now) - results["345295_noUat"] = await client.searchEstate("345295", countyIdx).catch((e: Error) => ({ error: e.message })); + // Also extract any inline JSON data (Angular scope initialization) + const jsonMatches = cartPageHtml.match(/ng-init\s*=\s*["']([^"']{10,500})["']/g); - // Test with uatId=24 (spec says Cluj-Napoca) - results["345295_uat24"] = await client.searchEstate("345295", countyIdx, 24).catch((e: Error) => ({ error: e.message })); + // Find county/UAT dropdown references + const selectMatches = cartPageHtml.match(/]*(?:judet|county|uat)[^>]*>/gi); - // Other parcels without uatId - results["63565_noUat"] = await client.searchEstate("63565", countyIdx).catch((e: Error) => ({ error: e.message })); - results["88089_noUat"] = await client.searchEstate("88089", countyIdx).catch((e: Error) => ({ error: e.message })); + // Look for the specific search function + const searchFuncMatch = cartPageHtml.match( + /function\s+\w*[Ss]earch\w*\s*\([^)]*\)\s*\{[^}]{0,500}/g, + ); return NextResponse.json({ step: "search", - countyIdx, basketRowId, - results, + pageLength: cartPageHtml.length, + angularEndpoints: found, + ngInit: jsonMatches?.slice(0, 5) ?? [], + selectDropdowns: selectMatches?.slice(0, 5) ?? [], + searchFunctions: searchFuncMatch?.slice(0, 3) ?? [], + // Also check: does the page contain "SearchEstate" anywhere? + hasSearchEstate: cartPageHtml.includes("SearchEstate"), + hasJsonInterceptor: cartPageHtml.includes("EpayJsonInterceptor"), + // Sample of any .action URLs in the page + actionUrls: [...new Set( + (cartPageHtml.match(/['"][^'"]*\.action[^'"]*['"]/g) ?? []) + .map((s: string) => s.replace(/['"]/g, "")) + .filter((s: string) => s.length < 100), + )].slice(0, 20), }); } diff --git a/src/modules/parcel-sync/services/epay-client.ts b/src/modules/parcel-sync/services/epay-client.ts index cb280ea..73bae3a 100644 --- a/src/modules/parcel-sync/services/epay-client.ts +++ b/src/modules/parcel-sync/services/epay-client.ts @@ -245,6 +245,17 @@ export class EpayClient { } } + /* ── Raw HTML fetch (for page scraping) ─────────────────── */ + + async getRawHtml(url: string): Promise { + const response = await this.client.get(url, { + timeout: DEFAULT_TIMEOUT_MS, + maxRedirects: 5, + validateStatus: () => true, + }); + return String(response.data ?? ""); + } + /* ── Credits ───────────────────────────────────────────────── */ async getCredits(): Promise {