文件
hyb-oa/scripts/qa/business-numbering-db-fixture.cjs
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(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

210 行
16 KiB
JavaScript

const assert = require('node:assert/strict');
const path = require('node:path');
const { randomUUID } = require('node:crypto');
const { writeFileSync } = require('node:fs');
const { spawnSync } = require('node:child_process');
const { createRequire } = require('node:module');
const { setTimeout: delay } = require('node:timers/promises');
const req = createRequire(path.resolve(__dirname, '../../server/package.json'));
const { PrismaClient } = req('@prisma/client');
const { PrismaPg } = req('@prisma/adapter-pg');
const { resolveNumberingContext, issueBusinessNumber, reserveProjectNumber, setNumberingPolicy } = req('./src/services/businessNumbering');
const { issueDocumentNumber, resolveDocumentSequenceConfig } = req('./src/services/documentSequence');
module.exports = async function runNumberingFixture(pool, scratch) {
const prisma = new PrismaClient({ adapter: new PrismaPg(pool, { disposeExternalPool: false }) });
const checks = [];
const instant = new Date('2026-10-01T00:00:00Z');
try {
const department = await prisma.department.create({ data: { name: '合成编号部门' } });
const actor = await prisma.employee.create({ data: {
email: `${randomUUID()}@example.invalid`, name: '合成编号用户', department_id: department.id,
title: '合成岗位', hire_date: new Date('2025-01-01'), bank_name: 'synthetic',
bank_account_encrypted: 'synthetic-unused', password_hash: 'synthetic-unused',
} });
const baseDraft = { applicant_id: actor.id, department_id: department.id, title: '合成采购' };
const oldDraft = await prisma.purchaseRequest.create({ data: baseDraft });
assert.equal(oldDraft.numbering_version, 1);
assert.equal(await prisma.businessNumberPolicy.count(), 0);
const oldContext = await prisma.$transaction(tx => resolveNumberingContext(tx, { now: instant }));
assert.equal(oldContext.version, 1);
const oldForm = await prisma.reimbursementForm.create({ data: { form_number: 'BX-20260918-0500',
submitter_id: actor.id, owner_id: actor.id, department_id: department.id, reimbursement_date: instant } });
const oldBatch = await prisma.paymentBatch.create({ data: { batch_no: 'PB-20260917-001', name: '合成旧批次',
entity_type: 'reimbursement', created_by: actor.id,
items: { create: { entity_id: oldForm.id, entity_code: oldForm.form_number, entity_title: '合成报销', amount: '1.00', snapshot_json: {} } } }, include: { items: true } });
checks.push('migration_inactive_existing_draft_legacy');
const effectiveAt = new Date('2026-09-01T00:00:00Z');
const change = (action, more = {}) => prisma.$transaction(tx => setNumberingPolicy(tx, {
action, actorId: actor.id, reference: 'synthetic-numbering', writersCompatible: true,
now: new Date('2026-08-31T00:00:00Z'), ...more,
}));
await prisma.documentSequence.create({ data: { document_type: 'reimbursement', prefix: 'BX', date_format: 'YYYYMM',
separator: '-', number_length: 4, reset_period: 'none', current_number: 12 } });
await assert.rejects(change('activate', { effectiveAt }), /compatibility review/);
assert.equal(await prisma.businessNumberPolicy.count(), 0);
assert.equal((await prisma.documentSequence.findUnique({ where: { document_type: 'reimbursement' } })).current_number, 12);
// Synthetic legacy-rule setup only; the activation operation itself never edits it.
await prisma.documentSequence.update({ where: { document_type: 'reimbursement' }, data: { date_format: 'YYYYMMDD' } });
checks.push('overlapping_active_legacy_rule_blocks_activation_without_rewriting_it');
await change('activate', { effectiveAt });
const activeConfig = await resolveDocumentSequenceConfig(prisma, { documentType: 'reimbursement' });
assert.equal(activeConfig.dateFormat, 'YYYYMM'); assert.equal(activeConfig.resetPeriod, 'monthly');
const newDraft = await prisma.$transaction(async tx => {
const ctx = await resolveNumberingContext(tx, { now: instant });
return tx.purchaseRequest.create({ data: { ...baseDraft, numbering_version: ctx.version } });
});
assert.equal(newDraft.numbering_version, 2);
async function submitDraft(draft) {
return prisma.$transaction(async tx => {
const row = await tx.purchaseRequest.findUniqueOrThrow({ where: { id: draft.id } });
const issued = await issueDocumentNumber(tx, { documentType: 'purchase_request', numberingVersion: row.numbering_version, now: instant });
return tx.purchaseRequest.update({ where: { id: row.id }, data: { request_no: issued.documentNumber } });
});
}
assert.match((await submitDraft(oldDraft)).request_no, /^CG-\d{8}-0001$/);
await change('pause');
assert.equal((await submitDraft(newDraft)).request_no, 'CG-2026-0001');
assert.equal((await prisma.$transaction(tx => resolveNumberingContext(tx, { now: instant }))).version, 1);
await change('resume');
assert.equal((await prisma.businessNumberPolicy.findUnique({ where: { version: 2 } })).effective_at.getTime(), effectiveAt.getTime());
await assert.rejects(pool.query('UPDATE purchase_requests SET numbering_version=2 WHERE id=$1', [oldDraft.id]), error => error.code === '23514');
await assert.rejects(pool.query("UPDATE business_number_policies SET effective_at='2020-01-01' WHERE version=2"), error => error.code === '23514');
checks.push('stored_draft_version_cutover_pause_resume_config');
const concurrent = await Promise.all(Array.from({ length: 12 }, () => prisma.$transaction(async tx => {
const context = await resolveNumberingContext(tx, { now: instant });
const issued = await issueBusinessNumber(tx, { family: 'reimbursement', ...context });
return tx.reimbursementForm.create({ data: { form_number: issued.documentNumber,
submitter_id: actor.id, owner_id: actor.id, department_id: department.id, reimbursement_date: instant } });
})));
assert.equal(new Set(concurrent.map(row => row.form_number)).size, 12);
assert.deepEqual(concurrent.map(row => row.form_number).sort(), Array.from({ length: 12 }, (_, i) => `BX-202610-${String(i + 1).padStart(4, '0')}`));
const deleted = concurrent.find(row => row.form_number.endsWith('0012'));
await prisma.reimbursementForm.delete({ where: { id: deleted.id } });
const next = await prisma.$transaction(tx => issueBusinessNumber(tx, { family: 'reimbursement', now: instant }));
assert.equal(next.documentNumber, 'BX-202610-0013');
const key = 'v2:reimbursement:BX:202610';
await assert.rejects(prisma.$transaction(async tx => {
await issueBusinessNumber(tx, { family: 'reimbursement', now: instant });
// Actual unique violation aborts the complete transaction including its counter.
await tx.reimbursementForm.create({ data: { form_number: oldForm.form_number,
submitter_id: actor.id, owner_id: actor.id, department_id: department.id, reimbursement_date: instant } });
}), error => error.code === 'P2002');
assert.equal((await prisma.businessNumberCounter.findUnique({ where: { key } })).current_number, 13n);
await assert.rejects(pool.query('UPDATE business_number_counters SET current_number=0 WHERE key=$1', [key]), error => error.code === '23514');
await assert.rejects(pool.query('DELETE FROM business_number_counters WHERE key=$1', [key]), error => error.code === '23514');
checks.push('twelve_concurrent_commits_unique_deletion_no_reuse_real_rollback');
// Existing same-format occupancy is scanned under the family lock; permanent
// high-water survives both imported-object deletion and process/client restart.
const projectData = { project_no: 'PRJ-2026-9999', name: '合成项目', client_name: '合成客户', owner_employee_id: actor.id };
await prisma.$transaction(async tx => { await reserveProjectNumber(tx, projectData.project_no); await tx.project.create({ data: projectData }); });
const projectNo = await prisma.$transaction(tx => issueBusinessNumber(tx, { family: 'project', now: instant }));
assert.equal(projectNo.documentNumber, 'PRJ-2026-10000');
await prisma.project.delete({ where: { project_no: projectData.project_no } });
const otherClient = new PrismaClient({ adapter: new PrismaPg(pool, { disposeExternalPool: false }) });
try {
assert.equal((await otherClient.$transaction(tx => issueBusinessNumber(tx, { family: 'project', now: instant }))).documentNumber, 'PRJ-2026-10001');
} finally { await otherClient.$disconnect(); }
const imported = await prisma.project.create({ data: { ...projectData, project_no: 'PRJ-2027-0042' } });
assert.equal((await prisma.$transaction(tx => issueBusinessNumber(tx, { family: 'project', now: new Date('2026-12-31T16:00:00Z') }))).documentNumber, 'PRJ-2027-0043');
await prisma.project.delete({ where: { id: imported.id } });
checks.push('occupied_format_expansion_year_boundary_manual_reservation_restart');
const csvPath = path.join(scratch, 'synthetic-project.csv');
writeFileSync(csvPath, `project_no,name,client_name,owner_employee_no
PRJ-2030-0042,synthetic,synthetic,${actor.employee_no}
`);
const dbUrl = `postgresql://synthetic@localhost/numbering_synthetic?host=${encodeURIComponent(pool.options.host)}`;
const cliEnv = { PATH: process.env.PATH, LANG: 'C.UTF-8', DATABASE_URL: dbUrl };
const runCli = (file, args) => {
const result = spawnSync(process.execPath, [path.resolve(__dirname, '../../server/src/scripts', file), ...args], {
cwd: path.resolve(__dirname, '../..'), env: cliEnv, encoding: 'utf8', timeout: 30000,
});
assert.equal(result.status, 0, 'synthetic CLI must succeed');
return JSON.parse(result.stdout);
};
for (const apply of [false, true, true]) {
const result = runCli('import-project-ledgers.js', ['--type', 'projects', '--file', csvPath, '--json', ...(apply ? ['--apply'] : [])]);
assert.equal(result.errorCount, 0);
if (!apply) assert.equal(await prisma.project.count({ where: { project_no: 'PRJ-2030-0042' } }), 0);
}
assert.equal((await prisma.project.findUnique({ where: { project_no: 'PRJ-2030-0042' } })).name, 'synthetic');
await prisma.projectMember.deleteMany({ where: { project: { project_no: 'PRJ-2030-0042' } } });
await prisma.project.delete({ where: { project_no: 'PRJ-2030-0042' } });
assert.equal((await prisma.$transaction(tx => issueBusinessNumber(tx, { family: 'project', now: new Date('2030-07-01T00:00:00Z') }))).documentNumber, 'PRJ-2030-0043');
assert.equal(runCli('manage-business-numbering.js', ['status']).enabled, true);
assert.equal(runCli('manage-business-numbering.js', ['pause', actor.id, 'synthetic-cli-pause', '--writers-compatible']).enabled, false);
assert.equal(runCli('manage-business-numbering.js', ['resume', actor.id, 'synthetic-cli-resume', '--writers-compatible']).enabled, true);
checks.push('real_project_import_dry_run_apply_update_reservation_and_policy_cli');
const assetCompany = await prisma.legalEntity.create({ data: { code: 'SYNTHETIC-NUMBERING', name: '合成资产公司' } });
const assetCenter = await prisma.costCenter.create({ data: { code: 'SYNTHETIC-NUMBERING', name: '合成资产成本', legal_entity_id: assetCompany.id } });
const assetCategory = await prisma.assetCategory.create({ data: { code: 'SYNTHETIC-NUMBERING', name: '合成资产分类' } });
for (const prefix of ['EA.DOC+', '电子🧾']) {
await prisma.assetCard.create({ data: { asset_no: `${prefix}-4321`, name: '合成已有资产',
category_id: assetCategory.id, legal_entity_id: assetCompany.id, cost_center_id: assetCenter.id } });
const issued = await prisma.$transaction(tx => issueBusinessNumber(tx, { family: 'asset_card', prefix, now: instant }));
assert.equal(issued.documentNumber, `${prefix}-4322`);
}
checks.push('existing_custom_asset_prefix_literal_regex_and_unicode_offset');
// A real held shared lock must block the admin's exclusive transition.
let release, acquired;
const held = new Promise(resolve => { release = resolve; });
const ready = new Promise(resolve => { acquired = resolve; });
const holder = prisma.$transaction(async tx => { await resolveNumberingContext(tx, { now: instant }); acquired(); await held; });
await ready;
const pausing = change('pause');
let blocked = false;
try {
for (let i = 0; i < 30; i++) {
const locks = await pool.query("SELECT count(*)::int AS count FROM pg_locks WHERE locktype='advisory' AND classid=1003 AND objid=0 AND NOT granted");
if (locks.rows[0].count > 0) { blocked = true; break; }
await delay(10);
}
assert.ok(blocked, 'policy pause must wait for an in-flight writer');
} finally { release(); await holder; await pausing; }
await change('resume');
checks.push('real_policy_shared_exclusive_lock_boundary');
const mixed = await prisma.$transaction(async tx => {
const ctx = await resolveNumberingContext(tx, { now: instant });
const issued = await issueBusinessNumber(tx, { family: 'payment_batch', ...ctx });
return tx.paymentBatch.create({ data: { batch_no: issued.documentNumber, name: '合成新旧混合批次',
entity_type: 'reimbursement', created_by: actor.id, items: { create: [oldForm, concurrent[0]].map(form => ({
entity_id: form.id, entity_code: form.form_number, entity_title: '合成来源', amount: '1.00', snapshot_json: {},
})) } }, include: { items: true } });
});
assert.equal(mixed.batch_no, 'PB-2026-0001');
assert.deepEqual(mixed.items.map(i => i.entity_code).sort(), [oldForm.form_number, concurrent[0].form_number].sort());
assert.deepEqual(await prisma.paymentBatch.findUnique({ where: { id: oldBatch.id }, include: { items: true } }), oldBatch);
assert.deepEqual(await prisma.reimbursementForm.findUnique({ where: { id: oldForm.id } }), oldForm);
const freshPB = await prisma.$transaction(tx => issueBusinessNumber(tx, { family: 'payment_batch', now: new Date('2026-12-31T16:00:00Z') }));
assert.equal(freshPB.documentNumber, 'PB-2027-0001');
checks.push('old_identifiers_and_item_snapshots_unchanged_mixed_sources');
await require('./company-loan-http-fixture.cjs')(pool, scratch);
const loans = await prisma.companyLoan.findMany({ select: { loan_no: true } });
const repayments = await prisma.loanRepayment.findMany({ select: { repayment_no: true } });
const batches = await prisma.paymentBatch.findMany({ where: { entity_type: 'loan_repayment' }, select: { batch_no: true } });
assert.ok(loans.length > 0 && loans.every(row => /^JK-\d{4}-\d{4,}$/.test(row.loan_no)));
assert.ok(repayments.length > 0 && repayments.every(row => /^HK-\d{4}-\d{4,}$/.test(row.repayment_no)));
assert.ok(batches.length > 0 && batches.every(row => /^PB-\d{4}-\d{4,}$/.test(row.batch_no)));
for (const [rows, column, family, prefix] of [[loans, 'loan_no', 'company_loan', 'JK'], [repayments, 'repayment_no', 'loan_repayment', 'HK']]) {
const groups = new Map();
for (const row of rows) {
const [, year, serial] = row[column].split('-');
if (!groups.has(year)) groups.set(year, []);
groups.get(year).push(Number(serial));
}
for (const [year, serials] of groups) {
assert.deepEqual(serials.sort((a, b) => a - b), Array.from({ length: serials.length }, (_, i) => i + 1));
assert.equal((await prisma.businessNumberCounter.findUnique({ where: { key: `v2:${family}:${prefix}:${year}` } })).current_number, BigInt(serials.length));
}
}
checks.push('active_numbering_real_authenticated_loan_repayment_payment_idempotency');
console.log(JSON.stringify({ status: 'PASS', numberingChecks: checks }));
} finally { await prisma.$disconnect(); }
};