汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
28 行
2.1 KiB
JavaScript
28 行
2.1 KiB
JavaScript
const { lockBudgetPoolsForUpdate, resolveApplicableBudgetPoolByContext } = require('../../../src/services/budgetPool');
|
|
|
|
describe('budget pool write protection', () => {
|
|
it('locks unique pools in stable order with parameters and rejects a root client', async () => {
|
|
const tx = { $queryRaw: vi.fn().mockResolvedValue([]), $transaction: vi.fn() };
|
|
await lockBudgetPoolsForUpdate(tx, ['b', 'a', 'b', null]);
|
|
const query = tx.$queryRaw.mock.calls[0][0];
|
|
expect(query.sql).toMatch(/ORDER BY id FOR NO KEY UPDATE/);
|
|
expect(query.values).toEqual(['a', 'b']);
|
|
await expect(lockBudgetPoolsForUpdate({ ...tx, $connect: vi.fn() }, ['a'])).rejects.toThrow(/transaction/);
|
|
});
|
|
it('does not lock preview reads but refreshes usage after a submission lock', async () => {
|
|
const first = { id: 'pool', used_amount: '0.00' }; const latest = { ...first, used_amount: '60.00' };
|
|
const tx = { $queryRaw: vi.fn().mockResolvedValue([]), budgetPool: { findMany: vi.fn().mockResolvedValueOnce([first]).mockResolvedValueOnce([first]).mockResolvedValueOnce([latest]) } };
|
|
const context = { effectiveDate: new Date('2026-09-19') };
|
|
expect(await resolveApplicableBudgetPoolByContext(tx, context)).toEqual(first);
|
|
expect(tx.$queryRaw).not.toHaveBeenCalled();
|
|
expect(await resolveApplicableBudgetPoolByContext(tx, context, { forUpdate: true })).toEqual(latest);
|
|
expect(tx.budgetPool.findMany.mock.invocationCallOrder[1]).toBeLessThan(tx.$queryRaw.mock.invocationCallOrder[0]);
|
|
expect(tx.$queryRaw.mock.invocationCallOrder[0]).toBeLessThan(tx.budgetPool.findMany.mock.invocationCallOrder[2]);
|
|
});
|
|
it.each([{ records: [] }, { records: [{ id: 'replacement' }] }])('rejects a candidate changed during the wait: %j', async ({ records }) => {
|
|
const tx = { $queryRaw: vi.fn().mockResolvedValue([]), budgetPool: { findMany: vi.fn().mockResolvedValueOnce([{ id: 'old' }]).mockResolvedValueOnce(records) } };
|
|
await expect(resolveApplicableBudgetPoolByContext(tx, { effectiveDate: new Date('2026-09-19') }, { forUpdate: true }))
|
|
.rejects.toMatchObject({ statusCode: 409, details: { reasonCode: 'budget.pool_changed' } });
|
|
});
|
|
});
|