fix: simplify balance billing and govern operation logs

This commit is contained in:
hectorzhao
2026-07-14 17:40:32 +08:00
parent f35691f185
commit 28dad93e3e
40 changed files with 740 additions and 395 deletions
+16 -29
View File
@@ -1,21 +1,15 @@
import { BillingService } from './billing.service';
function createPrismaMock() {
const accountState = { tenantId: 'tenant-1', balanceCents: 1000, smsUnits: 20, creditCents: 200 };
const accountState = { tenantId: 'tenant-1', balanceCents: 1000 };
return {
accountState,
billingPlan: {
findMany: jest.fn(),
create: jest.fn(),
findUnique: jest.fn(),
},
tenantAccount: {
findMany: jest.fn(),
create: jest.fn(),
upsert: jest.fn().mockImplementation(() => Promise.resolve({ ...accountState })),
update: jest.fn().mockImplementation(({ data }) => {
accountState.balanceCents = data.balanceCents;
accountState.smsUnits = data.smsUnits;
return Promise.resolve({ ...accountState });
}),
},
@@ -68,38 +62,33 @@ describe('BillingService', () => {
);
});
it('checks balance, credit, and package units before sending', async () => {
it('checks only the cash balance before sending', async () => {
const prisma = createPrismaMock();
const service = new BillingService(prisma as never);
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 1100, smsUnits: 20 })).resolves.toEqual(
expect.objectContaining({ availableAmount: 1200, availableSmsUnits: 20, canSend: true }),
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 5 })).resolves.toEqual(
expect.objectContaining({ availableAmount: 1000, canSend: true }),
);
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 1300, smsUnits: 20 })).resolves.toEqual(
expect.objectContaining({ canSend: false }),
);
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 100, smsUnits: 21 })).resolves.toEqual(
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 1001 })).resolves.toEqual(
expect.objectContaining({ canSend: false }),
);
});
it('creates recharge orders and account transactions from plans', async () => {
it('creates cash recharge orders and account transactions without plans', async () => {
const prisma = createPrismaMock();
prisma.billingPlan.findUnique.mockResolvedValue({ id: 'plan-1', priceCents: 500, smsUnits: 100 });
const service = new BillingService(prisma as never);
const order = await service.createRechargeOrder({ tenantId: 'tenant-1', planId: 'plan-1', remark: 'manual top up' });
const order = await service.createRechargeOrder({ tenantId: 'tenant-1', amountCents: 500, remark: 'manual top up' });
expect(order).toEqual(expect.objectContaining({ amountCents: 500, smsUnits: 100, status: 'paid' }));
expect(order).toEqual(expect.objectContaining({ amountCents: 500, status: 'paid' }));
expect(prisma.tenantAccount.update).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1' },
data: { balanceCents: 1500, smsUnits: 120 },
data: { balanceCents: 1500 },
});
expect(prisma.accountTransaction.create).toHaveBeenCalledWith({
data: expect.objectContaining({
transactionType: 'recharge',
amountCents: 500,
smsUnits: 100,
balanceAfter: 1500,
relatedType: 'recharge_order',
relatedId: 'order-1',
@@ -114,7 +103,6 @@ describe('BillingService', () => {
const order = await service.createManualRecharge({
tenantId: 'tenant-1',
amountCents: 2000,
smsUnits: 0,
operatorId: 'admin-1',
remark: '线下转账人工充值',
});
@@ -175,7 +163,6 @@ describe('BillingService', () => {
const order = await service.createManualRecharge({
tenantId: 'tenant-1',
amountCents: -300,
smsUnits: 0,
operatorId: 'admin-1',
remark: '人工冲正',
});
@@ -183,7 +170,7 @@ describe('BillingService', () => {
expect(order).toEqual(expect.objectContaining({ amountCents: -300, payMethod: 'manual_topup', status: 'paid' }));
expect(prisma.tenantAccount.update).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1' },
data: { balanceCents: 700, smsUnits: 20 },
data: { balanceCents: 700 },
});
expect(prisma.accountTransaction.create).toHaveBeenCalledWith({
data: expect.objectContaining({
@@ -205,11 +192,11 @@ describe('BillingService', () => {
const prisma = createPrismaMock();
const service = new BillingService(prisma as never);
await service.freeze({ tenantId: 'tenant-1', amountCents: 100, smsUnits: 2, relatedType: 'sms_batch_task', relatedId: 'task-1' });
await service.charge({ tenantId: 'tenant-1', amountCents: 50, smsUnits: 1, relatedType: 'sms_message_record', relatedId: 'msg-1' });
await service.release({ tenantId: 'tenant-1', amountCents: 25, smsUnits: 1 });
await service.refund({ tenantId: 'tenant-1', amountCents: 10, smsUnits: 1 });
await service.adjust({ tenantId: 'tenant-1', amountCents: 5, smsUnits: 0 });
await service.freeze({ tenantId: 'tenant-1', amountCents: 100, relatedType: 'sms_batch_task', relatedId: 'task-1' });
await service.charge({ tenantId: 'tenant-1', amountCents: 50, relatedType: 'sms_message_record', relatedId: 'msg-1' });
await service.release({ tenantId: 'tenant-1', amountCents: 25 });
await service.refund({ tenantId: 'tenant-1', amountCents: 10 });
await service.adjust({ tenantId: 'tenant-1', amountCents: 5 });
expect(prisma.accountTransaction.create.mock.calls.map(([arg]) => arg.data.transactionType)).toEqual([
'frozen',
@@ -218,7 +205,7 @@ describe('BillingService', () => {
'refunded',
'adjusted',
]);
expect(prisma.accountState).toEqual(expect.objectContaining({ balanceCents: 890, smsUnits: 19 }));
expect(prisma.accountState).toEqual(expect.objectContaining({ balanceCents: 890 }));
});
it('creates SMS billing records linked to message and task identifiers', async () => {