3921852eb5
Foundation (Phase 1): - CfExtract Prisma model with version tracking, expiry, MinIO path - epay-types.ts: all ePay API response types - epay-counties.ts: WORKSPACE_ID → ePay county index mapping (42 counties) - epay-storage.ts: MinIO helpers (bucket, naming, upload, download) - docker-compose.yml: ANCPI env vars ePay Client (Phase 2): - epay-client.ts: full HTTP client (login, credits, cart, search estate, submit order, poll status, download PDF) with cookie jar + auto-relogin - epay-session-store.ts: separate session from eTerra Queue + API (Phase 3): - epay-queue.ts: sequential FIFO queue (global cart constraint), 10-step workflow per order with DB status updates at each step - POST /api/ancpi/session: connect/disconnect - POST /api/ancpi/order: create single or bulk orders - GET /api/ancpi/orders: list all extracts - GET /api/ancpi/credits: live credit balance - GET /api/ancpi/download: stream PDF from MinIO Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
151 lines
4.0 KiB
TypeScript
151 lines
4.0 KiB
TypeScript
/**
|
|
* MinIO storage helpers for ANCPI CF extracts.
|
|
*
|
|
* Bucket: ancpi-documente (separate from general "tools" bucket)
|
|
* Naming: {index:02d}_Extras CF_{nrCadastral} - {DD-MM-YYYY}.pdf
|
|
*/
|
|
|
|
import { minioClient } from "@/core/storage/minio-client";
|
|
import { Readable } from "stream";
|
|
|
|
const BUCKET = process.env.MINIO_BUCKET_ANCPI || "ancpi-documente";
|
|
|
|
let bucketChecked = false;
|
|
|
|
/** Ensure the ANCPI bucket exists (idempotent, cached) */
|
|
export async function ensureAncpiBucket(): Promise<void> {
|
|
if (bucketChecked) return;
|
|
try {
|
|
const exists = await minioClient.bucketExists(BUCKET);
|
|
if (!exists) {
|
|
await minioClient.makeBucket(BUCKET);
|
|
console.log(`[epay-storage] Bucket '${BUCKET}' created.`);
|
|
}
|
|
bucketChecked = true;
|
|
} catch (error) {
|
|
console.error("[epay-storage] Bucket check/create failed:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the next file index for a given cadastral number.
|
|
* Scans existing objects to find the highest index.
|
|
*/
|
|
export async function getNextFileIndex(
|
|
nrCadastral: string,
|
|
): Promise<number> {
|
|
await ensureAncpiBucket();
|
|
|
|
const pattern = new RegExp(
|
|
`^(\\d+)_Extras CF_${nrCadastral.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} -`,
|
|
);
|
|
|
|
let maxIndex = 0;
|
|
const stream = minioClient.listObjects(BUCKET, "", true);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
stream.on("data", (obj) => {
|
|
if (!obj.name) return;
|
|
const match = obj.name.match(pattern);
|
|
if (match) {
|
|
const idx = parseInt(match[1] ?? "0", 10);
|
|
if (idx > maxIndex) maxIndex = idx;
|
|
}
|
|
});
|
|
stream.on("end", () => resolve(maxIndex + 1));
|
|
stream.on("error", reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build the display filename for a CF extract.
|
|
* Format: 01_Extras CF_291479 - 22-03-2026.pdf
|
|
*/
|
|
export function buildFileName(
|
|
index: number,
|
|
nrCadastral: string,
|
|
date: Date,
|
|
): string {
|
|
const idx = String(index).padStart(2, "0");
|
|
const dd = String(date.getDate()).padStart(2, "0");
|
|
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
const yyyy = date.getFullYear();
|
|
return `${idx}_Extras CF_${nrCadastral} - ${dd}-${mm}-${yyyy}.pdf`;
|
|
}
|
|
|
|
/**
|
|
* Store a CF extract PDF in MinIO.
|
|
* Returns the MinIO path and file index.
|
|
*/
|
|
export async function storeCfExtract(
|
|
pdfBuffer: Buffer,
|
|
nrCadastral: string,
|
|
metadata: Record<string, string>,
|
|
): Promise<{ path: string; fileName: string; index: number }> {
|
|
await ensureAncpiBucket();
|
|
|
|
const index = await getNextFileIndex(nrCadastral);
|
|
const fileName = buildFileName(index, nrCadastral, new Date());
|
|
// Store in subfolder per cadastral number
|
|
const path = `parcele/${nrCadastral}/${fileName}`;
|
|
|
|
await minioClient.putObject(BUCKET, path, pdfBuffer, pdfBuffer.length, {
|
|
"Content-Type": "application/pdf",
|
|
...metadata,
|
|
});
|
|
|
|
console.log(
|
|
`[epay-storage] Stored: ${path} (${pdfBuffer.length} bytes)`,
|
|
);
|
|
|
|
return { path, fileName, index };
|
|
}
|
|
|
|
/**
|
|
* Get a readable stream for a stored CF extract.
|
|
*/
|
|
export async function getCfExtractStream(
|
|
minioPath: string,
|
|
): Promise<Readable> {
|
|
return minioClient.getObject(BUCKET, minioPath);
|
|
}
|
|
|
|
/**
|
|
* List all stored CF extracts for a cadastral number.
|
|
*/
|
|
export async function listExtractsForParcel(
|
|
nrCadastral: string,
|
|
): Promise<Array<{ name: string; lastModified: Date; size: number }>> {
|
|
await ensureAncpiBucket();
|
|
|
|
const prefix = `parcele/${nrCadastral}/`;
|
|
const results: Array<{ name: string; lastModified: Date; size: number }> =
|
|
[];
|
|
|
|
const stream = minioClient.listObjects(BUCKET, prefix, true);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
stream.on("data", (obj) => {
|
|
if (obj.name) {
|
|
results.push({
|
|
name: obj.name,
|
|
lastModified: obj.lastModified ?? new Date(),
|
|
size: obj.size ?? 0,
|
|
});
|
|
}
|
|
});
|
|
stream.on("end", () => resolve(results));
|
|
stream.on("error", reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate a presigned download URL (7 day expiry).
|
|
*/
|
|
export async function getPresignedUrl(
|
|
minioPath: string,
|
|
expirySeconds = 7 * 24 * 3600,
|
|
): Promise<string> {
|
|
return minioClient.presignedGetObject(BUCKET, minioPath, expirySeconds);
|
|
}
|