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

779 行
28 KiB
JavaScript

const crypto = require("node:crypto");
const { maskApprovalIdentifier } = require("./wecomApproval");
const TERMINAL_STATUSES = new Set(["approved", "rejected", "revoked", "terminated", "changed"]);
function toSafeString(value, maxLength = 500) {
if (value == null) return "";
return String(value).trim().slice(0, maxLength);
}
function sha256(value) {
return crypto.createHash("sha256").update(String(value ?? "")).digest("hex");
}
function stableJson(value) {
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(",")}]`;
}
if (value && typeof value === "object") {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}
function digestObject(value) {
return sha256(stableJson(value));
}
function corpHash(config = {}) {
return sha256(toSafeString(config.wecomCorpId || config.corpId || "unknown-corp", 160));
}
function normalizeWecomApprovalStatus(value) {
const raw = toSafeString(value, 40);
return {
"1": "pending",
"2": "approved",
"3": "rejected",
"4": "revoked",
"6": "revoked",
"7": "terminated",
"10": "approved",
}[raw] || (raw ? "unknown" : "unknown");
}
function isTerminalStatus(value) {
return TERMINAL_STATUSES.has(toSafeString(value, 40));
}
function parseWecomTime(value) {
const raw = toSafeString(value, 80);
if (!raw) return null;
if (/^\d+$/.test(raw)) {
const numeric = Number(raw);
if (!Number.isFinite(numeric) || numeric <= 0) return null;
const millis = numeric > 100000000000 ? numeric : numeric * 1000;
const date = new Date(millis);
return Number.isNaN(date.getTime()) ? null : date;
}
const parsed = Date.parse(raw);
if (!Number.isFinite(parsed)) return null;
return new Date(parsed);
}
function pickText(source, names, maxLength = 500) {
if (!source || typeof source !== "object") return "";
for (const name of names) {
const value = source[name];
if (value == null) continue;
if (typeof value === "object") {
const nested = pickText(value, ["userid", "user_id", "UserId", "UserID", "name", "Name", "text", "value", "#text"], maxLength);
if (nested) return nested;
} else {
const text = toSafeString(value, maxLength);
if (text) return text;
}
}
return "";
}
function toArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
function inferApprovalType({ spName, templateId, fallback }) {
const text = `${spName || ""} ${templateId || ""} ${fallback || ""}`;
if (/补卡|missed|punch|打卡/i.test(text)) return "missed_punch";
if (/请假|leave/i.test(text)) return "leave";
return fallback || "unknown";
}
function valueToText(value, maxLength = 4000) {
if (value == null) return "";
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return toSafeString(value, maxLength);
}
if (Array.isArray(value)) {
return value.map((item) => valueToText(item, 400)).filter(Boolean).join("; ").slice(0, maxLength);
}
if (typeof value === "object") {
const preferred = pickText(value, ["text", "value", "new_value", "date", "time", "title", "name", "option"], maxLength);
if (preferred) return preferred;
try {
return JSON.stringify(value).slice(0, maxLength);
} catch {
return "";
}
}
return "";
}
function controlTitle(control, index) {
const title = control?.title || control?.name || control?.label || control?.property?.title;
if (typeof title === "string") return toSafeString(title, 160);
if (Array.isArray(title)) {
return toSafeString(title.map((item) => item?.text || item?.name || item?.value).filter(Boolean).join("/"), 160);
}
if (title && typeof title === "object") {
return pickText(title, ["text", "name", "value"], 160);
}
return `field_${index}`;
}
function controlType(control) {
return toSafeString(
control?.control
|| control?.control_type
|| control?.controlType
|| control?.type
|| control?.property?.control
|| control?.property?.control_type
|| control?.property?.type
|| "unknown",
80,
);
}
function normalizeField(control, index) {
const name = controlTitle(control, index);
const type = controlType(control);
const rawValue = control?.value ?? control?.new_value ?? control?.text ?? control?.title ?? control?.property?.value;
const normalizedValue = valueToText(rawValue || control, 4000);
const keyBase = toSafeString(control?.id || control?.key || control?.control_id || name || `field_${index}`, 100);
const sensitive = /原因|事由|说明|备注|reason|comment/i.test(name) ? "restricted" : "normal";
return {
field_key: `${keyBase || "field"}_${index}`.slice(0, 120),
field_index: index,
field_name: name,
field_type: type,
normalized_value: normalizedValue,
value_preview: normalizedValue ? `${normalizedValue.slice(0, 24)}${normalizedValue.length > 24 ? "..." : ""}` : null,
sensitive_level: sensitive,
display_allowed: sensitive === "normal",
};
}
function normalizeFields(info) {
const applyData = info?.apply_data && typeof info.apply_data === "object" ? info.apply_data : {};
const contents = toArray(applyData.contents || info?.contents || info?.applyData?.contents);
return contents
.map((control, index) => normalizeField(control, index))
.filter((item) => item.field_key);
}
function extractUserId(value) {
if (!value) return "";
if (typeof value === "string") return toSafeString(value, 128);
return pickText(value, ["userid", "user_id", "UserId", "UserID", "open_userid", "OpenUserId"], 128);
}
function normalizeParticipants(info) {
const participants = [];
const applyer = info?.applyer || info?.applicant || info?.creator;
const applicantUserId = extractUserId(applyer);
if (applicantUserId) {
participants.push({
role: "applicant",
node_index: 0,
external_user_id: applicantUserId,
is_current: false,
display_summary: null,
});
}
for (const [nodeIndex, record] of toArray(info?.sp_record || info?.spRecord).entries()) {
const details = toArray(record?.details || record?.detail || record?.approvers || record?.approver);
const users = details.length ? details : toArray(record?.userid || record?.user_id || record?.UserId);
for (const user of users) {
const userId = extractUserId(user);
if (!userId) continue;
participants.push({
role: "approver",
node_index: nodeIndex + 1,
external_user_id: userId,
is_current: false,
display_summary: null,
});
}
}
for (const [index, notifyer] of toArray(info?.notifyer || info?.notifier).entries()) {
const userId = extractUserId(notifyer);
if (!userId) continue;
participants.push({
role: "notifyer",
node_index: index,
external_user_id: userId,
is_current: false,
display_summary: null,
});
}
const seen = new Set();
return participants.filter((item) => {
const key = `${item.role}:${item.node_index}:${item.external_user_id}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function actionTypeFromStatus(value) {
const status = normalizeWecomApprovalStatus(value);
if (status === "approved") return "approve";
if (status === "rejected") return "reject";
if (status === "revoked") return "revoke";
if (status === "terminated") return "terminate";
if (status === "pending") return "pending";
return "status_change";
}
function normalizeActions(info, source) {
const actions = [];
for (const [nodeIndex, record] of toArray(info?.sp_record || info?.spRecord).entries()) {
const details = toArray(record?.details || record?.detail || record?.approvers || record?.approver);
const actionItems = details.length ? details : [record];
for (const [detailIndex, detail] of actionItems.entries()) {
const operator = extractUserId(detail) || extractUserId(record);
const status = pickText(detail, ["sp_status", "status", "result", "Speech"], 40)
|| pickText(record, ["sp_status", "status", "result"], 40)
|| info?.sp_status;
const actionTime = parseWecomTime(
pickText(detail, ["speech_time", "operate_time", "time", "mtime"], 80)
|| pickText(record, ["operate_time", "time", "mtime"], 80)
|| info?.apply_time,
);
const comment = pickText(detail, ["speech", "comment", "opinion", "remark"], 500);
const actionType = actionTypeFromStatus(status);
const idempotencyKey = digestObject([source, info?.sp_no, actionType, operator, actionTime?.toISOString(), nodeIndex, detailIndex]).slice(0, 64);
actions.push({
action_type: actionType,
operator_external_user_id: operator || null,
action_time: actionTime,
node_index: nodeIndex + 1,
comment_summary: comment || null,
source,
idempotency_key: idempotencyKey,
evidence_digest: idempotencyKey,
});
}
}
if (!actions.length && info?.sp_no) {
const actionTime = parseWecomTime(info?.apply_time || info?.applyTime);
const actionType = actionTypeFromStatus(info?.sp_status);
const idempotencyKey = digestObject([source, info.sp_no, actionType, actionTime?.toISOString()]).slice(0, 64);
actions.push({
action_type: actionType,
operator_external_user_id: extractUserId(info.applyer) || null,
action_time: actionTime,
node_index: 0,
comment_summary: null,
source,
idempotency_key: idempotencyKey,
evidence_digest: idempotencyKey,
});
}
return actions;
}
function normalizeApprovalDetailPayload(payload, options = {}) {
const info = payload?.info && typeof payload.info === "object" ? payload.info : payload || {};
const spNo = toSafeString(info.sp_no ?? info.spNo ?? options.spNo, 160);
const templateId = toSafeString(info.template_id ?? info.templateId ?? options.templateId, 160);
const spName = toSafeString(info.sp_name ?? info.spName ?? options.spName, 160);
const wecomStatus = toSafeString(info.sp_status ?? info.spStatus, 40);
const templateType = inferApprovalType({ spName, templateId, fallback: options.target });
const fields = normalizeFields(info);
const participants = normalizeParticipants(info);
const actions = normalizeActions({ ...info, sp_no: spNo }, options.source || "compensation_pull");
const appliedAt = parseWecomTime(info.apply_time ?? info.applyTime);
const submittedAt = parseWecomTime(info.apply_time ?? info.applyTime ?? info.create_time);
const statusObservedAt = parseWecomTime(info.update_time ?? info.updateTime ?? info.apply_time ?? info.applyTime) || new Date();
const completedAt = isTerminalStatus(normalizeWecomApprovalStatus(wecomStatus)) ? statusObservedAt : null;
const applicantExternalUserId = extractUserId(info.applyer || info.applicant || info.creator) || null;
const normalized = {
sp_no: spNo,
template_id: templateId || null,
template_type: templateType,
sp_name: spName || null,
normalized_status: normalizeWecomApprovalStatus(wecomStatus),
wecom_status: wecomStatus || null,
applicant_external_user_id: applicantExternalUserId,
submitted_at: submittedAt,
applied_at: appliedAt,
completed_at: completedAt,
status_observed_at: statusObservedAt,
fields,
participants,
actions,
};
return {
...normalized,
detail_digest: digestObject({
sp_no: normalized.sp_no,
template_id: normalized.template_id,
normalized_status: normalized.normalized_status,
wecom_status: normalized.wecom_status,
fields: normalized.fields,
actions: normalized.actions,
}),
};
}
function publicArchiveSummary(detailOrRecord) {
return {
archiveId: detailOrRecord?.id || detailOrRecord?.archive_id || null,
spNoPrefix: maskApprovalIdentifier(detailOrRecord?.sp_no),
templateIdPrefix: maskApprovalIdentifier(detailOrRecord?.template_id),
templateType: detailOrRecord?.template_type || detailOrRecord?.approval_type || "unknown",
normalizedStatus: detailOrRecord?.normalized_status || "unknown",
source: detailOrRecord?.last_source || detailOrRecord?.source || null,
};
}
async function mapEmployeeIdByWecomUserId(tx, externalUserId) {
const userId = toSafeString(externalUserId, 128);
if (!userId || !tx?.wecomIdentityBinding) return null;
const binding = await tx.wecomIdentityBinding.findUnique({
where: { external_user_id: userId },
select: { employee_id: true },
}).catch(() => null);
return binding?.employee_id || null;
}
function shouldAdvanceStatus(current, incoming) {
if (!current?.status_observed_at) return true;
if (!incoming.status_observed_at) return false;
if (new Date(incoming.status_observed_at).getTime() < new Date(current.status_observed_at).getTime()
&& isTerminalStatus(current.normalized_status)
&& !isTerminalStatus(incoming.normalized_status)) {
return false;
}
return new Date(incoming.status_observed_at).getTime() >= new Date(current.status_observed_at).getTime();
}
async function upsertArchiveActions(tx, archiveId, actions = []) {
for (const action of actions) {
const operatorEmployeeId = await mapEmployeeIdByWecomUserId(tx, action.operator_external_user_id);
await tx.wecomApprovalAction.upsert({
where: {
archive_id_idempotency_key: {
archive_id: archiveId,
idempotency_key: action.idempotency_key,
},
},
update: {
action_type: action.action_type,
operator_external_user_id: action.operator_external_user_id,
operator_employee_id: operatorEmployeeId,
action_time: action.action_time,
node_index: action.node_index,
comment_summary: action.comment_summary,
source: action.source,
evidence_digest: action.evidence_digest,
},
create: {
...action,
archive_id: archiveId,
operator_employee_id: operatorEmployeeId,
},
});
}
}
async function replaceArchiveChildren(tx, archiveId, detail) {
await tx.wecomApprovalFieldSummary.deleteMany({ where: { archive_id: archiveId } });
await tx.wecomApprovalParticipant.deleteMany({ where: { archive_id: archiveId } });
if (detail.fields.length) {
await tx.wecomApprovalFieldSummary.createMany({
data: detail.fields.map((field) => ({ ...field, archive_id: archiveId })),
skipDuplicates: true,
});
}
if (detail.participants.length) {
const participants = [];
for (const participant of detail.participants) {
participants.push({
...participant,
archive_id: archiveId,
employee_id: await mapEmployeeIdByWecomUserId(tx, participant.external_user_id),
mapping_status: participant.external_user_id ? "mapped_or_external" : "unknown",
});
}
await tx.wecomApprovalParticipant.createMany({ data: participants, skipDuplicates: true });
}
await upsertArchiveActions(tx, archiveId, detail.actions);
}
async function upsertApprovalArchive(tx, config, detail, source) {
const corp_id_hash = corpHash(config);
const existing = await tx.wecomApprovalArchive.findUnique({
where: { corp_id_hash_sp_no: { corp_id_hash, sp_no: detail.sp_no } },
});
const applicantEmployeeId = await mapEmployeeIdByWecomUserId(tx, detail.applicant_external_user_id);
const shouldAdvance = shouldAdvanceStatus(existing, detail);
const baseData = {
template_id: detail.template_id,
template_type: detail.template_type,
sp_name: detail.sp_name,
applicant_employee_id: applicantEmployeeId,
applicant_external_user_id: detail.applicant_external_user_id,
submitted_at: detail.submitted_at,
applied_at: detail.applied_at,
completed_at: detail.completed_at,
last_source: source,
last_detail_digest: detail.detail_digest,
single_tap_approve_risk: detail.template_type === "leave" || detail.template_type === "missed_punch",
readability_status: "PASS_READABLE",
source_summary: {
source,
spNoPrefix: maskApprovalIdentifier(detail.sp_no),
templateIdPrefix: maskApprovalIdentifier(detail.template_id),
},
};
const statusData = shouldAdvance ? {
normalized_status: detail.normalized_status,
wecom_status: detail.wecom_status,
status_observed_at: detail.status_observed_at,
} : {};
return tx.wecomApprovalArchive.upsert({
where: { corp_id_hash_sp_no: { corp_id_hash, sp_no: detail.sp_no } },
update: {
...baseData,
...statusData,
last_compensation_at: source === "compensation_pull" ? new Date() : existing?.last_compensation_at,
last_callback_at: source === "callback" ? new Date() : existing?.last_callback_at,
},
create: {
corp_id_hash,
sp_no: detail.sp_no,
...baseData,
normalized_status: detail.normalized_status,
wecom_status: detail.wecom_status,
status_observed_at: detail.status_observed_at,
last_compensation_at: source === "compensation_pull" ? new Date() : null,
last_callback_at: source === "callback" ? new Date() : null,
},
});
}
function applyDefined(target, key, value, predicate = (item) => item != null) {
if (predicate(value)) target[key] = value;
}
async function upsertApprovalArchiveFromCallback(tx, config, detail) {
const corp_id_hash = corpHash(config);
const existing = await tx.wecomApprovalArchive.findUnique({
where: { corp_id_hash_sp_no: { corp_id_hash, sp_no: detail.sp_no } },
});
const shouldAdvance = shouldAdvanceStatus(existing, detail);
const updateData = {
last_source: "callback",
last_callback_at: new Date(),
source_summary: {
source: "callback",
spNoPrefix: maskApprovalIdentifier(detail.sp_no),
templateIdPrefix: maskApprovalIdentifier(detail.template_id),
},
};
if (shouldAdvance) {
updateData.normalized_status = detail.normalized_status;
updateData.wecom_status = detail.wecom_status;
updateData.status_observed_at = detail.status_observed_at;
}
applyDefined(updateData, "template_id", detail.template_id);
applyDefined(updateData, "template_type", detail.template_type, (item) => item && item !== "unknown");
applyDefined(updateData, "sp_name", detail.sp_name);
applyDefined(updateData, "submitted_at", detail.submitted_at);
applyDefined(updateData, "applied_at", detail.applied_at);
applyDefined(updateData, "completed_at", detail.completed_at);
applyDefined(updateData, "applicant_external_user_id", detail.applicant_external_user_id);
if (detail.applicant_external_user_id) {
updateData.applicant_employee_id = await mapEmployeeIdByWecomUserId(tx, detail.applicant_external_user_id);
}
return tx.wecomApprovalArchive.upsert({
where: { corp_id_hash_sp_no: { corp_id_hash, sp_no: detail.sp_no } },
update: updateData,
create: {
corp_id_hash,
sp_no: detail.sp_no,
template_id: detail.template_id,
template_type: detail.template_type,
sp_name: detail.sp_name,
normalized_status: detail.normalized_status,
wecom_status: detail.wecom_status,
applicant_employee_id: detail.applicant_external_user_id
? await mapEmployeeIdByWecomUserId(tx, detail.applicant_external_user_id)
: null,
applicant_external_user_id: detail.applicant_external_user_id,
submitted_at: detail.submitted_at,
applied_at: detail.applied_at,
completed_at: detail.completed_at,
status_observed_at: detail.status_observed_at,
last_callback_at: new Date(),
last_source: "callback",
last_detail_digest: null,
single_tap_approve_risk: detail.template_type === "leave" || detail.template_type === "missed_punch",
readability_status: detail.template_type === "leave" || detail.template_type === "missed_punch" ? "PASS_READABLE" : null,
source_summary: {
source: "callback",
spNoPrefix: maskApprovalIdentifier(detail.sp_no),
templateIdPrefix: maskApprovalIdentifier(detail.template_id),
},
},
});
}
async function persistApprovalDetail({ prisma, config, payload, spNo, target, source = "compensation_pull", compensationRunId = null }) {
if (!prisma) throw new Error("Prisma client is required for WeCom approval archive persistence");
const detail = normalizeApprovalDetailPayload(payload, { spNo, target, source });
if (!detail.sp_no) throw new Error("WeCom approval detail missing sp_no");
return prisma.$transaction(async (tx) => {
const archive = await upsertApprovalArchive(tx, config, detail, source);
await replaceArchiveChildren(tx, archive.id, detail);
if (compensationRunId) {
await tx.wecomApprovalCompensationItem.upsert({
where: {
run_id_sp_no: {
run_id: compensationRunId,
sp_no: detail.sp_no,
},
},
update: {
archive_id: archive.id,
template_id: detail.template_id,
template_type: detail.template_type,
normalized_status: detail.normalized_status,
detail_status: "stored",
detail_digest: detail.detail_digest,
source_summary: publicArchiveSummary(detail),
},
create: {
run_id: compensationRunId,
archive_id: archive.id,
sp_no: detail.sp_no,
template_id: detail.template_id,
template_type: detail.template_type,
normalized_status: detail.normalized_status,
detail_status: "stored",
detail_digest: detail.detail_digest,
source_summary: publicArchiveSummary(detail),
},
});
}
return publicArchiveSummary({ ...detail, id: archive.id, last_source: source });
});
}
function callbackDetailFromSummary(summary) {
return {
sp_no: toSafeString(summary?.spNo, 160),
template_id: toSafeString(summary?.templateId, 160) || null,
template_type: summary?.approvalType || "unknown",
sp_name: null,
normalized_status: normalizeWecomApprovalStatus(summary?.spStatus),
wecom_status: summary?.spStatus || null,
applicant_external_user_id: null,
submitted_at: parseWecomTime(summary?.applyTime),
applied_at: parseWecomTime(summary?.applyTime),
completed_at: isTerminalStatus(normalizeWecomApprovalStatus(summary?.spStatus)) ? parseWecomTime(summary?.createTime) || new Date() : null,
status_observed_at: parseWecomTime(summary?.createTime) || new Date(),
detail_digest: digestObject({
sp_no: summary?.spNo,
template_id: summary?.templateId,
status: summary?.spStatus,
event: summary?.eventType,
createTime: summary?.createTime,
}),
fields: [],
participants: [],
actions: [{
action_type: actionTypeFromStatus(summary?.spStatus),
operator_external_user_id: null,
action_time: parseWecomTime(summary?.createTime) || new Date(),
node_index: 0,
comment_summary: null,
source: "callback",
idempotency_key: summary?.idempotencyKey || digestObject(["callback", summary?.spNo, summary?.spStatus]).slice(0, 64),
evidence_digest: summary?.idempotencyKey || digestObject(["callback", summary?.spNo, summary?.spStatus]).slice(0, 64),
}],
};
}
async function recordApprovalCallbackEvent({ prisma, config, summary }) {
if (!prisma) throw new Error("Prisma client is required for WeCom callback archive persistence");
const detail = callbackDetailFromSummary(summary);
if (!detail.sp_no) throw new Error("WeCom callback missing full sp_no for archive persistence");
const corp_id_hash = corpHash(config);
const eventDigest = digestObject({
eventType: summary.eventType,
callbackKind: summary.callbackKind,
spNo: detail.sp_no,
templateId: detail.template_id,
spStatus: summary.spStatus,
createTime: summary.createTime,
});
return prisma.$transaction(async (tx) => {
const archive = await upsertApprovalArchiveFromCallback(tx, config, detail);
await upsertArchiveActions(tx, archive.id, detail.actions);
const event = await tx.wecomApprovalCallbackEvent.upsert({
where: { idempotency_key: summary.idempotencyKey },
update: {
retry_count: { increment: 1 },
archive_id: archive.id,
processing_status: "accepted",
},
create: {
archive_id: archive.id,
corp_id_hash,
sp_no: detail.sp_no,
template_id: detail.template_id,
event_type: summary.eventType || "unknown",
callback_kind: summary.callbackKind || "unknown_event",
approval_type: detail.template_type,
normalized_status: detail.normalized_status,
wecom_status: detail.wecom_status,
event_time: parseWecomTime(summary.createTime),
apply_time: parseWecomTime(summary.applyTime),
processing_status: "accepted",
idempotency_key: summary.idempotencyKey,
event_digest: eventDigest,
source_summary: {
spNoPrefix: maskApprovalIdentifier(detail.sp_no),
templateIdPrefix: maskApprovalIdentifier(detail.template_id),
statusLabel: summary.statusLabel,
},
},
});
return {
recorded: event.retry_count === 0,
duplicate: event.retry_count > 0,
archiveId: archive.id,
idempotencyKey: summary.idempotencyKey,
spNoPrefix: maskApprovalIdentifier(detail.sp_no),
};
});
}
async function createCompensationRun({ prisma, timeWindow, inputs, options }) {
if (!prisma) throw new Error("Prisma client is required for compensation run persistence");
const runKey = digestObject({
startTime: timeWindow.startTime,
endTime: timeWindow.endTime,
templates: inputs?.map((item) => [item.key, item.templateIdPrefix]),
size: options?.size,
}).slice(0, 64);
return prisma.wecomApprovalCompensationRun.create({
data: {
run_key: runKey,
mode: options?.live ? "live_read" : "dry_run",
status: "running",
start_time: new Date(timeWindow.startTime * 1000),
end_time: new Date(timeWindow.endTime * 1000),
template_filter: inputs?.map((item) => ({
key: item.key,
templateIdPrefix: item.templateIdPrefix,
configuredFrom: item.templateSource,
})) || [],
page_size: options?.size || 100,
},
});
}
async function finishCompensationRun({ prisma, runId, status = "completed", pageCount = 0, itemCount = 0, detailCount = 0, findingCount = 0, errorCode = null, errorMessage = null }) {
if (!prisma || !runId) return null;
return prisma.wecomApprovalCompensationRun.update({
where: { id: runId },
data: {
status,
page_count: pageCount,
item_count: itemCount,
detail_count: detailCount,
finding_count: findingCount,
error_code: errorCode,
error_message: errorMessage,
finished_at: new Date(),
},
});
}
async function recordReconcileFinding({ prisma, runId = null, archiveId = null, findingType, source = "compensation_compare", description = "", severity = "info", evidence = {} }) {
if (!prisma || !findingType) return null;
const evidenceDigest = digestObject({ findingType, archiveId, runId, evidence }).slice(0, 64);
const data = {
archive_id: archiveId,
run_id: runId,
finding_type: findingType,
status: "open",
severity,
evidence_digest: evidenceDigest,
source,
description: toSafeString(description, 1000) || null,
};
if (runId) {
return prisma.wecomApprovalReconcileFinding.upsert({
where: {
run_id_finding_type_evidence_digest: {
run_id: runId,
finding_type: findingType,
evidence_digest: evidenceDigest,
},
},
update: {
status: "open",
severity,
source,
description: data.description,
archive_id: archiveId,
},
create: data,
});
}
if (archiveId) {
return prisma.wecomApprovalReconcileFinding.upsert({
where: {
archive_id_finding_type_evidence_digest: {
archive_id: archiveId,
finding_type: findingType,
evidence_digest: evidenceDigest,
},
},
update: {
status: "open",
severity,
source,
description: data.description,
run_id: runId,
},
create: data,
});
}
return prisma.wecomApprovalReconcileFinding.create({ data });
}
module.exports = {
corpHash,
createCompensationRun,
digestObject,
finishCompensationRun,
isTerminalStatus,
normalizeApprovalDetailPayload,
normalizeWecomApprovalStatus,
persistApprovalDetail,
publicArchiveSummary,
recordApprovalCallbackEvent,
recordReconcileFinding,
};