Files
gongxue-base/apps/server/src/bills/bills.service.spec.ts

228 lines
8.7 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { BillsService } from './bills.service';
import { Bill } from '../entities/bill.entity';
import { BillItem } from '../entities/bill-item.entity';
import { RoomExpense } from '../entities/room-expense.entity';
import { PersonalExpense } from '../entities/personal-expense.entity';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { Deposit } from '../entities/deposit.entity';
type MockRepository<T> = Partial<Record<keyof Repository<T>, jest.Mock>>;
function mockRepo<T>(): MockRepository<T> {
return {
find: jest.fn().mockResolvedValue([]),
save: jest.fn().mockImplementation((entity) => Promise.resolve({ id: 1, ...entity })),
create: jest.fn().mockImplementation((entity) => entity),
createQueryBuilder: jest.fn().mockReturnValue({
delete: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 1 }),
}),
findOne: jest.fn().mockResolvedValue(null),
remove: jest.fn().mockResolvedValue(undefined),
};
}
function mockQueryBuilder<T>(results: T[]) {
return {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(results),
};
}
describe('BillsService — generateBills', () => {
let service: BillsService;
let billRepo: MockRepository<Bill>;
let itemRepo: MockRepository<BillItem>;
let roomExpRepo: MockRepository<RoomExpense>;
let personalExpRepo: MockRepository<PersonalExpense>;
let occRepo: MockRepository<Occupancy>;
let roomRepo: MockRepository<Room>;
let depositRepo: MockRepository<Deposit>;
let dataSource: { transaction: jest.Mock };
beforeEach(async () => {
billRepo = mockRepo<Bill>();
itemRepo = mockRepo<BillItem>();
roomExpRepo = mockRepo<RoomExpense>();
personalExpRepo = mockRepo<PersonalExpense>();
occRepo = mockRepo<Occupancy>();
roomRepo = mockRepo<Room>();
depositRepo = mockRepo<Deposit>();
dataSource = { transaction: jest.fn() };
const module: TestingModule = await Test.createTestingModule({
providers: [
BillsService,
{ provide: getRepositoryToken(Bill), useValue: billRepo },
{ provide: getRepositoryToken(BillItem), useValue: itemRepo },
{ provide: getRepositoryToken(RoomExpense), useValue: roomExpRepo },
{ provide: getRepositoryToken(PersonalExpense), useValue: personalExpRepo },
{ provide: getRepositoryToken(Occupancy), useValue: occRepo },
{ provide: getRepositoryToken(Room), useValue: roomRepo },
{ provide: getRepositoryToken(Deposit), useValue: depositRepo },
{ provide: DataSource, useValue: dataSource },
],
}).compile();
service = module.get<BillsService>(BillsService);
});
const PERIOD = { periodStart: '2026-06-01', periodEnd: '2026-06-30' };
it('long-term occupancy → individual bill with monthlyRate', async () => {
// One room expense for room 1
(roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<RoomExpense>([
{
id: 1, roomId: 1, expenseType: 'utility',
amount: '500' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
} as RoomExpense,
]),
);
// One long-term occupancy in room 1
(occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<Occupancy>([
{
id: 1, studentId: 10, roomId: 1,
billingStartDate: '2026-06-01', billingEndDate: '2026-06-30',
rentalType: 'long',
room: { roomNumber: '101', monthlyRate: '800' as unknown as number } as Room,
} as Occupancy,
]),
);
// No personal expenses
(personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<PersonalExpense>([]),
);
const result = await service.generateBills(PERIOD);
expect(result.count).toBe(1);
const savedCalls = (billRepo.save as jest.Mock).mock.calls;
expect(savedCalls.length).toBe(1);
// The long-term bill should carry the monthly rate (shared = 800)
const billData = savedCalls[0][0] as Record<string, unknown>;
expect(Number(billData.totalAmount)).toBe(800);
expect(Number(billData.sharedAmount)).toBe(800);
expect(billData.studentId).toBe(10);
});
it('short-term occupancy → per-diem allocation pool', async () => {
// One room expense for room 1
(roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<RoomExpense>([
{
id: 1, roomId: 1, expenseType: 'utility',
amount: '300' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
} as RoomExpense,
]),
);
// Two short-term occupancies: student 10 (10 days), student 11 (20 days) → 30 total
(occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<Occupancy>([
{
id: 1, studentId: 10, roomId: 1,
billingStartDate: '2026-06-01', billingEndDate: '2026-06-10',
rentalType: 'short', room: undefined,
} as Occupancy,
{
id: 2, studentId: 11, roomId: 1,
billingStartDate: '2026-06-01', billingEndDate: '2026-06-20',
rentalType: 'short', room: undefined,
} as Occupancy,
]),
);
(personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<PersonalExpense>([]),
);
const result = await service.generateBills(PERIOD);
expect(result.count).toBe(2);
const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
const student10Bill = savedCalls.find((c) => c[0].studentId === 10);
const student11Bill = savedCalls.find((c) => c[0].studentId === 11);
// Student 10: 10/30 ≈ 100.00
expect(Number(student10Bill![0].sharedAmount)).toBeCloseTo(100, 0);
// Student 11: 20/30 ≈ 200.00
expect(Number(student11Bill![0].sharedAmount)).toBeCloseTo(200, 0);
// Total bill amounts sum to the expense
expect(
Number(student10Bill![0].totalAmount) + Number(student11Bill![0].totalAmount),
).toBeCloseTo(300, 0);
});
it('mixed → long-term get individual bills, short-term share expenses', async () => {
// Room 1: two expenses
(roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<RoomExpense>([
{
id: 1, roomId: 1, expenseType: 'utility',
amount: '200' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
} as RoomExpense,
{
id: 2, roomId: 1, expenseType: 'water',
amount: '100' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
} as RoomExpense,
]),
);
// Mixed: one long-term (student 10) + two short-term (11, 12)
(occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<Occupancy>([
{
id: 1, studentId: 10, roomId: 1,
billingStartDate: '2026-06-01', billingEndDate: '2026-06-30',
rentalType: 'long',
room: { roomNumber: '101', monthlyRate: '600' as unknown as number } as Room,
} as Occupancy,
{
id: 2, studentId: 11, roomId: 1,
billingStartDate: '2026-06-01', billingEndDate: '2026-06-15',
rentalType: 'short', room: undefined,
} as Occupancy,
{
id: 3, studentId: 12, roomId: 1,
billingStartDate: '2026-06-01', billingEndDate: '2026-06-15',
rentalType: 'short', room: undefined,
} as Occupancy,
]),
);
(personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
mockQueryBuilder<PersonalExpense>([]),
);
const result = await service.generateBills(PERIOD);
const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
// All 3 students get a bill
expect(result.count).toBe(3);
expect(savedCalls.length).toBe(3);
// Long-term student 10 should have shared = monthlyRate (600)
const longBill = savedCalls.find((c) => c[0].studentId === 10)!;
expect(Number(longBill[0].sharedAmount)).toBe(600);
// Short-term students 11 & 12 split 300 in expenses equally (15 days each)
const short11 = savedCalls.find((c) => c[0].studentId === 11)!;
const short12 = savedCalls.find((c) => c[0].studentId === 12)!;
expect(Number(short11[0].sharedAmount)).toBeCloseTo(150, 0);
expect(Number(short12[0].sharedAmount)).toBeCloseTo(150, 0);
});
});