perf: batch paid CMPP accounting and weighted routes

This commit is contained in:
hectorzhao
2026-08-21 10:25:40 +08:00
parent 0176aa6952
commit 487b5282a6
12 changed files with 219 additions and 40 deletions
+28 -3
View File
@@ -13,10 +13,12 @@ function createPrismaMock() {
tenantAccount: {
findMany: jest.fn(),
findUnique: jest.fn().mockImplementation(() => Promise.resolve({ ...accountState })),
findUniqueOrThrow: jest.fn().mockImplementation(() => Promise.resolve({ ...accountState })),
create: jest.fn(),
upsert: jest.fn().mockImplementation(() => Promise.resolve({ ...accountState })),
updateMany: jest.fn().mockImplementation(({ data }) => {
if (data.balanceCents?.increment !== undefined) accountState.balanceCents += data.balanceCents.increment;
else if (data.balanceCents?.decrement !== undefined) accountState.balanceCents -= data.balanceCents.decrement;
accountState.updatedAt = new Date(accountState.updatedAt.getTime() + 1);
return Promise.resolve({ count: 1 });
}),
@@ -31,7 +33,9 @@ function createPrismaMock() {
findMany: jest.fn(),
findFirst: jest.fn(),
findUnique: jest.fn().mockResolvedValue(null),
findUniqueOrThrow: jest.fn().mockImplementation(({ where }) => Promise.resolve({ id: 'tx-charged', idempotencyKey: where.idempotencyKey })),
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: `tx-${data.transactionType}`, ...data })),
createMany: jest.fn().mockResolvedValue({ count: 2 }),
},
rechargeOrder: {
findMany: jest.fn(),
@@ -94,7 +98,7 @@ describe('BillingService', () => {
);
});
it('allows sending only when cash balance plus credit is greater than zero', async () => {
it('allows sending only when cash balance plus credit covers the required amount', async () => {
const prisma = createPrismaMock();
const service = new BillingService(prisma as never);
@@ -102,7 +106,7 @@ describe('BillingService', () => {
expect.objectContaining({ availableAmount: 1000, canSend: true }),
);
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 1001 })).resolves.toEqual(
expect.objectContaining({ availableAmount: 1000, canSend: true }),
expect.objectContaining({ availableAmount: 1000, canSend: false }),
);
await service.updateCreditLimit('tenant-1', { creditCents: -1000, operatorId: 'admin-1' });
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 1 })).resolves.toEqual(
@@ -110,7 +114,7 @@ describe('BillingService', () => {
);
await service.updateCreditLimit('tenant-1', { creditCents: 500, operatorId: 'admin-1' });
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 999999 })).resolves.toEqual(
expect.objectContaining({ availableAmount: 1500, creditCents: 500, canSend: true }),
expect.objectContaining({ availableAmount: 1500, creditCents: 500, canSend: false }),
);
await expect(service.updateCreditLimit('tenant-1', { creditCents: 1.5 })).rejects.toThrow('授信额度最多支持人民币小数点后 4 位');
expect(prisma.operationLog.create).toHaveBeenCalledWith({
@@ -312,6 +316,27 @@ describe('BillingService', () => {
expect(prisma.accountState).toEqual(expect.objectContaining({ balanceCents: 890 }));
});
it('settles a frozen SMS charge with one account lock and an idempotent ledger pair', async () => {
const prisma = createPrismaMock();
const rows = new Map<string, Record<string, unknown>>();
prisma.accountTransaction.findUnique.mockImplementation(({ where }) => Promise.resolve(rows.get(where.idempotencyKey) ?? null));
prisma.accountTransaction.findUniqueOrThrow.mockImplementation(({ where }) => Promise.resolve(rows.get(where.idempotencyKey)));
prisma.accountTransaction.createMany.mockImplementation(({ data }) => {
data.forEach((row: Record<string, unknown>) => rows.set(String(row.idempotencyKey), { id: `tx-${row.transactionType}`, ...row }));
return Promise.resolve({ count: data.length });
});
const service = new BillingService(prisma as never);
const input = { tenantId: 'tenant-1', amountCents: 325, messageId: 'msg-paid-1', taskId: 'task-paid-1' };
const first = await service.settleFrozenCharge(input);
const replay = await service.settleFrozenCharge(input);
expect(first).toEqual(expect.objectContaining({ transactionType: 'charged', amountCents: -325 }));
expect(replay).toEqual(first);
expect(prisma.accountTransaction.createMany).toHaveBeenCalledTimes(1);
expect(prisma.tenantAccount.update).not.toHaveBeenCalled();
});
it('serializes and replays concurrent refunds with one balance mutation', async () => {
const prisma = createPrismaMock();
let transactionChain = Promise.resolve<unknown>(undefined);