const { runTencentGeneralInvoiceOcr } = require("./tencent"); const { runMockOcr } = require("./mock"); const OCR_PDF_MAX_BYTES = 7 * 1024 * 1024; const REQUIRED_OCR_FIELDS_BY_TYPE = { vat: ["invoiceCode", "invoiceNumber", "invoiceDate", "totalAmount", "sellerName"], }; const REQUIRED_OCR_FIELDS_BY_DOCUMENT_CATEGORY = { vat_invoice: ["invoiceCode", "invoiceNumber", "invoiceDate", "totalAmount", "sellerName"], full_vat_invoice: ["invoiceNumber", "invoiceDate", "totalAmount", "sellerName"], electronic_train_ticket: ["invoiceNumber", "invoiceDate", "totalAmount", "buyerName"], }; const NUMBER_ONLY_DEDUP_DOCUMENT_CATEGORIES = new Set([ "full_vat_invoice", "electronic_train_ticket", ]); const SAFE_DOCUMENT_CATEGORIES = new Set([ "vat_invoice", "full_vat_invoice", "electronic_train_ticket", ]); const SAFE_QUALITY_ERRORS = new Set([ "invoice_not_detected", "multiple_invoices_not_supported", "invoice_recognition_failed", "unsupported_invoice_subtype", ]); const SAFE_NORMALIZED_FIELDS = [ "invoiceCode", "invoiceNumber", "invoiceDate", "amount", "taxAmount", "totalAmount", "sellerName", "buyerName", ]; function isOcrEnabledForType(config, ocrType) { if (config.ocrProvider === "mock") { return ocrType === "vat"; } return config.ocrProvider === "tencent" && ocrType === "vat"; } function getMissingRequiredOcrFields(ocrType, normalized) { const requiredFields = REQUIRED_OCR_FIELDS_BY_DOCUMENT_CATEGORY[normalized?.documentCategory] ?? REQUIRED_OCR_FIELDS_BY_TYPE[ocrType] ?? []; if (!requiredFields.length) { return []; } return requiredFields.filter((field) => { const value = normalized?.[field]; return value == null || value === ""; }); } function buildMissingRequiredOcrError(ocrType, normalized) { if (typeof normalized?.qualityError === "string" && normalized.qualityError) { return normalized.qualityError; } const missingFields = getMissingRequiredOcrFields(ocrType, normalized); if (!missingFields.length) { return null; } return `missing_required_fields:${missingFields.join(",")}`; } async function runOcrForInvoice({ config, ocrType, imageBase64, sourceMimeType }) { if (config.ocrProvider === "mock") { return runMockOcr({ ocrType, imageBase64 }); } if (config.ocrProvider !== "tencent") { throw new Error(`Unsupported OCR provider: ${config.ocrProvider}`); } if (ocrType !== "vat") { throw new Error(`Unsupported OCR type: ${ocrType}`); } return runTencentGeneralInvoiceOcr(config, { imageBase64, sourceMimeType }); } function shouldDeduplicateByInvoiceNumberOnly(normalized) { return Boolean( normalized?.invoiceNumber && !normalized?.invoiceCode && NUMBER_ONLY_DEDUP_DOCUMENT_CATEGORIES.has(normalized?.documentCategory), ); } function normalizeSafeString(value, maxLength) { if (typeof value !== "string") { return null; } const normalized = value.trim(); return normalized ? normalized.slice(0, maxLength) : null; } function sanitizeOcrResultForPersistence(ocrProvider, normalized) { if (ocrProvider !== "tencent") { return normalized; } const safe = { documentSubtype: normalizeSafeString(normalized?.documentSubtype, 64), documentCategory: SAFE_DOCUMENT_CATEGORIES.has(normalized?.documentCategory) ? normalized.documentCategory : null, documentCount: Number.isInteger(normalized?.documentCount) && normalized.documentCount >= 0 ? Math.min(normalized.documentCount, 100) : 1, }; for (const field of SAFE_NORMALIZED_FIELDS) { safe[field] = normalizeSafeString(normalized?.[field], field.endsWith("Name") ? 256 : 64); } if (SAFE_QUALITY_ERRORS.has(normalized?.qualityError)) { safe.qualityError = normalized.qualityError; } if (normalized?.transportMode === "pdf_native") { safe.transportMode = "pdf_native"; } return safe; } module.exports = { OCR_PDF_MAX_BYTES, buildMissingRequiredOcrError, getMissingRequiredOcrFields, isOcrEnabledForType, runOcrForInvoice, sanitizeOcrResultForPersistence, shouldDeduplicateByInvoiceNumberOnly, };