文件
hyb-oa/server/src/services/invoiceGovernance.js
T

290 行
7.7 KiB
JavaScript

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",
};
const OCR_FIELD_LABELS = {
invoiceCode: "发票代码",
invoiceNumber: "发票号码",
invoiceDate: "开票日期",
totalAmount: "总金额",
sellerName: "销售方",
};
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 isInactiveArchivedInvoice(invoice) {
return Boolean(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 (isInactiveArchivedInvoice(conflict)) {
return null;
}
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,
AND: [
{ OR: filters },
{
OR: [
{ status: { in: ["pending_ocr", "ocr_failed", "available", "bound"] } },
{ status: "archived", archived_reason: USED_COMPLETE_ARCHIVED_REASON },
],
},
],
},
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;
}
}
function parseMissingInvoiceOcrFields(rawError) {
if (typeof rawError !== "string") {
return [];
}
const matched = rawError.match(/^missing_required_fields:(.+)$/);
if (!matched?.[1]) {
return [];
}
return matched[1]
.split(",")
.map((field) => field.trim())
.filter(Boolean);
}
function formatInvoiceOcrFailureMessage(rawError) {
const missingFields = parseMissingInvoiceOcrFields(rawError);
if (missingFields.length) {
const labels = missingFields.map((field) => OCR_FIELD_LABELS[field] ?? field);
return `票据识别缺少关键字段:${labels.join("、")}。`;
}
if (rawError === "mock_ocr_failed") {
return "票据识别失败,请重试或人工补齐关键信息。";
}
if (!rawError) {
return "票据识别失败,请稍后重试。";
}
return "票据识别失败,请重新识别或手动处理。";
}
function resolveInvoiceAvailability(invoice) {
if (!invoice) {
return {
reason: "not_found",
message: "未找到发票",
};
}
if (invoice.status === "pending_ocr") {
return {
reason: "pending_ocr",
message: "发票正在识别,完成后会自动刷新",
};
}
if (invoice.status === "ocr_failed") {
const rawError = String(invoice.ocr_last_error ?? "");
if (rawError === "duplicate_invoice_code_number" || rawError.includes("duplicate_invoice")) {
return {
reason: "duplicate_invoice",
message: "已识别出相同发票,不能重复添加",
};
}
return {
reason: "ocr_failed",
message: formatInvoiceOcrFailureMessage(rawError),
};
}
if (isUsedInvoice(invoice)) {
return {
reason: INVOICE_UNAVAILABLE_REASONS.usedByCompany,
message: mapInvoiceUnavailableReason(INVOICE_UNAVAILABLE_REASONS.usedByCompany),
};
}
if (invoice.status === "archived") {
return {
reason: "archived",
message: "发票已归档,不可添加",
};
}
return {
reason: "available",
message: "发票可添加",
};
}
module.exports = {
INVOICE_UNAVAILABLE_REASONS,
buildInvoiceDedupFilters,
classifyInvoiceConflict,
findInvoiceDedupConflicts,
formatInvoiceOcrFailureMessage,
isUsedInvoice,
mapInvoiceUnavailableReason,
parseMissingInvoiceOcrFields,
resolveInvoiceAvailability,
resolveInvoiceDedupBlocker,
resolveUploadDedupDecision,
};