const assert = require("node:assert/strict"); const { readFileSync, writeFileSync } = require("node:fs"); const { createHash, randomUUID } = require("node:crypto"); const { createRequire } = require("node:module"); const path = require("node:path"); const { setTimeout: delay } = require("node:timers/promises"); module.exports = async function verifyRetainedDeletion(context) { const { prisma, pool, origin, tokens, employee, manager, expenseType, makeForm, makeInvoice, bind, post, uploadedRecord, storageDir, passed, root, browser, preview, loginPassword, } = context; const remove = (form, actor) => post(`/reimbursements/${form.id}`, {}, actor, "DELETE"); const get = async (url, actor = employee.id) => { const response = await fetch(`${origin}/api${url}`, { headers: { authorization: `Bearer ${tokens.get(actor)}` }, signal: AbortSignal.timeout(15000), }); return { status: response.status, data: (await response.json()).data }; }; const populate = async (form, invoice) => { assert.equal((await bind(invoice, form)).status, 200); const item = await prisma.reimbursementItem.findFirst({ where: { reimbursement_id: form.id } }); assert.equal((await post(`/reimbursements/${form.id}/items/classify`, { itemIds: [item.id], expenseTypeId: expenseType.id })).status, 200); assert.equal((await post(`/reimbursements/${form.id}/items/${item.id}`, { description: "synthetic retained history" }, null, "PATCH")).status, 200); }; const prepare = async (action) => { const form = await makeForm(); const fileName = `synthetic-delete-${randomUUID()}.pdf`; const bytes = Buffer.concat([readFileSync(path.join(storageDir, uploadedRecord.storage_path)), Buffer.from(`\n% ${fileName}\n`)]); writeFileSync(path.join(storageDir, fileName), bytes); const invoice = await makeInvoice({ storage_path: fileName, file_sha256: createHash("sha256").update(bytes).digest("hex"), file_size: bytes.length, }); await populate(form, invoice); const submitted = await post(`/reimbursements/${form.id}/submit`, {}); assert.equal(submitted.status, 200); const instanceId = submitted.body.data.workflowInstanceId; const task = await prisma.workflowTask.findFirst({ where: { instance_id: instanceId, status: "pending" } }); if (action === "withdraw") { assert.equal((await post(`/reimbursements/${form.id}/withdraw`, { comment: "synthetic withdraw" })).status, 200); } else { assert.equal((await post(`/workflow/tasks/${task.id}/${action}`, { comment: "synthetic correction" }, manager.id)).status, 200); } return { form, invoice, instanceId }; }; const verify = async ({ form, invoice, instanceId }, action) => { const record = await prisma.reimbursementForm.findUnique({ where: { id: form.id } }); assert.equal(record.status, "cancelled"); assert.ok(record.deleted_at); assert.equal(record.deleted_by, employee.id); assert.ok(await prisma.workflowInstance.findUnique({ where: { id: instanceId } })); assert.ok(await prisma.workflowHistory.count({ where: { instance_id: instanceId } })); assert.ok(await prisma.reimbursementItem.count({ where: { reimbursement_id: form.id } })); const detail = await get(`/reimbursements/${form.id}`); assert.equal(detail.status, 200); assert.ok(detail.data.deletedAt); assert.equal(detail.data.invoices.length, 0); if (action !== "withdraw") { const evidence = await prisma.reimbursementAttachment.findFirst({ where: { reimbursement_id: form.id, attachment_type: "invoice" } }); assert.ok(evidence); const response = await fetch(`${origin}/api/reimbursements/${form.id}/attachments/${evidence.id}/file`, { headers: { authorization: `Bearer ${tokens.get(employee.id)}` } }); assert.equal(response.status, 200); const bytes = Buffer.from(await response.arrayBuffer()); assert.equal(createHash("sha256").update(bytes).digest("hex"), evidence.file_sha256); assert.deepEqual(bytes, readFileSync(path.join(storageDir, invoice.storage_path))); } for (const scope of ["own", "all"]) { const listing = await get(`/reimbursements?scope=${scope}&status=cancelled`, scope === "all" ? manager.id : employee.id); assert.equal(listing.status, 200); assert.ok(!listing.data.items.some((item) => item.id === form.id)); } const summary = await get("/reimbursements/stats/summary", manager.id); assert.equal(summary.status, 200); assert.equal(summary.data.byStatus.cancelled.count, 0); const serverRequire = createRequire(path.join(root, "server/package.json")); const ledgerWhere = serverRequire("./src/services/financeLedger").buildFinanceLedgerWhere({ status: "cancelled" }); assert.equal(await prisma.reimbursementForm.count({ where: ledgerWhere }), 0); assert.equal((await remove(form)).status, 200); assert.equal(await prisma.auditLog.count({ where: { action: "REIMBURSEMENT_DELETED", target_id: form.id } }), 1); assert.equal((await post(`/reimbursements/${form.id}/submit`, {})).status, 409); assert.equal((await post(`/reimbursements/${form.id}`, { description: "forbidden" }, null, "PATCH")).status, 409); assert.equal((await bind(await makeInvoice(), form)).status, 409); const replacement = await makeForm(); await populate(replacement, invoice); assert.equal((await remove(form)).status, 200); assert.equal((await prisma.invoicePool.findUnique({ where: { id: invoice.id } })).linked_reimbursement_id, replacement.id); assert.equal((await post(`/reimbursements/${replacement.id}/submit`, {})).status, 200); const historicalDetail = await get(`/reimbursements/${form.id}`); assert.ok(historicalDetail.data.deletedAt); assert.equal(historicalDetail.data.invoices.length, 0); assert.equal(historicalDetail.data.attachments.length, detail.data.attachments.length); assert.ok(historicalDetail.data.processingProgress.stages.every((stage) => !["pending", "current"].includes(stage.state))); }; for (const action of ["reject", "return", "withdraw"]) { const fixture = await prepare(action); assert.equal((await remove(fixture.form, manager.id)).status, 403); assert.equal((await remove(fixture.form)).status, 200); await verify(fixture, action); } passed.push("reject/return/withdraw -> retained deletion -> old history readable -> invoice reused and new form submitted; old record immutable; lists/raw stats/ledger exclude deleted records"); const rollback = await prepare("reject"); await prisma.$executeRawUnsafe("CREATE FUNCTION synthetic_reject_delete_audit() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.action = 'REIMBURSEMENT_DELETED' THEN RAISE EXCEPTION 'synthetic audit failure'; END IF; RETURN NEW; END $$"); await prisma.$executeRawUnsafe("CREATE TRIGGER synthetic_reject_delete_audit BEFORE INSERT ON audit_logs FOR EACH ROW EXECUTE FUNCTION synthetic_reject_delete_audit()"); assert.equal((await remove(rollback.form)).status, 500); assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: rollback.form.id } })).status, "rejected"); assert.equal((await prisma.invoicePool.findUnique({ where: { id: rollback.invoice.id } })).linked_reimbursement_id, rollback.form.id); assert.equal(await prisma.reimbursementAttachment.count({ where: { reimbursement_id: rollback.form.id } }), 0); await prisma.$executeRawUnsafe("DROP TRIGGER synthetic_reject_delete_audit ON audit_logs"); passed.push("audit failure atomically rolls back deletion, invoice release and evidence attachment"); for (const competitor of ["submit", "bind", "delete"]) { const fixture = await prepare("reject"); const blocker = await pool.connect(); const waitForLocks = async (count) => { for (let attempt = 0; attempt < 100; attempt += 1) { const result = await pool.query("SELECT count(*)::int AS count FROM pg_stat_activity WHERE datname = current_database() AND wait_event_type = 'Lock' AND query LIKE '%reimbursement_forms%'"); if (result.rows[0].count >= count) return; await delay(20); } assert.fail("delete concurrency barrier timed out"); }; let deletion; let competing; try { await blocker.query("BEGIN"); await blocker.query("SELECT id FROM reimbursement_forms WHERE id = $1 FOR UPDATE", [fixture.form.id]); deletion = remove(fixture.form); await waitForLocks(1); competing = competitor === "delete" ? remove(fixture.form) : competitor === "submit" ? post(`/reimbursements/${fixture.form.id}/submit`, {}) : bind(await makeInvoice(), fixture.form); await waitForLocks(2); } finally { await blocker.query("ROLLBACK"); blocker.release(); } const results = await Promise.all([deletion, competing]); assert.deepEqual(results.map((result) => result.status), [200, competitor === "delete" ? 200 : 409]); assert.equal(await prisma.auditLog.count({ where: { action: "REIMBURSEMENT_DELETED", target_id: fixture.form.id } }), 1); } passed.push("real PostgreSQL barriers: deletion vs submit/bind/repeated delete has one legal terminal state"); if (browser && preview) { const { expect } = createRequire(path.join(root, "client/package.json"))("@playwright/test"); const uiOrigin = `http://127.0.0.1:${preview.httpServer.address().port}`; for (const mobile of [false, true]) { const fixture = await prepare("reject"); const browserContext = await browser.newContext({ serviceWorkers: "block", viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 960 } }); try { const fingerprint = `synthetic-delete-${mobile}`; await browserContext.addCookies([{ name: "hyb_oa_device_fingerprint", value: fingerprint, url: uiOrigin }]); const login = await browserContext.request.post(`${uiOrigin}/api/auth/login`, { headers: { "x-device-fingerprint": fingerprint }, data: { identifier: employee.email, password: loginPassword } }); assert.equal(login.status(), 200); const page = await browserContext.newPage(); const target = `${uiOrigin}/${mobile ? "mobile/" : ""}reimbursements/${fixture.form.id}`; await page.goto(target); if (!mobile) await page.getByTestId("reimbursement-more-toggle").click(); else await page.getByTestId("mobile-reimbursement-more").click(); await page.getByRole("button", { name: "删除报销单", exact: true }).click(); await expect(page.getByTestId("action-confirm-dialog")).toContainText("审批记录仍保留"); await page.getByTestId("action-confirm-cancel").click(); assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: fixture.form.id } })).deleted_at, null); await page.getByRole("button", { name: "删除报销单", exact: true }).click(); await page.getByTestId("action-confirm-submit").click(); await expect.poll(async () => Boolean((await prisma.reimbursementForm.findUnique({ where: { id: fixture.form.id } })).deleted_at)).toBe(true); await page.goto(target); await expect(page.getByText("已删除", { exact: true }).first()).toBeVisible(); await expect(page.getByTestId("reimbursement-submit")).toHaveCount(0); await expect(page.getByTestId("reimbursement-invoice-add")).toHaveCount(0); await expect(page.getByRole("button", { name: "删除报销单", exact: true })).toHaveCount(0); await verify(fixture, "reject"); } finally { await browserContext.close(); } } passed.push("desktop/mobile real browser: rejected deletion cancel/confirm, persisted historical deep link, no edit/submit/delete actions"); } };