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(); } };