test: add first-version coverage
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { BillingService } from './billing.service';
|
||||
|
||||
function createPrismaMock() {
|
||||
const accountState = { tenantId: 'tenant-1', balanceCents: 1000, smsUnits: 20, creditCents: 200 };
|
||||
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 });
|
||||
}),
|
||||
},
|
||||
accountTransaction: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: `tx-${data.transactionType}`, ...data })),
|
||||
},
|
||||
rechargeOrder: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'order-1', ...data })),
|
||||
},
|
||||
smsBillingRecord: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'bill-1', ...data })),
|
||||
},
|
||||
billingRule: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('BillingService', () => {
|
||||
it('estimates SMS cost using 70/67 billing units', () => {
|
||||
const service = new BillingService(createPrismaMock() as never);
|
||||
|
||||
expect(
|
||||
service.estimateSmsCost({ tenantId: 'tenant-1', content: 'a'.repeat(70), phoneCount: 3, unitPrice: 5 }),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
contentLength: 70,
|
||||
billingUnitsPerMessage: 1,
|
||||
totalBillingUnits: 3,
|
||||
amountCents: 15,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
service.estimateSmsCost({ tenantId: 'tenant-1', content: 'a'.repeat(71), phoneCount: 2, unitPrice: 5 }),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
contentLength: 71,
|
||||
billingUnitsPerMessage: 2,
|
||||
totalBillingUnits: 4,
|
||||
amountCents: 20,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('checks balance, credit, and package units 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: 1300, smsUnits: 20 })).resolves.toEqual(
|
||||
expect.objectContaining({ canSend: false }),
|
||||
);
|
||||
await expect(service.checkAccount({ tenantId: 'tenant-1', amountCents: 100, smsUnits: 21 })).resolves.toEqual(
|
||||
expect.objectContaining({ canSend: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it('creates recharge orders and account transactions from 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' });
|
||||
|
||||
expect(order).toEqual(expect.objectContaining({ amountCents: 500, smsUnits: 100, status: 'paid' }));
|
||||
expect(prisma.tenantAccount.update).toHaveBeenCalledWith({
|
||||
where: { tenantId: 'tenant-1' },
|
||||
data: { balanceCents: 1500, smsUnits: 120 },
|
||||
});
|
||||
expect(prisma.accountTransaction.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
transactionType: 'recharge',
|
||||
amountCents: 500,
|
||||
smsUnits: 100,
|
||||
balanceAfter: 1500,
|
||||
relatedType: 'recharge_order',
|
||||
relatedId: 'order-1',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('writes freeze, charge, release, refund, and adjustment transactions', async () => {
|
||||
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 });
|
||||
|
||||
expect(prisma.accountTransaction.create.mock.calls.map(([arg]) => arg.data.transactionType)).toEqual([
|
||||
'frozen',
|
||||
'charged',
|
||||
'released',
|
||||
'refunded',
|
||||
'adjusted',
|
||||
]);
|
||||
expect(prisma.accountState).toEqual(expect.objectContaining({ balanceCents: 890, smsUnits: 19 }));
|
||||
});
|
||||
|
||||
it('creates SMS billing records linked to message and task identifiers', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new BillingService(prisma as never);
|
||||
|
||||
const record = await service.createSmsBillingRecord({
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
taskId: 'task-1',
|
||||
messageId: 'msg-1',
|
||||
phoneNumber: '13800000001',
|
||||
content: 'a'.repeat(134),
|
||||
unitPrice: 4,
|
||||
});
|
||||
|
||||
expect(record).toEqual(
|
||||
expect.objectContaining({
|
||||
tenantId: 'tenant-1',
|
||||
taskId: 'task-1',
|
||||
messageId: 'msg-1',
|
||||
billingUnits: 2,
|
||||
amountCents: 8,
|
||||
billingStatus: 'estimated',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user