完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)

汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
2026-09-19 13:11:04 +08:00
父节点 729d90576f
当前提交 1f8d5b658a
共修改 367 个文件,包含 21079 行新增和 12287 行删除
-91
查看文件
@@ -1,91 +0,0 @@
const cron = require('node-cron');
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require('../services/featureFlags');
const { syncErpMasterData } = require('../services/integrations/erpMasterData');
async function runErpMasterDataSyncOnce(prisma, config, logger) {
if (!config.erpMasterDataSyncCron) {
return {
skipped: true,
reason: 'cron_disabled',
items: [],
};
}
const gate = await evaluateFeatureFlag({
prisma,
config,
flagKey: FEATURE_FLAG_KEYS.erpSync,
departmentId: null,
strictDepartment: false,
});
if (!gate.enabled) {
logger.info(
{
featureFlag: FEATURE_FLAG_KEYS.erpSync,
reason: gate.reason,
},
'ERP master data sync skipped by ERP gate',
);
return {
skipped: true,
reason: gate.reason,
items: [],
};
}
return syncErpMasterData(prisma, config, { logger });
}
function startErpMasterDataSyncJob(fastify) {
if (!fastify.config.erpMasterDataSyncCron) {
fastify.log.info('ERP master data sync cron disabled');
return null;
}
let running = false;
const task = cron.schedule(fastify.config.erpMasterDataSyncCron, async () => {
if (running) {
return;
}
running = true;
try {
await runErpMasterDataSyncOnce(fastify.prisma, fastify.config, fastify.log);
fastify.metrics?.jobRunsTotal?.inc?.({
job: 'erp.master-data.sync',
driver: 'cron',
outcome: 'success',
});
} catch (error) {
fastify.log.error({ err: error }, 'ERP master data sync job failed');
fastify.metrics?.jobRunsTotal?.inc?.({
job: 'erp.master-data.sync',
driver: 'cron',
outcome: 'failure',
});
} finally {
running = false;
}
});
fastify.addHook('onClose', async () => {
task.stop();
task.destroy();
});
fastify.log.info(
{
cron: fastify.config.erpMasterDataSyncCron,
},
'ERP master data sync job scheduled',
);
return task;
}
module.exports = {
runErpMasterDataSyncOnce,
startErpMasterDataSyncJob,
};
-2
查看文件
@@ -3,7 +3,6 @@ const { startInvoiceArchiveJob } = require("./invoiceArchiveJob");
const { startOcrJob } = require("./ocrJob");
const { startReceiptOcrJob } = require("./paymentReceiptOcrJob");
const { startIntegrationOutboxWorker } = require("./integrationOutboxWorker");
const { startErpMasterDataSyncJob } = require("./erpMasterDataSyncJob");
const { startPgBossJobs } = require("./pgBossJobs");
async function startJobs(fastify) {
@@ -22,7 +21,6 @@ async function startJobs(fastify) {
startReceiptOcrJob(fastify);
startInvoiceArchiveJob(fastify);
startIntegrationOutboxWorker(fastify);
startErpMasterDataSyncJob(fastify);
return { driver: "cron" };
}
+11 -1
查看文件
@@ -72,13 +72,23 @@ async function claimReceiptJob(prisma) {
`;
if (!batches.length) return null;
const batch = await tx.paymentBatch.findUnique({ where: { id: batches[0].id }, include: { items: true } });
if (batch?.entity_type !== "reimbursement" || !requiresBankReceipt(batch)) {
if (!["reimbursement", "loan_repayment"].includes(batch?.entity_type) || !requiresBankReceipt(batch)) {
await tx.paymentBatchReceiptAttachment.updateMany({
where: { payment_batch_id: batches[0].id, receipt_ocr_state: "queued" },
data: { receipt_ocr_state: "legacy" },
});
return null;
}
if (batch.entity_type === "loan_repayment") {
try { await require("../services/companyLoanPayments").validateLoanPayment(tx, batch); }
catch (error) {
if (!error?.details?.reasonCode?.startsWith("loan.")) throw error;
await tx.paymentBatchReceiptAttachment.updateMany({
where: { payment_batch_id: batch.id, receipt_ocr_state: "queued" }, data: { receipt_ocr_state: "legacy" },
});
return null;
}
}
const attachments = await tx.$queryRaw`
SELECT id FROM payment_batch_receipt_attachments
WHERE payment_batch_id = ${batches[0].id}::uuid AND receipt_ocr_state = 'queued'
-48
查看文件
@@ -6,7 +6,6 @@ const { archiveUnusedInvoices } = require("./invoiceArchiveJob");
const { runOcrForInvoiceId, runOcrOnce } = require("./ocrJob");
const { runVerificationForRecord } = require("./verificationJob");
const { runIntegrationOutboxOnce } = require("./integrationOutboxJob");
const { runErpMasterDataSyncOnce } = require("./erpMasterDataSyncJob");
const { publishInvoiceUpdated } = require("../services/events");
const {
FEATURE_FLAG_KEYS,
@@ -19,7 +18,6 @@ const JOB_DELEGATION_REVOKE = "delegation.revoke-expired";
const JOB_INVOICE_ARCHIVE = "invoice.archive-unused";
const JOB_INVOICE_VERIFY = "invoice.verify";
const JOB_INTEGRATION_OUTBOX = "integration.outbox.dispatch";
const JOB_ERP_MASTER_DATA_SYNC = "erp.master-data.sync";
function resolveDatabaseConnectionString() {
const connectionString = process.env.DATABASE_URL;
@@ -52,9 +50,6 @@ async function startPgBossJobs(fastify) {
await boss.createQueue(JOB_INVOICE_ARCHIVE);
await boss.createQueue(JOB_INVOICE_VERIFY);
await boss.createQueue(JOB_INTEGRATION_OUTBOX);
if (fastify.config.erpMasterDataSyncCron) {
await boss.createQueue(JOB_ERP_MASTER_DATA_SYNC);
}
const onOcrInvoiceProcessed = (event) =>
publishInvoiceUpdated(fastify, {
@@ -278,37 +273,6 @@ async function startPgBossJobs(fastify) {
}
});
if (fastify.config.erpMasterDataSyncCron) {
await boss.work(JOB_ERP_MASTER_DATA_SYNC, { batchSize: 1 }, async (jobs) => {
const job = jobs?.[0];
try {
const result = await runErpMasterDataSyncOnce(
fastify.prisma,
fastify.config,
fastify.log,
);
fastify.metrics?.jobRunsTotal?.inc?.({
job: JOB_ERP_MASTER_DATA_SYNC,
driver: "pgboss",
outcome: "success",
});
if (!result?.skipped) {
fastify.log.info({ jobId: job?.id, result }, "pg-boss ERP master data sync processed");
}
} catch (error) {
fastify.metrics?.jobRunsTotal?.inc?.({
job: JOB_ERP_MASTER_DATA_SYNC,
driver: "pgboss",
outcome: "failure",
});
fastify.log.error({ err: error, jobId: job?.id }, "pg-boss ERP master data sync failed");
throw error;
}
});
}
await boss.work(JOB_INTEGRATION_OUTBOX, { batchSize: 1 }, async (jobs) => {
const job = jobs?.[0];
@@ -405,16 +369,6 @@ async function startPgBossJobs(fastify) {
singletonSeconds: 55,
});
if (fastify.config.erpMasterDataSyncCron) {
await boss.schedule(JOB_ERP_MASTER_DATA_SYNC, fastify.config.erpMasterDataSyncCron, null, {
tz: fastify.config.appTimezone,
singletonKey: JOB_ERP_MASTER_DATA_SYNC,
singletonSeconds: 55,
});
} else {
await boss.unschedule(JOB_ERP_MASTER_DATA_SYNC).catch(() => {});
}
fastify.log.info(
{
schema: fastify.config.pgBossSchema,
@@ -426,7 +380,6 @@ async function startPgBossJobs(fastify) {
delegationCron: fastify.config.delegationRevokeCron,
invoiceArchiveCron: fastify.config.invoiceArchiveCron,
integrationOutboxCron: fastify.config.integrationOutboxCron,
erpMasterDataSyncCron: fastify.config.erpMasterDataSyncCron || null,
},
"pg-boss job system started",
);
@@ -454,7 +407,6 @@ module.exports = {
JOB_INVOICE_ARCHIVE,
JOB_INVOICE_VERIFY,
JOB_INTEGRATION_OUTBOX,
JOB_ERP_MASTER_DATA_SYNC,
enqueueOcrInvoice,
startPgBossJobs,
};