feat(ancpi): batch ordering + download existing orders

Major rewrite:
- Queue now processes batches: addToCart×N → saveMetadata×N → ONE
  submitOrder → poll → download ALL documents → store in MinIO
- Removed unique constraint on orderId (shared across batch items)
- Added step=download to test endpoint: downloads PDFs from 5
  existing orders (9685480-9685484) and stores in MinIO
- step=order now uses enqueueBatch for 2 test parcels (61904, 309952)
  as ONE ePay order instead of separate orders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-23 03:20:36 +02:00
parent 08cd7164cb
commit 8488a53e3b
4 changed files with 502 additions and 204 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ model RegistryAudit {
model CfExtract { model CfExtract {
id String @id @default(uuid()) id String @id @default(uuid())
orderId String? @unique // ePay orderId orderId String? // ePay orderId (shared across batch items)
basketRowId Int? // ePay cart item ID basketRowId Int? // ePay cart item ID
nrCadastral String // cadastral number nrCadastral String // cadastral number
nrCF String? // CF number if different nrCF String? // CF number if different
+2 -2
View File
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { getEpayCredentials } from "@/modules/parcel-sync/services/epay-session-store"; import { getEpayCredentials } from "@/modules/parcel-sync/services/epay-session-store";
import { import {
enqueueOrder, enqueueOrder,
enqueueBulk, enqueueBatch,
} from "@/modules/parcel-sync/services/epay-queue"; } from "@/modules/parcel-sync/services/epay-queue";
import type { CfExtractCreateInput } from "@/modules/parcel-sync/services/epay-types"; import type { CfExtractCreateInput } from "@/modules/parcel-sync/services/epay-types";
@@ -62,7 +62,7 @@ export async function POST(req: Request) {
}); });
} }
const ids = await enqueueBulk(parcels); const ids = await enqueueBatch(parcels);
const orders = ids.map((id, i) => ({ const orders = ids.map((id, i) => ({
id, id,
nrCadastral: parcels[i]?.nrCadastral ?? "", nrCadastral: parcels[i]?.nrCadastral ?? "",
+244 -30
View File
@@ -4,13 +4,15 @@ import {
createEpaySession, createEpaySession,
getEpayCredentials, getEpayCredentials,
} from "@/modules/parcel-sync/services/epay-session-store"; } from "@/modules/parcel-sync/services/epay-session-store";
import { enqueueOrder } from "@/modules/parcel-sync/services/epay-queue"; import { enqueueBatch } from "@/modules/parcel-sync/services/epay-queue";
import { storeCfExtract } from "@/modules/parcel-sync/services/epay-storage";
import { prisma } from "@/core/storage/prisma";
export const runtime = "nodejs"; export const runtime = "nodejs";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
/** /**
* GET /api/ancpi/test?step=login|uats|order * GET /api/ancpi/test?step=login|uats|order|download
* *
* ePay internal county IDs = eTerra WORKSPACE_IDs. * ePay internal county IDs = eTerra WORKSPACE_IDs.
* ePay UAT IDs = SIRUTA codes. * ePay UAT IDs = SIRUTA codes.
@@ -67,8 +69,232 @@ export async function GET(req: Request) {
}); });
} }
// ── order ── (USES 3 CREDITS!) // ── download ── Download PDFs from 5 existing orders
// Uses WORKSPACE_ID as county ID, SIRUTA as UAT ID — zero discovery if (step === "download") {
const client = await EpayClient.create(username, password);
createEpaySession(username, password, await client.getCredits());
// Known orders from previous test
const orderIds = ["9685480", "9685481", "9685482", "9685483", "9685484"];
// Mapping: orderId → nrCadastral (5 orders for 3 parcels)
// Orders were for: 345295 (Cluj-Napoca), 63565 (Feleacu), 88089 (Florești)
// 5 orders for 3 parcels = some duplicates
const orderParcelMap: Record<
string,
{ nrCadastral: string; judetName: string; uatName: string }
> = {
"9685480": {
nrCadastral: "345295",
judetName: "CLUJ",
uatName: "Cluj-Napoca",
},
"9685481": {
nrCadastral: "63565",
judetName: "CLUJ",
uatName: "Feleacu",
},
"9685482": {
nrCadastral: "88089",
judetName: "CLUJ",
uatName: "Florești",
},
"9685483": {
nrCadastral: "345295",
judetName: "CLUJ",
uatName: "Cluj-Napoca",
},
"9685484": {
nrCadastral: "63565",
judetName: "CLUJ",
uatName: "Feleacu",
},
};
const results: Array<{
orderId: string;
nrCadastral: string;
status: string;
documents: number;
downloaded: boolean;
minioPath?: string;
error?: string;
}> = [];
for (const orderId of orderIds) {
const parcelInfo = orderParcelMap[orderId];
if (!parcelInfo) {
results.push({
orderId,
nrCadastral: "unknown",
status: "error",
documents: 0,
downloaded: false,
error: "No parcel mapping for orderId",
});
continue;
}
try {
// Get order status and document info
const orderStatus = await client.getOrderStatus(orderId);
console.log(
`[ancpi-test] Order ${orderId}: status=${orderStatus.status}, docs=${orderStatus.documents.length}`,
);
if (orderStatus.documents.length === 0) {
results.push({
orderId,
nrCadastral: parcelInfo.nrCadastral,
status: orderStatus.status,
documents: 0,
downloaded: false,
error: "No documents found in order",
});
continue;
}
// Find downloadable PDF document
const doc = orderStatus.documents.find(
(d) => d.downloadValabil && d.contentType === "application/pdf",
);
if (!doc) {
results.push({
orderId,
nrCadastral: parcelInfo.nrCadastral,
status: orderStatus.status,
documents: orderStatus.documents.length,
downloaded: false,
error: "No downloadable PDF found",
});
continue;
}
// Download the PDF
const pdfBuffer = await client.downloadDocument(doc.idDocument, 4);
console.log(
`[ancpi-test] Downloaded doc ${doc.idDocument}: ${pdfBuffer.length} bytes`,
);
// Store in MinIO
const { path, index } = await storeCfExtract(
pdfBuffer,
parcelInfo.nrCadastral,
{
"ancpi-order-id": orderId,
"nr-cadastral": parcelInfo.nrCadastral,
judet: parcelInfo.judetName,
uat: parcelInfo.uatName,
"data-document": doc.dataDocument ?? "",
stare: orderStatus.status,
produs: "EXI_ONLINE",
},
);
// Upsert CfExtract record — find by orderId or create
const documentDate = doc.dataDocument
? new Date(doc.dataDocument)
: new Date();
const expiresAt = new Date(documentDate);
expiresAt.setDate(expiresAt.getDate() + 30);
// Try to find existing record by orderId
const existing = await prisma.cfExtract.findFirst({
where: { orderId },
});
if (existing) {
// Update existing record
await prisma.cfExtract.update({
where: { id: existing.id },
data: {
status: "completed",
epayStatus: orderStatus.status,
idDocument: doc.idDocument,
documentName: doc.nume,
documentDate,
minioPath: path,
minioIndex: index,
completedAt: new Date(),
expiresAt,
errorMessage: null,
},
});
} else {
// Create new record
const maxVersion = await prisma.cfExtract.aggregate({
where: { nrCadastral: parcelInfo.nrCadastral },
_max: { version: true },
});
await prisma.cfExtract.create({
data: {
orderId,
nrCadastral: parcelInfo.nrCadastral,
nrCF: parcelInfo.nrCadastral,
judetIndex: 127,
judetName: parcelInfo.judetName,
uatId:
parcelInfo.uatName === "Cluj-Napoca"
? 54975
: parcelInfo.uatName === "Feleacu"
? 57582
: 57706,
uatName: parcelInfo.uatName,
status: "completed",
epayStatus: orderStatus.status,
idDocument: doc.idDocument,
documentName: doc.nume,
documentDate,
minioPath: path,
minioIndex: index,
completedAt: new Date(),
expiresAt,
version: (maxVersion._max.version ?? 0) + 1,
},
});
}
results.push({
orderId,
nrCadastral: parcelInfo.nrCadastral,
status: orderStatus.status,
documents: orderStatus.documents.length,
downloaded: true,
minioPath: path,
});
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
console.error(
`[ancpi-test] Failed to process order ${orderId}:`,
message,
);
results.push({
orderId,
nrCadastral: parcelInfo.nrCadastral,
status: "error",
documents: 0,
downloaded: false,
error: message,
});
}
}
return NextResponse.json({
step: "download",
totalOrders: orderIds.length,
results,
summary: {
downloaded: results.filter((r) => r.downloaded).length,
failed: results.filter((r) => !r.downloaded).length,
},
});
}
// ── order ── Batch order test (USES 2 CREDITS!)
// Uses enqueueBatch to create ONE ePay order for all parcels
if (step === "order") { if (step === "order") {
if (!getEpayCredentials()) { if (!getEpayCredentials()) {
createEpaySession(username, password, 0); createEpaySession(username, password, 0);
@@ -78,24 +304,9 @@ export async function GET(req: Request) {
const credits = await client.getCredits(); const credits = await client.getCredits();
createEpaySession(username, password, credits); createEpaySession(username, password, credits);
if (credits < 3) {
return NextResponse.json({
error: `Doar ${credits} credite, trebuie 3.`,
});
}
// workspacePk=127 (CLUJ), SIRUTA codes as UAT IDs
const parcels = [ const parcels = [
{ {
nrCadastral: "345295", nrCadastral: "61904",
siruta: "54975", // SIRUTA = ePay UAT ID
judetIndex: 127, // workspacePk = ePay county ID
judetName: "CLUJ",
uatId: 54975,
uatName: "Cluj-Napoca",
},
{
nrCadastral: "63565",
siruta: "57582", siruta: "57582",
judetIndex: 127, judetIndex: 127,
judetName: "CLUJ", judetName: "CLUJ",
@@ -103,26 +314,29 @@ export async function GET(req: Request) {
uatName: "Feleacu", uatName: "Feleacu",
}, },
{ {
nrCadastral: "88089", nrCadastral: "309952",
siruta: "57706", siruta: "54975",
judetIndex: 127, judetIndex: 127,
judetName: "CLUJ", judetName: "CLUJ",
uatId: 57706, uatId: 54975,
uatName: "Florești", uatName: "Cluj-Napoca",
}, },
]; ];
const ids: string[] = []; if (credits < parcels.length) {
for (const p of parcels) { return NextResponse.json({
const id = await enqueueOrder(p); error: `Doar ${credits} credite, trebuie ${parcels.length}.`,
ids.push(id); });
} }
// Use enqueueBatch — ONE order for all parcels
const ids = await enqueueBatch(parcels);
return NextResponse.json({ return NextResponse.json({
step: "order", step: "order",
credits, credits,
message: `Enqueued ${ids.length} orders (4 requests each, zero discovery).`, message: `Enqueued batch of ${ids.length} parcels as ONE order.`,
orderIds: ids, extractIds: ids,
parcels: parcels.map((p, i) => ({ parcels: parcels.map((p, i) => ({
nrCadastral: p.nrCadastral, nrCadastral: p.nrCadastral,
uatName: p.uatName, uatName: p.uatName,
+165 -81
View File
@@ -1,25 +1,22 @@
/** /**
* ePay sequential order queue. * ePay batch order queue.
* *
* ePay has a GLOBAL cart per account — only one order can be processed * ePay has a GLOBAL cart per account — only one order can be processed
* at a time. This queue ensures sequential execution. * at a time. This queue ensures sequential execution.
* *
* Flow per item: * BATCH flow (correct — ONE order for N parcels):
* 1. Check credits * 1. Check credits (need >= N)
* 2. Add to cart (prodId=14200) * 2. For each parcel: addToCart → saveMetadata
* 3. Search estate on ePay * 3. ONE EditCartSubmit → ONE orderId
* 4. Submit order (EditCartSubmit) * 4. Poll that orderId until "Finalizata"
* 5. Poll status (15s × 40 = max 10 min) * 5. Download ALL documents from that order
* 6. Download PDF * 6. Store each in MinIO, update DB records
* 7. Store in MinIO
* 8. Update DB record
*/ */
import { prisma } from "@/core/storage/prisma"; import { prisma } from "@/core/storage/prisma";
import { EpayClient } from "./epay-client"; import { EpayClient } from "./epay-client";
import { getEpayCredentials, updateEpayCredits } from "./epay-session-store"; import { getEpayCredentials, updateEpayCredits } from "./epay-session-store";
import { storeCfExtract } from "./epay-storage"; import { storeCfExtract } from "./epay-storage";
import { resolveEpayCountyIndex } from "./epay-counties";
import type { CfExtractCreateInput } from "./epay-types"; import type { CfExtractCreateInput } from "./epay-types";
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -29,6 +26,11 @@ import type { CfExtractCreateInput } from "./epay-types";
type QueueItem = { type QueueItem = {
extractId: string; // CfExtract.id in DB extractId: string; // CfExtract.id in DB
input: CfExtractCreateInput; input: CfExtractCreateInput;
basketRowId?: number; // set during cart phase
};
type BatchJob = {
items: QueueItem[];
}; };
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -36,22 +38,41 @@ type QueueItem = {
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
const g = globalThis as { const g = globalThis as {
__epayQueue?: QueueItem[]; __epayBatchQueue?: BatchJob[];
__epayQueueProcessing?: boolean; __epayQueueProcessing?: boolean;
}; };
if (!g.__epayQueue) g.__epayQueue = []; if (!g.__epayBatchQueue) g.__epayBatchQueue = [];
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Public API */ /* Public API */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** /**
* Enqueue a CF extract order. Creates a DB record and adds to the * Enqueue a single CF extract order (backwards compatible).
* processing queue. Returns the CfExtract.id immediately. * Creates a DB record, wraps as a batch of 1, and adds to the queue.
*/ */
export async function enqueueOrder( export async function enqueueOrder(
input: CfExtractCreateInput, input: CfExtractCreateInput,
): Promise<string> { ): Promise<string> {
const ids = await enqueueBatch([input]);
const first = ids[0];
if (!first) throw new Error("enqueueBatch returned empty array");
return first;
}
/**
* Enqueue a batch of CF extract orders.
* Creates all DB records, then processes as ONE ePay order.
* Returns the CfExtract IDs immediately.
*/
export async function enqueueBatch(
inputs: CfExtractCreateInput[],
): Promise<string[]> {
if (inputs.length === 0) return [];
const items: QueueItem[] = [];
for (const input of inputs) {
// Create DB record in "queued" status // Create DB record in "queued" status
const record = await prisma.cfExtract.create({ const record = await prisma.cfExtract.create({
data: { data: {
@@ -65,7 +86,6 @@ export async function enqueueOrder(
gisFeatureId: input.gisFeatureId, gisFeatureId: input.gisFeatureId,
prodId: input.prodId ?? 14200, prodId: input.prodId ?? 14200,
status: "queued", status: "queued",
// Version: find max version for this cadastral number and increment
version: version:
(( ((
await prisma.cfExtract.aggregate({ await prisma.cfExtract.aggregate({
@@ -76,30 +96,19 @@ export async function enqueueOrder(
}, },
}); });
g.__epayQueue!.push({ extractId: record.id, input }); items.push({ extractId: record.id, input });
}
g.__epayBatchQueue!.push({ items });
console.log( console.log(
`[epay-queue] Enqueued: ${input.nrCadastral} (id=${record.id}, queue=${g.__epayQueue!.length})`, `[epay-queue] Enqueued batch of ${items.length}: ${items.map((i) => i.input.nrCadastral).join(", ")} (queue=${g.__epayBatchQueue!.length})`,
); );
// Start processing if not already running // Start processing if not already running
void processQueue(); void processQueue();
return record.id; return items.map((i) => i.extractId);
}
/**
* Enqueue multiple orders at once (bulk).
*/
export async function enqueueBulk(
inputs: CfExtractCreateInput[],
): Promise<string[]> {
const ids: string[] = [];
for (const input of inputs) {
const id = await enqueueOrder(input);
ids.push(id);
}
return ids;
} }
/** /**
@@ -108,10 +117,9 @@ export async function enqueueBulk(
export function getQueueStatus(): { export function getQueueStatus(): {
length: number; length: number;
processing: boolean; processing: boolean;
currentItem?: string;
} { } {
return { return {
length: g.__epayQueue?.length ?? 0, length: g.__epayBatchQueue?.length ?? 0,
processing: g.__epayQueueProcessing ?? false, processing: g.__epayQueueProcessing ?? false,
}; };
} }
@@ -124,13 +132,13 @@ async function processQueue(): Promise<void> {
if (g.__epayQueueProcessing) return; // already running if (g.__epayQueueProcessing) return; // already running
g.__epayQueueProcessing = true; g.__epayQueueProcessing = true;
// Track all orderIds from this batch to avoid duplicates // Track all orderIds from this session to avoid duplicates
const knownOrderIds = new Set<string>(); const knownOrderIds = new Set<string>();
try { try {
while (g.__epayQueue && g.__epayQueue.length > 0) { while (g.__epayBatchQueue && g.__epayBatchQueue.length > 0) {
const item = g.__epayQueue.shift()!; const batch = g.__epayBatchQueue.shift()!;
const orderId = await processItem(item, knownOrderIds); const orderId = await processBatch(batch.items, knownOrderIds);
if (orderId) knownOrderIds.add(orderId); if (orderId) knownOrderIds.add(orderId);
} }
} finally { } finally {
@@ -149,52 +157,62 @@ async function updateStatus(
}); });
} }
async function processItem( /**
item: QueueItem, * Process a batch of items as ONE ePay order:
* 1. Check credits (>= N)
* 2. addToCart + saveMetadata for each
* 3. ONE submitOrder
* 4. Poll until complete
* 5. Download + store ALL documents
*/
async function processBatch(
items: QueueItem[],
knownOrderIds: Set<string>, knownOrderIds: Set<string>,
): Promise<string | null> { ): Promise<string | null> {
const { extractId, input } = item; const extractIds = items.map((i) => i.extractId);
const count = items.length;
try { try {
// Get ePay credentials // Get ePay credentials
const creds = getEpayCredentials(); const creds = getEpayCredentials();
if (!creds) { if (!creds) {
await updateStatus(extractId, "failed", { for (const id of extractIds) {
await updateStatus(id, "failed", {
errorMessage: "Nu ești conectat la ePay.", errorMessage: "Nu ești conectat la ePay.",
}); });
}
return null; return null;
} }
const client = await EpayClient.create(creds.username, creds.password); const client = await EpayClient.create(creds.username, creds.password);
// Step 1: Check credits // Step 1: Check credits (need >= count)
const credits = await client.getCredits(); const credits = await client.getCredits();
updateEpayCredits(credits); updateEpayCredits(credits);
if (credits < 1) { if (credits < count) {
await updateStatus(extractId, "failed", { for (const id of extractIds) {
errorMessage: `Credite insuficiente: ${credits}. Reîncărcați contul pe epay.ancpi.ro.`, await updateStatus(id, "failed", {
errorMessage: `Credite insuficiente: ${credits} disponibile, ${count} necesare.`,
}); });
}
return null; return null;
} }
// Step 2: Add to cart // Step 2: addToCart + saveMetadata for EACH item
for (const item of items) {
const { extractId, input } = item;
await updateStatus(extractId, "cart"); await updateStatus(extractId, "cart");
const basketRowId = await client.addToCart(input.prodId ?? 14200); const basketRowId = await client.addToCart(input.prodId ?? 14200);
item.basketRowId = basketRowId;
await updateStatus(extractId, "cart", { basketRowId }); await updateStatus(extractId, "cart", { basketRowId });
// Step 3: Save metadata + submit order // Resolve county/UAT from SIRUTA if available
// ePay internal county IDs = eTerra WORKSPACE_IDs let countyInternalId = input.judetIndex;
// ePay UAT IDs = SIRUTA codes let uatInternalId = input.uatId;
// So we can skip all discovery calls!
await updateStatus(extractId, "ordering");
// Resolve county ID (workspacePk) and UAT ID (siruta) from our DB
let countyInternalId = input.judetIndex; // might already be workspacePk
let uatInternalId = input.uatId; // might already be SIRUTA
let countyName = input.judetName; let countyName = input.judetName;
let uatName = input.uatName; let uatName = input.uatName;
// If siruta is provided, look up workspacePk from GisUat
if (input.siruta) { if (input.siruta) {
const uat = await prisma.gisUat.findUnique({ const uat = await prisma.gisUat.findUnique({
where: { siruta: input.siruta }, where: { siruta: input.siruta },
@@ -209,6 +227,8 @@ async function processItem(
} }
const nrCF = input.nrCF ?? input.nrCadastral; const nrCF = input.nrCF ?? input.nrCadastral;
await updateStatus(extractId, "ordering");
const saved = await client.saveMetadata( const saved = await client.saveMetadata(
basketRowId, basketRowId,
countyInternalId, countyInternalId,
@@ -219,25 +239,44 @@ async function processItem(
input.nrCadastral, input.nrCadastral,
process.env.ANCPI_DEFAULT_SOLICITANT_ID || "14452", process.env.ANCPI_DEFAULT_SOLICITANT_ID || "14452",
); );
if (!saved) { if (!saved) {
await updateStatus(extractId, "failed", { await updateStatus(extractId, "failed", {
errorMessage: "Salvarea metadatelor în ePay a eșuat.", errorMessage: "Salvarea metadatelor în ePay a eșuat.",
}); });
// Continue with remaining items — the cart still has them
// but this one won't get metadata. Remove from batch.
item.basketRowId = undefined;
}
}
// Filter to only items that had successful metadata saves
const validItems = items.filter((i) => i.basketRowId !== undefined);
if (validItems.length === 0) {
return null; return null;
} }
// Step 3: ONE submitOrder for ALL items
console.log(
`[epay-queue] Submitting order for ${validItems.length} items...`,
);
const orderId = await client.submitOrder(knownOrderIds); const orderId = await client.submitOrder(knownOrderIds);
await updateStatus(extractId, "polling", { orderId }); // Update all valid items with the shared orderId
for (const item of validItems) {
await updateStatus(item.extractId, "polling", { orderId });
}
// Step 5: Poll until complete // Step 4: Poll until complete
const finalStatus = await client.pollUntilComplete( const finalStatus = await client.pollUntilComplete(
orderId, orderId,
async (attempt, status) => { async (attempt, status) => {
await updateStatus(extractId, "polling", { for (const item of validItems) {
await updateStatus(item.extractId, "polling", {
epayStatus: status, epayStatus: status,
pollAttempts: attempt, pollAttempts: attempt,
}); });
}
}, },
); );
@@ -245,26 +284,57 @@ async function processItem(
finalStatus.status === "Anulata" || finalStatus.status === "Anulata" ||
finalStatus.status === "Plata refuzata" finalStatus.status === "Plata refuzata"
) { ) {
await updateStatus(extractId, "cancelled", { for (const item of validItems) {
await updateStatus(item.extractId, "cancelled", {
epayStatus: finalStatus.status, epayStatus: finalStatus.status,
errorMessage: `Comanda ${finalStatus.status.toLowerCase()}.`, errorMessage: `Comanda ${finalStatus.status.toLowerCase()}.`,
}); });
}
return null; return null;
} }
// Step 6: Download PDF // Step 5: Download ALL documents and match to items
const doc = finalStatus.documents.find( // The order may contain multiple documents — match by filename/nrCadastral
const downloadableDocs = finalStatus.documents.filter(
(d) => d.downloadValabil && d.contentType === "application/pdf", (d) => d.downloadValabil && d.contentType === "application/pdf",
); );
if (!doc) {
await updateStatus(extractId, "failed", { console.log(
`[epay-queue] Order ${orderId}: ${downloadableDocs.length} documents for ${validItems.length} items`,
);
// Try to match documents to items by cadastral number in filename
// ePay filenames look like "Extras_Informare_65297.pdf" (the number is an internal ID, not nrCadastral)
// If we have exactly as many docs as items, assign in order.
// Otherwise, download all and try best-effort matching.
if (downloadableDocs.length === 0) {
for (const item of validItems) {
await updateStatus(item.extractId, "failed", {
epayStatus: finalStatus.status, epayStatus: finalStatus.status,
errorMessage: "Nu s-a găsit documentul PDF în comanda finalizată.", errorMessage: "Nu s-au găsit documente PDF în comanda finalizată.",
}); });
return null; }
return orderId;
} }
await updateStatus(extractId, "downloading", { // Simple matching strategy:
// If docs.length === items.length, assign 1:1 in order
// Otherwise, assign first doc to first item, etc., and leftover items get "failed"
for (let i = 0; i < validItems.length; i++) {
const item = validItems[i]!;
const doc = downloadableDocs[i];
if (!doc) {
await updateStatus(item.extractId, "failed", {
epayStatus: finalStatus.status,
errorMessage: `Document lipsă (${downloadableDocs.length} documente pentru ${validItems.length} parcele).`,
});
continue;
}
try {
await updateStatus(item.extractId, "downloading", {
idDocument: doc.idDocument, idDocument: doc.idDocument,
documentName: doc.nume, documentName: doc.nume,
documentDate: doc.dataDocument ? new Date(doc.dataDocument) : null, documentDate: doc.dataDocument ? new Date(doc.dataDocument) : null,
@@ -272,29 +342,29 @@ async function processItem(
const pdfBuffer = await client.downloadDocument(doc.idDocument, 4); const pdfBuffer = await client.downloadDocument(doc.idDocument, 4);
// Step 7: Store in MinIO // Step 6: Store in MinIO
const { path, index } = await storeCfExtract( const { path, index } = await storeCfExtract(
pdfBuffer, pdfBuffer,
input.nrCadastral, item.input.nrCadastral,
{ {
"ancpi-order-id": orderId, "ancpi-order-id": orderId,
"nr-cadastral": input.nrCadastral, "nr-cadastral": item.input.nrCadastral,
judet: input.judetName, judet: item.input.judetName,
uat: input.uatName, uat: item.input.uatName,
"data-document": doc.dataDocument ?? "", "data-document": doc.dataDocument ?? "",
stare: finalStatus.status, stare: finalStatus.status,
produs: "EXI_ONLINE", produs: "EXI_ONLINE",
}, },
); );
// Step 8: Complete // Complete
const documentDate = doc.dataDocument const documentDate = doc.dataDocument
? new Date(doc.dataDocument) ? new Date(doc.dataDocument)
: new Date(); : new Date();
const expiresAt = new Date(documentDate); const expiresAt = new Date(documentDate);
expiresAt.setDate(expiresAt.getDate() + 30); expiresAt.setDate(expiresAt.getDate() + 30);
await updateStatus(extractId, "completed", { await updateStatus(item.extractId, "completed", {
minioPath: path, minioPath: path,
minioIndex: index, minioIndex: index,
epayStatus: finalStatus.status, epayStatus: finalStatus.status,
@@ -303,19 +373,33 @@ async function processItem(
expiresAt, expiresAt,
}); });
console.log(
`[epay-queue] Completed: ${item.input.nrCadastral}${path}`,
);
} catch (error) {
const message =
error instanceof Error ? error.message : "Eroare download/stocare";
await updateStatus(item.extractId, "failed", {
errorMessage: message,
});
}
}
// Update credits after successful order // Update credits after successful order
const newCredits = await client.getCredits(); const newCredits = await client.getCredits();
updateEpayCredits(newCredits); updateEpayCredits(newCredits);
console.log( console.log(
`[epay-queue] Completed: ${input.nrCadastral}${path} (credits: ${newCredits})`, `[epay-queue] Batch complete: orderId=${orderId}, credits remaining=${newCredits}`,
); );
return orderId; return orderId;
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Eroare necunoscută"; const message =
console.error(`[epay-queue] Failed: ${input.nrCadastral}:`, message); error instanceof Error ? error.message : "Eroare necunoscută";
await updateStatus(extractId, "failed", { errorMessage: message }); console.error(`[epay-queue] Batch failed:`, message);
for (const id of extractIds) {
await updateStatus(id, "failed", { errorMessage: message });
}
return null; return null;
} }
} }