const USED_COMPLETE_ARCHIVED_REASON = "used_complete"; const INVOICE_UNAVAILABLE_REASONS = { duplicateAvailable: "duplicate_available_in_company", duplicatePending: "duplicate_pending_in_company", duplicateFailed: "duplicate_failed_in_company", duplicateOtherOwner: "duplicate_other_owner_in_company", usedByCompany: "used_by_company", }; function buildInvoiceDedupFilters({ fileSha256 = null, invoiceCode = null, invoiceNumber = null }) { const filters = []; if (fileSha256) { filters.push({ file_sha256: fileSha256 }); } if (invoiceCode && invoiceNumber) { filters.push({ invoice_code: invoiceCode, invoice_number: invoiceNumber, }); } return filters; } function isUsedInvoice(invoice) { return Boolean( invoice?.status === "bound" || invoice?.linked_reimbursement_id || invoice?.travel_order_id || (invoice?.status === "archived" && invoice?.archived_reason === USED_COMPLETE_ARCHIVED_REASON), ); } function classifyInvoiceConflict(conflict, ownerEmployeeId) { if (!conflict) { return null; } if (isUsedInvoice(conflict)) { return INVOICE_UNAVAILABLE_REASONS.usedByCompany; } if (conflict.owner_employee_id && conflict.owner_employee_id !== ownerEmployeeId) { return INVOICE_UNAVAILABLE_REASONS.duplicateOtherOwner; } if (conflict.status === "pending_ocr") { return INVOICE_UNAVAILABLE_REASONS.duplicatePending; } if (conflict.status === "ocr_failed") { return INVOICE_UNAVAILABLE_REASONS.duplicateFailed; } return INVOICE_UNAVAILABLE_REASONS.duplicateAvailable; } function rankInvoiceConflict(reason) { switch (reason) { case INVOICE_UNAVAILABLE_REASONS.usedByCompany: return 10; case INVOICE_UNAVAILABLE_REASONS.duplicateOtherOwner: return 20; case INVOICE_UNAVAILABLE_REASONS.duplicateAvailable: return 30; case INVOICE_UNAVAILABLE_REASONS.duplicatePending: return 40; case INVOICE_UNAVAILABLE_REASONS.duplicateFailed: return 50; default: return 100; } } function pickBlockingInvoiceConflict(conflicts, ownerEmployeeId) { const ranked = conflicts .map((conflict) => ({ conflict, reason: classifyInvoiceConflict(conflict, ownerEmployeeId), })) .filter((item) => item.reason) .sort((a, b) => rankInvoiceConflict(a.reason) - rankInvoiceConflict(b.reason)); return ranked[0] ?? null; } async function findInvoiceDedupConflicts(tx, input) { const filters = buildInvoiceDedupFilters(input); if (!filters.length) { return []; } return tx.invoicePool.findMany({ where: { ...(input.excludeInvoiceId ? { id: { not: input.excludeInvoiceId } } : {}), is_duplicate_override: false, OR: filters, status: { in: ["pending_ocr", "ocr_failed", "available", "bound", "archived"] }, }, orderBy: [{ created_at: "asc" }, { id: "asc" }], select: { id: true, owner_employee_id: true, uploaded_by_employee_id: true, status: true, archived_reason: true, linked_reimbursement_id: true, linked_item_id: true, travel_order_id: true, invoice_code: true, invoice_number: true, file_sha256: true, linked_reimbursement: { select: { form_number: true, status: true, owner_id: true, submitter_id: true, }, }, }, }); } async function resolveInvoiceDedupBlocker(tx, input) { const conflicts = await findInvoiceDedupConflicts(tx, input); return pickBlockingInvoiceConflict(conflicts, input.ownerEmployeeId); } async function resolveUploadDedupDecision(tx, { fileSha256, ownerEmployeeId }) { const conflicts = await findInvoiceDedupConflicts(tx, { fileSha256, ownerEmployeeId }); const blocker = pickBlockingInvoiceConflict(conflicts, ownerEmployeeId); if (!blocker) { return { action: "create" }; } const conflict = blocker.conflict; if (!isUsedInvoice(conflict) && conflict.owner_employee_id === ownerEmployeeId) { return { action: "reuse_existing", reason: blocker.reason, invoice: conflict, }; } return { action: "blocked", reason: blocker.reason, invoice: conflict, }; } function mapInvoiceUnavailableReason(reason) { switch (reason) { case INVOICE_UNAVAILABLE_REASONS.usedByCompany: return "该发票已用于报销,不能重复添加"; case INVOICE_UNAVAILABLE_REASONS.duplicateOtherOwner: return "公司内已有相同发票,需管理员处理"; case INVOICE_UNAVAILABLE_REASONS.duplicatePending: return "相同发票正在识别,请稍后刷新"; case INVOICE_UNAVAILABLE_REASONS.duplicateFailed: return "相同发票已存在但识别失败,请先处理已有记录"; case INVOICE_UNAVAILABLE_REASONS.duplicateAvailable: return "公司内已有相同可用发票,不能重复添加"; default: return null; } } module.exports = { INVOICE_UNAVAILABLE_REASONS, buildInvoiceDedupFilters, classifyInvoiceConflict, findInvoiceDedupConflicts, isUsedInvoice, mapInvoiceUnavailableReason, resolveInvoiceDedupBlocker, resolveUploadDedupDecision, };