const assert = require("node:assert/strict"); const { execFileSync } = require("node:child_process"); const { randomUUID } = require("node:crypto"); const { readFileSync, readdirSync } = require("node:fs"); const { createRequire } = require("node:module"); const path = require("node:path"); const { setTimeout: delay } = require("node:timers/promises"); const root = path.resolve(__dirname, "../.."); const serverRequire = createRequire(path.join(root, "server/package.json")); const docker = (args, input) => { try { return execFileSync("docker", args, { input, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 60000, }).trim(); } catch (error) { error.dockerOperation = args[0]; error.dockerExit = Number.isInteger(error.status) ? error.status : null; throw error; } }; async function main() { assert.equal(process.env.RUN_ISOLATED_DB_TEST, "1"); const realAuthentication = process.env.RUN_RECEIPT_REAL_AUTH === "1"; let container; let prisma; let pool; const passed = []; try { const password = randomUUID(); container = docker(["run", "-d", "--rm", "--pull=never", "--tmpfs", "/var/lib/postgresql/data", "-p", "127.0.0.1::5432", "-e", `POSTGRES_PASSWORD=${password}`, "postgres:16-alpine"]); let ready = false; for (let attempt = 0; attempt < 30; attempt += 1) { try { docker(["exec", container, "pg_isready", "-U", "postgres"]); ready = true; break; } catch { await delay(1000); } } assert.ok(ready); const migrations = path.join(root, "server/prisma/migrations"); for (const directory of readdirSync(migrations).sort()) { if (directory === "migration_lock.toml") continue; docker(["exec", "-i", container, "psql", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-U", "postgres"], readFileSync(path.join(migrations, directory, "migration.sql"))); } passed.push("all_migrations"); const address = docker(["port", container, "5432/tcp"]); assert.match(address, /^127\.0\.0\.1:\d+$/); process.env.DATABASE_URL = `postgresql://postgres:${password}@${address}/postgres`; ({ prisma, pool } = serverRequire("./src/prismaClient").createPrismaClient()); const { encryptStringOrThrow } = serverRequire("./src/services/crypto"); const { normalizeBankSlip } = serverRequire("./src/services/paymentReceiptMatching"); const { claimVerifiedReceipts, storeReceiptRecognition, queueReceiptRetry } = serverRequire("./src/services/paymentReceiptVerification"); const { saveManualReceiptProposal, reviewManualReceiptProposal, readManualReceiptDetail } = serverRequire("./src/services/paymentReceiptManualPersistence"); const config = { fieldEncryptionKey: Buffer.alloc(32, 7).toString("base64") }; const department = await prisma.department.create({ data: { name: "synthetic-receipt" } }); const employee = await prisma.employee.create({ data: { name: "synthetic", email: "receipt@example.invalid", employee_no: "synthetic-receipt", department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-13"), bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash", } }); async function fixture(reference, manual = false) { const page = { pageNumber: 1, ...normalizeBankSlip(Object.entries({ 小写金额: "10.00", 币别: "人民币", 付款人账号: "000011112222", 收款人账号: "000033334444", 单据状态: "交易成功", 凭证号: reference, }).map(([Name, Value]) => ({ Name, Value }))) }; const batch = await prisma.paymentBatch.create({ data: { batch_no: `SYN-${randomUUID()}`, entity_type: "reimbursement", name: "synthetic", status: "reviewed", created_by: employee.id, payment_account_snapshot_json: { bankInstitutionCode: "CCB", accountNumberEncrypted: encryptStringOrThrow(config, page.payerAccount) }, items: { create: { entity_id: randomUUID(), entity_code: "SYN", entity_title: "synthetic", amount: "10.00", snapshot_json: { payee: { bankInstitutionCode: "CCB", bankAccountEncrypted: encryptStringOrThrow(config, page.payeeAccount) } } } }, }, include: { items: true } }); const attachment = await prisma.paymentBatchReceiptAttachment.create({ data: { payment_batch_id: batch.id, storage_path: "synthetic-no-file", original_name: "synthetic.pdf", mime_type: "application/pdf", file_size: 1, file_sha256: "a".repeat(64), uploaded_by: employee.id, } }); const recognition = { status: "recognized", pagesExpected: 1, pagesProcessed: 1, pages: [page] }; if (manual) { recognition.status = "needs_review"; page.amount = null; page.issues = ["amount_unresolved"]; await prisma.paymentBatchReceiptAttachment.update({ where: { id: attachment.id }, data: { receipt_ocr_state: "needs_review" } }); } await prisma.$transaction((tx) => storeReceiptRecognition(tx, config, { batchId: batch.id, attachmentId: attachment.id, sourceSha256: attachment.file_sha256, expectedVersion: 0, recognition, })); return { batch, attachment, recognition }; } async function claim(batch, abort = false) { return prisma.$transaction(async (tx) => { await tx.$queryRaw`SELECT id FROM payment_batches WHERE id=${batch.id}::uuid FOR UPDATE`; await claimVerifiedReceipts(tx, config, batch); if (abort) throw new Error("synthetic_abort"); }, { timeout: 10000 }); } const first = await fixture("SYN-SAME"); await assert.rejects(claim(first.batch, true), /synthetic_abort/); assert.equal(await prisma.paymentReceiptClaim.count(), 0); passed.push("claim_rolls_back_with_transaction"); await claim(first.batch); await claim(first.batch); assert.equal(await prisma.paymentReceiptClaim.count(), 1); passed.push("idempotent_claim"); const second = await fixture("SYN-SAME"); await assert.rejects(claim(second.batch), { code: "receipt_transaction_reused" }); passed.push("cross_batch_duplicate_blocked"); const concurrentFirst = await fixture("SYN-CONCURRENT"); const concurrentSecond = await fixture("SYN-CONCURRENT"); const outcomes = await Promise.allSettled([claim(concurrentFirst.batch), claim(concurrentSecond.batch)]); assert.equal(outcomes.filter((outcome) => outcome.status === "fulfilled").length, 1); assert.equal(outcomes.find((outcome) => outcome.status === "rejected").reason.code, "receipt_transaction_reused"); passed.push("concurrent_duplicate_blocked"); const voided = await fixture("SYN-VOID"); await prisma.paymentBatchReceiptAttachment.update({ where: { id: voided.attachment.id }, data: { archived_at: new Date() } }); await assert.rejects(claim(voided.batch), { code: "receipt_verification_required" }); passed.push("voided_receipt_blocked"); const verificationCount = await prisma.paymentReceiptVerification.count(); await assert.rejects(prisma.$transaction((tx) => storeReceiptRecognition(tx, config, { batchId: voided.batch.id, attachmentId: voided.attachment.id, sourceSha256: voided.attachment.file_sha256, expectedVersion: 0, recognition: voided.recognition, })), { code: "receipt_unavailable" }); assert.equal(await prisma.paymentReceiptVerification.count(), verificationCount); passed.push("late_result_cannot_revive_voided_receipt"); await assert.rejects(prisma.$transaction((tx) => storeReceiptRecognition(tx, config, { batchId: first.batch.id, attachmentId: first.attachment.id, sourceSha256: "b".repeat(64), expectedVersion: 0, recognition: first.recognition, })), { code: "receipt_source_changed" }); passed.push("changed_source_rejected"); const superseded = await fixture("SYN-SUPERSEDED"); await prisma.paymentBatchReceiptAttachment.update({ where: { id: superseded.attachment.id }, data: { receipt_ocr_version: { increment: 1 } }, }); await assert.rejects(prisma.$transaction((tx) => storeReceiptRecognition(tx, config, { batchId: superseded.batch.id, attachmentId: superseded.attachment.id, expectedVersion: 0, sourceSha256: superseded.attachment.file_sha256, recognition: superseded.recognition, })), { code: "receipt_job_superseded" }); await assert.rejects(claim(superseded.batch), { code: "receipt_verification_required" }); passed.push("superseded_job_and_old_match_blocked"); const { claimReceiptJob, runReceiptOcrOnce } = serverRequire("./src/jobs/paymentReceiptOcrJob"); for (const scenario of ["cash", "travel", "settled"]) { const outside = await fixture(`SYN-SCOPE-${scenario}`); if (scenario === "settled") { await prisma.paymentBatchItem.update({ where: { id: outside.batch.items[0].id }, data: { status: "executed" } }); } else { await prisma.paymentBatch.update({ where: { id: outside.batch.id }, data: scenario === "cash" ? { default_payment_channel: "cash" } : { entity_type: "travel_order" } }); } await prisma.paymentBatchReceiptAttachment.update({ where: { id: outside.attachment.id }, data: { receipt_ocr_state: "queued" }, }); let externalOperations = 0; const forbidden = async () => { externalOperations += 1; throw new Error("synthetic_forbidden"); }; assert.deepEqual(await runReceiptOcrOnce({ prisma, config, readBuffer: forbidden, recognize: forbidden }), { status: "idle" }); assert.equal(externalOperations, 0); const retained = await prisma.paymentBatchReceiptAttachment.findUnique({ where: { id: outside.attachment.id } }); assert.equal(retained.receipt_ocr_state, "legacy"); assert.equal(retained.receipt_ocr_version, 0); passed.push(`worker_${scenario}_retains_evidence_without_file_read_or_cloud_call`); } const queued = await fixture("SYN-QUEUE"); const keyFailure = await fixture("SYN-KEY-FAILURE"); await prisma.paymentBatchReceiptAttachment.update({ where: { id: keyFailure.attachment.id }, data: { receipt_ocr_state: "queued" }, }); let forbiddenReads = 0; const forbiddenRead = async () => { forbiddenReads += 1; throw new Error("synthetic_forbidden"); }; assert.deepEqual(await runReceiptOcrOnce({ prisma, config: {}, readBuffer: forbiddenRead, recognize: forbiddenRead }), { status: "key_failed" }); assert.equal(forbiddenReads, 0); assert.equal((await prisma.paymentBatchReceiptAttachment.findUnique({ where: { id: keyFailure.attachment.id }, })).receipt_ocr_state, "key_failed"); passed.push("missing_key_terminal_state_persisted_without_cloud"); const persistFailure = await fixture("SYN-PERSIST-FAILURE"); await prisma.paymentBatchReceiptAttachment.update({ where: { id: persistFailure.attachment.id }, data: { receipt_ocr_state: "queued" }, }); const faultyPrisma = { $transaction: (callback, options) => prisma.$transaction(async (tx) => { const proxy = new Proxy(tx, { get(target, property) { if (property === "paymentReceiptVerification") return { create: async () => { await tx.$executeRawUnsafe('SELECT 1 / 0'); } }; return target[property]; } }); return callback(proxy); }, options) }; assert.deepEqual(await runReceiptOcrOnce({ prisma: faultyPrisma, config, readBuffer: forbiddenRead, recognize: forbiddenRead }), { status: "system_failed" }); assert.equal((await prisma.paymentBatchReceiptAttachment.findUnique({ where: { id: persistFailure.attachment.id }, })).receipt_ocr_state, "system_failed"); passed.push("aborted_database_transaction_records_failure_in_separate_transaction"); await prisma.paymentBatchReceiptAttachment.update({ where: { id: queued.attachment.id }, data: { receipt_ocr_state: "queued" }, }); const workers = await Promise.all([claimReceiptJob(prisma), claimReceiptJob(prisma)]); assert.equal(workers.filter(Boolean).length, 1); assert.equal(workers.find(Boolean).receipt_ocr_version, 1); await assert.rejects(claim(queued.batch), { code: "receipt_verification_required" }); passed.push("concurrent_worker_single_claim_and_version_invalidation"); const brokenFile = await fixture("SYN-QUEUE-FAIL"); await prisma.paymentBatchReceiptAttachment.update({ where: { id: brokenFile.attachment.id }, data: { receipt_ocr_state: "queued" }, }); let externalCalls = 0; const failedJob = await runReceiptOcrOnce({ prisma, config, readBuffer: async () => Buffer.from("wrong-file"), recognize: async () => { externalCalls += 1; throw new Error("must_not_call"); }, }); assert.equal(failedJob.status, "ocr_failed"); assert.equal(externalCalls, 0); assert.equal((await prisma.paymentBatchReceiptAttachment.findUnique({ where: { id: brokenFile.attachment.id }, })).receipt_ocr_state, "ocr_failed"); passed.push("worker_integrity_failure_persisted_without_external_call"); const failedAttachment = await prisma.paymentBatchReceiptAttachment.findUnique({ where: { id: brokenFile.attachment.id } }); async function retryReceipt() { return prisma.$transaction(async (tx) => { await tx.$queryRaw`SELECT id FROM payment_batches WHERE id=${brokenFile.batch.id}::uuid FOR UPDATE`; return queueReceiptRetry(tx, { batchId: brokenFile.batch.id, attachmentId: failedAttachment.id, expectedVersion: failedAttachment.receipt_ocr_version }); }); } const retries = await Promise.allSettled([retryReceipt(), retryReceipt()]); assert.equal(retries.filter((outcome) => outcome.status === "fulfilled").length, 1); assert.equal(retries.find((outcome) => outcome.status === "rejected").reason.code, "receipt_job_superseded"); passed.push("concurrent_retry_single_version"); const existing = await prisma.paymentReceiptClaim.findFirst(); await assert.rejects(prisma.paymentReceiptClaim.create({ data: { fingerprint: existing.fingerprint, key_id: existing.key_id, item_id: second.batch.items[0].id, verification_id: existing.verification_id, } }), { code: "P2002" }); passed.push("database_unique_constraint"); const reviewer = await prisma.employee.create({ data: { name: "synthetic-reviewer", email: "receipt-reviewer@example.invalid", employee_no: "synthetic-reviewer", department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-13"), bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash", roles: { create: { role_code: "ceo" } }, } }); function requestFor(actor, roleCodes = ["finance"]) { return { id: randomUUID(), headers: {}, employee: { id: actor.id, roleCodes } }; } async function manualFixture(reference) { const context = await fixture(reference, true); await prisma.reimbursementForm.create({ data: { id: context.batch.items[0].entity_id, submitter_id: employee.id, owner_id: employee.id, department_id: department.id, reimbursement_date: new Date("2026-09-14"), status: "approved", total_amount: "10.00", } }); return context; } function proposalInput(context, reference) { return { batchId: context.batch.id, attachmentId: context.attachment.id, expectedVersion: 0, reason: "合成测试更正", pages: [{ pageNumber: 1, itemId: context.batch.items[0].id, fields: Object.entries({ 小写金额: "10.00", 币别: "人民币", 付款人账号: "000011112222", 收款人账号: "000033334444", 单据状态: "交易成功", 凭证号: reference, }).map(([Name, Value]) => ({ Name, Value })) }] }; } const manual = await manualFixture("SYN-MANUAL-SAVE"); const manualInput = proposalInput(manual, "SYN-MANUAL-SAVE"); const auditBefore = await prisma.auditLog.count(); await assert.rejects(prisma.$transaction(async (tx) => { await saveManualReceiptProposal(tx, config, requestFor(employee), manualInput); throw new Error("synthetic_abort"); }), /synthetic_abort/); assert.equal(await prisma.paymentReceiptManualProposal.count(), 0); assert.equal(await prisma.auditLog.count(), auditBefore); assert.equal((await prisma.paymentBatchReceiptAttachment.findUnique({ where: { id: manual.attachment.id } })).receipt_ocr_version, 0); passed.push("manual_proposal_and_audit_rollback"); const saves = await Promise.allSettled([1, 2].map(() => prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor(employee), manualInput)))); assert.equal(saves.filter((outcome) => outcome.status === "fulfilled").length, 1); assert.equal(saves.find((outcome) => outcome.status === "rejected").reason.code, "receipt_job_superseded"); const saved = saves.find((outcome) => outcome.status === "fulfilled").value; assert.equal(saved.status, "pending_review"); assert.equal(saved.version, 1); assert.deepEqual(Object.keys(saved).sort(), ["id", "status", "version"]); const stored = await prisma.paymentReceiptManualProposal.findUnique({ where: { id: saved.id } }); assert.ok(!stored.payload_encrypted.includes("SYN-MANUAL-SAVE")); await assert.rejects(claim(manual.batch), { code: "receipt_verification_required" }); passed.push("manual_concurrent_save_encrypted_and_not_payment"); const reviewInput = { ...manualInput, expectedVersion: saved.version, proposalId: saved.id, decision: "approve", reason: "合成独立核对" }; await assert.rejects(prisma.$transaction((tx) => reviewManualReceiptProposal(tx, config, requestFor(employee, ["ceo"]), reviewInput, true)), { code: "receipt_review_identity_conflict" }); passed.push("manual_self_review_blocked"); const reviews = await Promise.allSettled([1, 2].map(() => prisma.$transaction((tx) => reviewManualReceiptProposal(tx, config, requestFor(reviewer, ["ceo"]), reviewInput, true)))); assert.equal(reviews.filter((outcome) => outcome.status === "fulfilled").length, 1); assert.equal(reviews.find((outcome) => outcome.status === "rejected").reason.code, "receipt_manual_not_reviewable"); assert.equal((await prisma.paymentReceiptManualProposal.findUnique({ where: { id: saved.id } })).status, "approved"); await claim(manual.batch); const manualClaim = await prisma.paymentReceiptClaim.findUnique({ where: { item_id: manual.batch.items[0].id } }); assert.equal(manualClaim.manual_proposal_id, saved.id); assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: manual.batch.items[0].entity_id } })).paid_at, null); passed.push("manual_claim_links_approved_proposal_without_recording_payment"); const reusedOriginal = await manualFixture("SYN-SAME"); await assert.rejects(prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor(employee), proposalInput(reusedOriginal, "SYN-NEW-REFERENCE"))), { code: "receipt_transaction_reused" }); passed.push("manual_original_cross_batch_duplicate_cannot_be_corrected_away"); const changed = await manualFixture("SYN-MANUAL-CHANGED"); const changedInput = proposalInput(changed, "SYN-MANUAL-CHANGED"); const changedSaved = await prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor(employee), changedInput)); await prisma.paymentBatchReceiptAttachment.update({ where: { id: changed.attachment.id }, data: { file_sha256: "b".repeat(64) } }); await assert.rejects(prisma.$transaction((tx) => reviewManualReceiptProposal(tx, config, requestFor(reviewer, ["ceo"]), { ...changedInput, expectedVersion: 1, proposalId: changedSaved.id, decision: "approve", reason: "合成测试" }, true)), { code: "receipt_source_changed" }); passed.push("manual_changed_source_blocks_review"); const rejected = await manualFixture("SYN-MANUAL-REJECT"); const rejectedInput = proposalInput(rejected, "SYN-MANUAL-REJECT"); const rejectedSaved = await prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor(employee), rejectedInput)); const rejectedDecision = await prisma.$transaction((tx) => reviewManualReceiptProposal(tx, config, requestFor(reviewer, ["ceo"]), { ...rejectedInput, expectedVersion: 1, proposalId: rejectedSaved.id, decision: "reject", reason: "合成拒绝" }, true)); assert.equal(rejectedDecision.status, "rejected"); await prisma.$transaction(async (tx) => { await tx.$queryRaw`SELECT id FROM payment_batches WHERE id=${rejected.batch.id}::uuid FOR UPDATE`; await queueReceiptRetry(tx, { batchId: rejected.batch.id, attachmentId: rejected.attachment.id, expectedVersion: 1 }); }); assert.equal((await prisma.paymentReceiptManualProposal.findUnique({ where: { id: rejectedSaved.id } })).status, "rejected"); passed.push("manual_rejection_preserved_and_explicit_retry_available"); const auditFailure = await manualFixture("SYN-AUDIT-FAILURE"); await assert.rejects(prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor({ id: randomUUID() }), proposalInput(auditFailure, "SYN-AUDIT-FAILURE"))), { code: "P2003" }); assert.equal(await prisma.paymentReceiptManualProposal.count({ where: { attachment_id: auditFailure.attachment.id } }), 0); assert.equal((await prisma.paymentBatchReceiptAttachment.findUnique({ where: { id: auditFailure.attachment.id } })).receipt_ocr_version, 0); passed.push("manual_audit_failure_rolls_back_proposal_and_version"); const association = await fixture("SYN-ASSOCIATION"); await prisma.paymentBatchItem.create({ data: { payment_batch_id: association.batch.id, entity_id: randomUUID(), entity_code: "SYN-OTHER", entity_title: "synthetic", amount: "10.00", snapshot_json: association.batch.items[0].snapshot_json, } }); await prisma.paymentBatchReceiptAttachment.update({ where: { id: association.attachment.id }, data: { receipt_ocr_state: "needs_review" } }); const associated = await prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor(employee), { batchId: association.batch.id, attachmentId: association.attachment.id, expectedVersion: 0, pages: [{ pageNumber: 1, itemId: association.batch.items[0].id }], })); assert.equal(associated.status, "association_ready"); assert.equal((await prisma.paymentReceiptManualProposal.findUnique({ where: { id: associated.id } })).reviewed_by, null); passed.push("manual_unchanged_association_needs_no_reentry_or_second_approval"); const lastVersion = await manualFixture("SYN-LAST-VERSION"); await prisma.paymentBatchReceiptAttachment.update({ where: { id: lastVersion.attachment.id }, data: { receipt_ocr_version: 2147483646 } }); await prisma.paymentReceiptVerification.updateMany({ where: { attachment_id: lastVersion.attachment.id }, data: { source_version: 2147483646 } }); const lastInput = { ...proposalInput(lastVersion, "SYN-LAST-VERSION"), expectedVersion: 2147483646 }; const lastSaved = await prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor(employee), lastInput)); assert.equal(lastSaved.version, 2147483647); const lastReview = await prisma.$transaction((tx) => reviewManualReceiptProposal(tx, config, requestFor(reviewer, ["ceo"]), { ...lastInput, proposalId: lastSaved.id, expectedVersion: lastSaved.version, decision: "approve", reason: "合成边界测试" }, true)); assert.equal(lastReview.status, "approved"); passed.push("manual_final_integer_version_still_reviewable_without_overflow"); const detailBefore = await prisma.auditLog.count(); const detail = await prisma.$transaction(async (tx) => { await tx.$executeRaw`SET TRANSACTION READ ONLY`; return readManualReceiptDetail(tx, config, { batchId: manual.batch.id, attachmentId: manual.attachment.id, reviewer: requestFor(reviewer, ["ceo"]).employee, hasReviewPermission: true }); }, { isolationLevel: "RepeatableRead" }); assert.equal(detail.proposal.status, "approved"); assert.equal(detail.proposal.pages[0].amount, "10.00"); assert.ok(!JSON.stringify(detail).includes("000011112222")); assert.ok(!JSON.stringify(detail).includes("000033334444")); assert.equal(await prisma.auditLog.count(), detailBefore); passed.push("manual_detail_readonly_and_accounts_masked"); async function approvedCorrection(originalReference, correctedReference) { const context = await manualFixture(originalReference); const input = proposalInput(context, correctedReference); const proposal = await prisma.$transaction((tx) => saveManualReceiptProposal(tx, config, requestFor(employee), input)); await prisma.$transaction((tx) => reviewManualReceiptProposal(tx, config, requestFor(reviewer, ["ceo"]), { ...input, proposalId: proposal.id, expectedVersion: proposal.version, decision: "approve", reason: "合成独立复核" }, true)); return context; } const aliased = await approvedCorrection("SYN-ORIGINAL-ALIAS", "SYN-CORRECTED-ALIAS"); const aliasesBefore = await prisma.paymentReceiptTransactionAlias.count(); await assert.rejects(claim(aliased.batch, true), /synthetic_abort/); assert.equal(await prisma.paymentReceiptTransactionAlias.count(), aliasesBefore); assert.equal(await prisma.paymentReceiptClaim.count({ where: { item_id: aliased.batch.items[0].id } }), 0); passed.push("manual_claim_and_original_alias_roll_back_together"); await claim(aliased.batch); await claim(aliased.batch); const aliasedClaim = await prisma.paymentReceiptClaim.findUnique({ where: { item_id: aliased.batch.items[0].id } }); assert.equal(await prisma.paymentReceiptTransactionAlias.count({ where: { claim_fingerprint: aliasedClaim.fingerprint } }), 2); const originalReuse = await fixture("SYN-ORIGINAL-ALIAS"); const correctedReuse = await fixture("SYN-CORRECTED-ALIAS"); await assert.rejects(claim(originalReuse.batch), { code: "receipt_transaction_reused" }); await assert.rejects(claim(correctedReuse.batch), { code: "receipt_transaction_reused" }); passed.push("original_and_corrected_transaction_identity_both_unique_and_idempotent"); const identityChanged = await approvedCorrection("SYN-ROLE-ORIGINAL", "SYN-ROLE-CORRECTED"); await prisma.employeeRole.delete({ where: { employee_id_role_code: { employee_id: reviewer.id, role_code: "ceo" } } }); await assert.rejects(claim(identityChanged.batch), { code: "receipt_review_permission_required" }); await prisma.employeeRole.create({ data: { employee_id: reviewer.id, role_code: "ceo" } }); await prisma.reimbursementForm.update({ where: { id: identityChanged.batch.items[0].entity_id }, data: { owner_id: reviewer.id } }); await assert.rejects(claim(identityChanged.batch), { code: "receipt_review_identity_conflict" }); passed.push("manual_execution_rechecks_current_reviewer_role_and_participants"); const concurrentManual = await approvedCorrection("SYN-RACE-ORIGINAL", "SYN-RACE-CORRECTED"); const concurrentAuto = await fixture("SYN-RACE-ORIGINAL"); const aliasRace = await Promise.allSettled([claim(concurrentManual.batch), claim(concurrentAuto.batch)]); assert.equal(aliasRace.filter((result) => result.status === "fulfilled").length, 1); assert.equal(aliasRace.find((result) => result.status === "rejected").reason.code, "receipt_transaction_reused"); passed.push("concurrent_manual_and_auto_original_identity_only_one_claim"); const mixed = await fixture("SYN-MIXED-BANK"); const cashItem = await prisma.paymentBatchItem.create({ data: { payment_batch_id: mixed.batch.id, entity_id: randomUUID(), entity_code: "SYN-CASH", entity_title: "synthetic", amount: "10.00", payment_channel_override: "cash", snapshot_json: {}, } }); const mixedBatch = await prisma.paymentBatch.findUnique({ where: { id: mixed.batch.id }, include: { items: true } }); await claim(mixedBatch); assert.equal(await prisma.paymentReceiptClaim.count({ where: { item_id: cashItem.id } }), 0); assert.equal(await prisma.paymentReceiptClaim.count({ where: { item_id: mixed.batch.items[0].id } }), 1); passed.push("mixed_history_only_bank_item_requires_and_consumes_receipt"); const { FEATURE_FLAG_KEYS } = serverRequire("./src/services/featureFlags"); await prisma.featureFlag.upsert({ where: { flag_key: FEATURE_FLAG_KEYS.paymentBatch }, create: { flag_key: FEATURE_FLAG_KEYS.paymentBatch, override_mode: "force_on", rollout_type: "global" }, update: { override_mode: "force_on", rollout_type: "global" } }); await prisma.employeeRole.create({ data: { employee_id: employee.id, role_code: "finance" } }); const { hasPermission, resolvePermissionsForRoles } = serverRequire("./src/constants/permissions"); const { AppError } = serverRequire("./src/utils/AppError"); const app = serverRequire("fastify")(); app.decorate("prisma", prisma); app.decorate("config", { ...config, paymentBatchEnabled: true, appTimezone: "Asia/Shanghai", erpSyncEnabled: false, emailEnabled: false, imNotifierEnabled: false, appOrigin: "http://127.0.0.1", jwtSecret: randomUUID(), accessTokenTtl: "15m", refreshTtlSeconds: 3600, refreshCookieName: "synthetic_refresh", cookieSecure: false, loginFailMaxAttempts: 5, loginFailWindowSeconds: 300, loginLockSeconds: 60 }); if (realAuthentication) { await app.register(serverRequire("@fastify/cookie")); await app.register(serverRequire("./src/plugins/auth")); await app.register(serverRequire("./src/routes/auth"), { prefix: "/api/auth" }); } else { app.decorate("authenticate", async (request) => { const actorId = request.headers.authorization === "Bearer synthetic-finance" ? employee.id : request.headers.authorization === "Bearer synthetic-reviewer" ? reviewer.id : null; if (!actorId) throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 }); const actor = await prisma.employee.findUnique({ where: { id: actorId }, include: { roles: true } }); request.employee = { ...actor, roleCodes: actor.roles.map((role) => role.role_code) }; }); app.decorate("hasPermission", (actor, permission) => hasPermission(resolvePermissionsForRoles(actor.roleCodes), permission)); } app.setErrorHandler((error, request, reply) => reply.code(error instanceof AppError ? error.statusCode : 500) .send({ error: { code: error.code ?? "unexpected", details: error instanceof AppError ? error.details : undefined } })); await app.register(serverRequire("./src/routes/paymentBatches"), { prefix: "/api" }); let financeHeaders = { authorization: "Bearer synthetic-finance" }; let reviewerHeaders = { authorization: "Bearer synthetic-reviewer" }; async function readyHttp(context) { await prisma.paymentBatch.update({ where: { id: context.batch.id }, data: { reviewed_by: reviewer.id, reviewed_at: new Date(), default_payment_channel: "bank", } }); return `/api/payment-batches/${context.batch.id}`; } try { if (realAuthentication) { const loginPassword = `Synthetic!${randomUUID()}`; const passwordHash = await serverRequire("./src/utils/crypto").hashPassword(loginPassword); await prisma.employee.updateMany({ where: { id: { in: [employee.id, reviewer.id] } }, data: { password_hash: passwordHash, must_change_password: false } }); async function loginActor(actorId) { const actor = await prisma.employee.findUniqueOrThrow({ where: { id: actorId } }); const response = await app.inject({ method: "POST", url: "/api/auth/login", headers: { "x-device-fingerprint": `synthetic-${actorId}` }, payload: { identifier: actor.email, password: loginPassword } }); assert.equal(response.statusCode, 200, "synthetic password login failed"); const token = response.json().data.accessToken; assert.equal(typeof token, "string"); assert.ok(response.cookies.some(cookie => cookie.name === "synthetic_refresh")); return { authorization: `Bearer ${token}` }; } financeHeaders = await loginActor(employee.id); reviewerHeaders = await loginActor(reviewer.id); passed.push("actual_password_login_jwt_and_refresh_cookie"); const forged = await app.inject({ method: "GET", url: `/api/payment-batches/${randomUUID()}/receipt-readiness`, headers: { authorization: "Bearer synthetic-finance" } }); assert.equal(forged.statusCode, 401); passed.push("synthetic_actor_header_rejected_by_real_authentication"); } const httpManual = await manualFixture("SYN-HTTP-ORIGINAL"); const httpBase = await readyHttp(httpManual); const httpProposalUrl = `${httpBase}/receipt-attachments/${httpManual.attachment.id}/manual-verification`; const input = proposalInput(httpManual, "SYN-HTTP-CORRECTED"); const payload = { expectedVersion: input.expectedVersion, reason: input.reason, pages: input.pages }; assert.equal((await app.inject({ method: "POST", url: httpProposalUrl, payload })).statusCode, 401); assert.equal((await app.inject({ method: "POST", url: httpProposalUrl, headers: reviewerHeaders, payload })).statusCode, 403); const submitted = await app.inject({ method: "POST", url: httpProposalUrl, headers: financeHeaders, payload }); assert.equal(submitted.statusCode, 200); const submittedItem = submitted.json().data.item; assert.equal((await app.inject({ method: "GET", url: `${httpBase}/receipt-readiness`, headers: financeHeaders })).json().data.item.ready, false); assert.equal((await app.inject({ method: "POST", url: `${httpBase}/execute`, headers: financeHeaders, payload: {} })).statusCode, 409); assert.equal(await prisma.paymentReceiptClaim.count({ where: { item_id: httpManual.batch.items[0].id } }), 0); const httpReview = await app.inject({ method: "POST", url: `${httpProposalUrl}/${submittedItem.id}/review`, headers: reviewerHeaders, payload: { expectedVersion: submittedItem.version, decision: "approve", reason: "合成HTTP复核" } }); assert.equal(httpReview.statusCode, 200); const claimCount = await prisma.paymentReceiptClaim.count(); const aliasesCount = await prisma.paymentReceiptTransactionAlias.count(); assert.equal((await app.inject({ method: "GET", url: `${httpBase}/receipt-readiness`, headers: financeHeaders })).json().data.item.ready, true); assert.equal(await prisma.paymentReceiptClaim.count(), claimCount); assert.equal(await prisma.paymentReceiptTransactionAlias.count(), aliasesCount); const completed = await app.inject({ method: "POST", url: `${httpBase}/execute`, headers: financeHeaders, payload: {} }); assert.equal(completed.statusCode, 200); assert.equal(completed.json().data.item.status, "executed"); assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: httpManual.batch.items[0].entity_id } })).status, "paid"); assert.equal((await prisma.paymentReceiptClaim.findUnique({ where: { item_id: httpManual.batch.items[0].id } })).manual_proposal_id, submittedItem.id); assert.equal((await app.inject({ method: "POST", url: `${httpBase}/execute`, headers: financeHeaders, payload: {} })).statusCode, 409); passed.push("real_db_http_manual_review_readiness_execution_and_replay_guard"); const rollback = await approvedCorrection("SYN-HTTP-ROLLBACK-OLD", "SYN-HTTP-ROLLBACK-NEW"); const rollbackBase = await readyHttp(rollback); await prisma.reimbursementForm.update({ where: { id: rollback.batch.items[0].entity_id }, data: { total_amount: "11.00" } }); const beforeAliases = await prisma.paymentReceiptTransactionAlias.count(); const refused = await app.inject({ method: "POST", url: `${rollbackBase}/execute`, headers: financeHeaders, payload: {} }); assert.equal(refused.statusCode, 409); assert.equal(refused.json().error.details.payment, "source_amount_changed"); assert.equal(await prisma.paymentReceiptClaim.count({ where: { item_id: rollback.batch.items[0].id } }), 0); assert.equal(await prisma.paymentReceiptTransactionAlias.count(), beforeAliases); assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: rollback.batch.items[0].entity_id } })).status, "approved"); passed.push("real_db_http_source_amount_change_rolls_back_claim_alias_and_payment"); } finally { await app.close(); } if (process.env.RUN_RECEIPT_BROWSER === "1") { assert.ok(realAuthentication); await require("./test-payment-receipt-browser.cjs")({ root, prisma, config, employee, reviewer, manualFixture, serverRequire, passed, }); } } finally { try { if (prisma) await prisma.$disconnect(); } finally { try { if (pool) await pool.end(); } finally { if (container) { try { docker(["rm", "-f", container]); } catch (error) { if (docker(["ps", "-a", "--no-trunc", "--format", "{{.ID}}"]).split("\n").includes(container)) throw error; } } } } } console.log(JSON.stringify({ status: "PASS_ISOLATED_RECEIPT_DB", passed, cleanupComplete: true, productionAccess: false, cloudCalls: false, httpTested: true, authentication: realAuthentication ? "actual_password_login_and_jwt" : "synthetic_actor_fixture", browserTested: process.env.RUN_RECEIPT_BROWSER === "1" })); } main().catch((error) => { const location = String(error.stack ?? "").match(/test-payment-receipt-db\.cjs:(\d+):(\d+)/); console.error(JSON.stringify({ status: "ISOLATED_RECEIPT_DB_FAILED", dockerOperation: error.dockerOperation ?? null, dockerExit: error.dockerExit ?? null, code: /^[A-Za-z0-9_]+$/.test(error.code ?? "") ? error.code : null, actualCode: /^[A-Za-z0-9_]+$/.test(error.actual?.code ?? "") ? error.actual.code : null, actualName: /^[A-Za-z0-9_]+$/.test(error.actual?.name ?? "") ? error.actual.name : null, databaseCode: /^[A-Za-z0-9_]+$/.test(error.actual?.meta?.code ?? "") ? error.actual.meta.code : null, line: location ? Number(location[1]) : null })); process.exitCode = 1; });