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

180 行
5.1 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",
};
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,
};