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 = Partial, jest.Mock>>; function mockRepo(): MockRepository { 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(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; let itemRepo: MockRepository; let roomExpRepo: MockRepository; let personalExpRepo: MockRepository; let occRepo: MockRepository; let roomRepo: MockRepository; let depositRepo: MockRepository; let dataSource: { transaction: jest.Mock }; beforeEach(async () => { billRepo = mockRepo(); itemRepo = mockRepo(); roomExpRepo = mockRepo(); personalExpRepo = mockRepo(); occRepo = mockRepo(); roomRepo = mockRepo(); depositRepo = mockRepo(); 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); }); 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([ { 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([ { 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([]), ); 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; 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([ { 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([ { 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([]), ); const result = await service.generateBills(PERIOD); expect(result.count).toBe(2); const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record]>; 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([ { 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([ { 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([]), ); const result = await service.generateBills(PERIOD); const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record]>; // 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); }); });