120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
import { WalletsService } from './wallets.service';
|
|
import { Bill } from '../entities/bill.entity';
|
|
|
|
const manager = (walletBalance: number) => {
|
|
const wallet = { id: 1, studentId: 10, balance: walletBalance };
|
|
const saved: any[] = [];
|
|
return {
|
|
wallet,
|
|
saved,
|
|
value: {
|
|
findOne: jest.fn(async () => wallet),
|
|
findOneByOrFail: jest.fn(async () => wallet),
|
|
save: jest.fn(async (value: any) => { saved.push(value); return value; }),
|
|
create: jest.fn((_entity: unknown, value: unknown) => value),
|
|
createQueryBuilder: jest.fn(),
|
|
},
|
|
};
|
|
};
|
|
|
|
describe('WalletsService payment rules', () => {
|
|
const service = new WalletsService({} as any, {} as any, {} as any, {} as any);
|
|
|
|
it('partially pays a bill when balance is insufficient', async () => {
|
|
const ctx = manager(40);
|
|
const bill = { id: 9, studentId: 10, totalAmount: 100, paidAmount: 0, outstandingAmount: 100, status: 'unpaid' } as Bill;
|
|
const result = await service.debitBill(ctx.value as any, bill, 1);
|
|
expect(result.status).toBe('partially_paid');
|
|
expect(Number(result.paidAmount)).toBe(40);
|
|
expect(Number(result.outstandingAmount)).toBe(60);
|
|
expect(Number(ctx.wallet.balance)).toBe(0);
|
|
expect(ctx.saved.some((row) => row.type === 'bill_payment' && Number(row.amount) === -40)).toBe(true);
|
|
});
|
|
|
|
it('marks a bill paid when balance covers it', async () => {
|
|
const ctx = manager(120);
|
|
const bill = { id: 9, studentId: 10, totalAmount: 100, paidAmount: 0, outstandingAmount: 100, status: 'unpaid' } as Bill;
|
|
const result = await service.debitBill(ctx.value as any, bill);
|
|
expect(result.status).toBe('paid');
|
|
expect(Number(result.outstandingAmount)).toBe(0);
|
|
expect(Number(ctx.wallet.balance)).toBe(20);
|
|
});
|
|
|
|
it('refunds paid amount and cancels the bill', async () => {
|
|
const ctx = manager(10);
|
|
const bill = { id: 9, studentId: 10, totalAmount: 100, paidAmount: 40, outstandingAmount: 60, status: 'partially_paid' } as Bill;
|
|
const result = await service.refundBill(ctx.value as any, bill, '录入错误', 1);
|
|
expect(result.status).toBe('cancelled');
|
|
expect(Number(ctx.wallet.balance)).toBe(50);
|
|
expect(ctx.saved.some((row) => row.type === 'bill_refund' && Number(row.amount) === 40)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('WalletsService financial boundaries', () => {
|
|
it('caps a debit at total minus already paid even when outstandingAmount is stale', async () => {
|
|
const ctx = manager(50);
|
|
const service = new WalletsService({} as any, {} as any, {} as any, {} as any);
|
|
const bill = {
|
|
id: 10,
|
|
studentId: 10,
|
|
totalAmount: 100,
|
|
paidAmount: 90,
|
|
outstandingAmount: 100,
|
|
status: 'partially_paid',
|
|
} as Bill;
|
|
|
|
await service.debitBill(ctx.value as any, bill);
|
|
|
|
expect(bill).toMatchObject({ paidAmount: 100, outstandingAmount: 0, status: 'paid' });
|
|
expect(ctx.wallet.balance).toBe(40);
|
|
expect(ctx.saved.some((row) => row.type === 'bill_payment' && row.amount === -10)).toBe(true);
|
|
});
|
|
|
|
it('caps a refund at the bill total when paidAmount is corrupt', async () => {
|
|
const ctx = manager(10);
|
|
const service = new WalletsService({} as any, {} as any, {} as any, {} as any);
|
|
const bill = {
|
|
id: 11,
|
|
studentId: 10,
|
|
totalAmount: 100,
|
|
paidAmount: 150,
|
|
outstandingAmount: 0,
|
|
status: 'paid',
|
|
} as Bill;
|
|
|
|
await service.refundBill(ctx.value as any, bill, '冲正');
|
|
|
|
expect(ctx.wallet.balance).toBe(110);
|
|
expect(ctx.saved.some((row) => row.type === 'bill_refund' && row.amount === 100)).toBe(true);
|
|
});
|
|
|
|
it('does not issue a second refund for an already cancelled bill', async () => {
|
|
const ctx = manager(10);
|
|
const service = new WalletsService({} as any, {} as any, {} as any, {} as any);
|
|
const bill = {
|
|
id: 12,
|
|
studentId: 10,
|
|
totalAmount: 100,
|
|
paidAmount: 100,
|
|
outstandingAmount: 0,
|
|
status: 'cancelled',
|
|
} as Bill;
|
|
|
|
await service.refundBill(ctx.value as any, bill, '重复取消');
|
|
|
|
expect(ctx.wallet.balance).toBe(10);
|
|
expect(ctx.saved).toHaveLength(0);
|
|
});
|
|
|
|
it('rejects an amount that rounds to zero before opening a transaction', async () => {
|
|
const dataSource = { transaction: jest.fn() };
|
|
const service = new WalletsService({} as any, {} as any, {} as any, dataSource as any);
|
|
|
|
await expect(service.changeBalance({ studentId: 1, amount: 0.004, type: 'adjustment' })).rejects.toBeInstanceOf(
|
|
BadRequestException,
|
|
);
|
|
expect(dataSource.transaction).not.toHaveBeenCalled();
|
|
});
|
|
});
|