Files
ArchiTools/src/modules/parcel-sync/services/epay-storage.ts
T
AI Assistant 3921852eb5 feat(parcel-sync): add ANCPI ePay CF extract ordering backend
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>
2026-03-23 00:09:52 +02:00

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);
}