文件
hyb-oa/.trellis/tasks/archive/2026-08/07-27-yang-jun-reimbursement-readiness/research/production-reimbursement-readiness-pre.cjs
T

271 行
12 KiB
JavaScript

"use strict";
const { createHash } = require("node:crypto");
const path = require("node:path");
const TARGET_NAME_SHA256 = "d6ad24a313331e19381654bab85b3a53de2ce2d518d7fe6a819124cca81c9cc6";
const MANAGER_NAME_SHA256 = "5da0f6dcab6e9b1e0aa52e06e18a7cc9e181ae51c4a10b64fb3411cfd607424a";
const REIMBURSEMENT_STATUSES = Object.freeze(["draft", "reviewing", "approved", "paid", "rejected", "cancelled"]);
const INVOICE_STATUSES = Object.freeze(["pending_ocr", "ocr_failed", "available", "bound", "archived"]);
const WORKFLOW_INSTANCE_STATUSES = Object.freeze(["pending", "approved", "rejected", "cancelled", "terminated"]);
const WORKFLOW_TASK_STATUSES = Object.freeze(["pending", "completed", "rejected", "cancelled", "skipped"]);
const PAYMENT_BATCH_STATUSES = Object.freeze(["draft", "reviewed", "executed", "failed", "cancelled"]);
const RECEIPT_GOVERNANCE_STATUSES = Object.freeze([
"draft", "under_approval", "pending_review", "approved", "archived", "returned", "rejected", "quarantined",
]);
function sha256(value) {
return createHash("sha256").update(String(value), "utf8").digest("hex");
}
function zeroFilled(rows, allowed) {
const result = Object.fromEntries(allowed.map((status) => [status, 0]));
for (const row of rows) {
const status = String(row.status ?? "");
if (!allowed.includes(status)) throw new Error("unexpected_status");
result[status] = Number(row.count ?? 0);
}
return result;
}
function rolesGranting(permissions, permission) {
return Object.entries(permissions)
.filter(([, values]) => values.includes("*") || values.includes(permission))
.map(([role]) => role)
.sort();
}
async function grouped(client, sql, allowed) {
const result = await client.query(sql);
return zeroFilled(result.rows, allowed);
}
async function run() {
const appRoot = path.resolve(process.env.REIMBURSEMENT_READINESS_APP_ROOT || process.cwd());
const { Client } = require(path.join(appRoot, "node_modules/pg"));
const { getConfig } = require(path.join(appRoot, "src/config.js"));
const { ROLE_PERMISSIONS } = require(path.join(appRoot, "src/constants/permissions.js"));
const config = getConfig();
const permissionRoles = {
expenseSubmit: rolesGranting(ROLE_PERMISSIONS, "expense.submit"),
invoiceUpload: rolesGranting(ROLE_PERMISSIONS, "invoice.upload"),
expenseApprove: rolesGranting(ROLE_PERMISSIONS, "expense.approve"),
};
if (Object.values(permissionRoles).some((roles) => roles.length === 0)) {
throw new Error("permission_role_contract_empty");
}
const client = new Client({ connectionString: process.env.DATABASE_URL });
let inTransaction = false;
try {
await client.connect();
await client.query("BEGIN TRANSACTION READ ONLY");
inTransaction = true;
await client.query("SET LOCAL statement_timeout = '30s'");
await client.query("SET LOCAL lock_timeout = '2s'");
const readOnly = await client.query("SELECT current_setting('transaction_read_only') AS value");
if (readOnly.rows[0]?.value !== "on") throw new Error("transaction_not_read_only");
const employees = await client.query(`
SELECT
e.id,
e.name,
e.status::text AS status,
e.archived_at,
e.email,
e.phone,
e.password_hash,
e.must_change_password,
e.login_locked_until,
e.direct_manager_id,
EXISTS (SELECT 1 FROM wecom_identity_bindings w WHERE w.employee_id = e.id) AS has_wecom_binding,
COALESCE(array_agg(er.role_code::text ORDER BY er.role_code::text)
FILTER (WHERE er.role_code IS NOT NULL), ARRAY[]::text[]) AS role_codes
FROM employees e
LEFT JOIN employee_roles er ON er.employee_id = e.id
GROUP BY e.id
ORDER BY e.id
`);
if (!Array.isArray(employees.rows) || employees.rows.length === 0) throw new Error("employees_empty");
const active = employees.rows.filter((item) => item.status === "active" && item.archived_at == null);
const permissionCount = (permission) => active.filter((item) => (
item.role_codes.some((role) => permissionRoles[permission].includes(role))
)).length;
const byId = new Map(employees.rows.map((item) => [item.id, item]));
const targetMatches = employees.rows.filter((item) => sha256(item.name) === TARGET_NAME_SHA256);
const managerMatches = employees.rows.filter((item) => sha256(item.name) === MANAGER_NAME_SHA256);
const target = targetMatches.length === 1 ? targetMatches[0] : null;
const manager = managerMatches.length === 1 ? managerMatches[0] : null;
const employeeSummary = {
total: employees.rows.length,
active: active.length,
activeWithLoginIdentifier: active.filter((item) => (
String(item.email ?? "").trim() || String(item.phone ?? "").trim()
)).length,
activeWithPasswordCredential: active.filter((item) => String(item.password_hash ?? "").trim()).length,
activeCurrentlyUnlocked: active.filter((item) => !item.login_locked_until || new Date(item.login_locked_until) <= new Date()).length,
activeWithoutInitialPasswordChange: active.filter((item) => item.must_change_password === false).length,
activeWithWecomBinding: active.filter((item) => item.has_wecom_binding === true).length,
activeWithDirectManager: active.filter((item) => item.direct_manager_id != null).length,
activeWithValidActiveDistinctManager: active.filter((item) => {
const linked = byId.get(item.direct_manager_id);
return linked && linked.id !== item.id && linked.status === "active" && linked.archived_at == null;
}).length,
permissions: {
expenseSubmit: permissionCount("expenseSubmit"),
invoiceUpload: permissionCount("invoiceUpload"),
expenseApprove: permissionCount("expenseApprove"),
},
targetManagerContract: {
targetUnique: targetMatches.length === 1,
managerUnique: managerMatches.length === 1,
distinct: Boolean(target && manager && target.id !== manager.id),
targetActive: Boolean(target && target.status === "active" && target.archived_at == null),
managerActive: Boolean(manager && manager.status === "active" && manager.archived_at == null),
managerCanApprove: Boolean(manager && manager.role_codes.some((role) => permissionRoles.expenseApprove.includes(role))),
relationshipMatches: Boolean(target && manager && target.direct_manager_id === manager.id),
},
};
const expenseTypes = await client.query("SELECT count(*)::int AS count FROM expense_types WHERE enabled = true");
const workflows = await client.query(`
SELECT count(*)::int AS count
FROM workflow_definitions
WHERE entity_type = 'reimbursement' AND is_active = true AND archived_at IS NULL
`);
const legalEntities = await client.query("SELECT count(*)::int AS count FROM legal_entities WHERE enabled = true AND archived_at IS NULL");
const paymentFlag = await client.query(`
SELECT override_mode, version
FROM feature_flags
WHERE flag_key = 'payment_batch_v1'
`);
if (paymentFlag.rows.length !== 1) throw new Error("payment_flag_not_unique");
const reimbursements = await grouped(
client,
"SELECT status::text AS status, count(*)::int AS count FROM reimbursement_forms GROUP BY status ORDER BY status",
REIMBURSEMENT_STATUSES,
);
const invoices = await grouped(
client,
"SELECT status::text AS status, count(*)::int AS count FROM invoice_pool GROUP BY status ORDER BY status",
INVOICE_STATUSES,
);
const workflowInstances = await grouped(
client,
"SELECT current_status::text AS status, count(*)::int AS count FROM workflow_instances WHERE entity_type = 'reimbursement' GROUP BY current_status ORDER BY current_status",
WORKFLOW_INSTANCE_STATUSES,
);
const workflowTasks = await grouped(
client,
`SELECT wt.status::text AS status, count(*)::int AS count
FROM workflow_tasks wt
JOIN workflow_instances wi ON wi.id = wt.instance_id
WHERE wi.entity_type = 'reimbursement'
GROUP BY wt.status ORDER BY wt.status`,
WORKFLOW_TASK_STATUSES,
);
const paymentBatches = await grouped(
client,
"SELECT status::text AS status, count(*)::int AS count FROM payment_batches WHERE entity_type = 'reimbursement' GROUP BY status ORDER BY status",
PAYMENT_BATCH_STATUSES,
);
const receiptGovernance = await grouped(
client,
`SELECT pra.governance_status AS status, count(*)::int AS count
FROM payment_batch_receipt_attachments pra
JOIN payment_batches pb ON pb.id = pra.payment_batch_id
WHERE pb.entity_type = 'reimbursement'
GROUP BY pra.governance_status ORDER BY pra.governance_status`,
RECEIPT_GOVERNANCE_STATUSES,
);
const receiptCounts = await client.query(`
SELECT
count(*)::int AS total,
count(DISTINCT pra.payment_batch_id)::int AS batches_with_receipt,
count(DISTINCT pra.payment_batch_id) FILTER (WHERE pb.status = 'executed')::int AS executed_batches_with_receipt
FROM payment_batch_receipt_attachments pra
JOIN payment_batches pb ON pb.id = pra.payment_batch_id
WHERE pb.entity_type = 'reimbursement'
`);
const closedLoop = await client.query(`
SELECT count(DISTINCT rf.id)::int AS count
FROM reimbursement_forms rf
JOIN workflow_instances wi
ON wi.reimbursement_id = rf.id
AND wi.entity_type = 'reimbursement'
AND wi.current_status = 'approved'
JOIN payment_batch_items pbi ON pbi.entity_id = rf.id
JOIN payment_batches pb
ON pb.id = pbi.payment_batch_id
AND pb.entity_type = 'reimbursement'
AND pb.status = 'executed'
WHERE rf.status = 'paid'
AND EXISTS (
SELECT 1 FROM payment_batch_receipt_attachments pra
WHERE pra.payment_batch_id = pb.id
AND pra.governance_status = 'approved'
)
`);
const ocrProvider = String(process.env.OCR_PROVIDER ?? "off").trim().toLowerCase();
if (!["off", "mock", "tencent"].includes(ocrProvider)) throw new Error("ocr_provider_unexpected");
const result = {
status: "PASS",
transactionReadOnly: true,
sourceBuildSha: config.buildSha,
employees: employeeSummary,
configuration: {
activeExpenseTypes: Number(expenseTypes.rows[0]?.count ?? -1),
activeReimbursementWorkflows: Number(workflows.rows[0]?.count ?? -1),
activeLegalEntitiesObservationOnly: Number(legalEntities.rows[0]?.count ?? -1),
invoiceUploadConfigured: Number(config.maxUploadSizeBytes ?? 0) > 0,
ocrProvider,
invoiceVerificationEnabled: config.invoiceVerificationEnabled === true,
paymentBatchFlag: {
overrideMode: String(paymentFlag.rows[0].override_mode),
version: Number(paymentFlag.rows[0].version),
},
},
lifecycle: {
reimbursements,
invoices,
workflow: { instances: workflowInstances, tasks: workflowTasks },
paymentBatches,
receipts: {
total: Number(receiptCounts.rows[0]?.total ?? 0),
batchesWithReceipt: Number(receiptCounts.rows[0]?.batches_with_receipt ?? 0),
executedBatchesWithReceipt: Number(receiptCounts.rows[0]?.executed_batches_with_receipt ?? 0),
byGovernanceStatus: receiptGovernance,
},
completeSubmitApprovePayReceiptLoops: Number(closedLoop.rows[0]?.count ?? 0),
},
};
if (result.configuration.activeExpenseTypes < 0 || result.configuration.activeReimbursementWorkflows < 0) {
throw new Error("configuration_count_invalid");
}
if (!result.employees.targetManagerContract.targetUnique || !result.employees.targetManagerContract.managerUnique) {
throw new Error("target_manager_not_unique");
}
await client.query("COMMIT");
inTransaction = false;
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} catch {
if (inTransaction) {
try { await client.query("ROLLBACK"); } catch { /* fail closed */ }
}
process.stderr.write("[reimbursement-readiness-pre][FAILED] code=QUERY_FAILED\n");
process.exitCode = 1;
} finally {
await client.end().catch(() => {});
}
}
if (require.main === module || process.argv[1] === "-") {
run();
}
module.exports = { zeroFilled, rolesGranting };