import { BadRequestException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { DateTime } from '../../../common/value-objects/date-time/date-time'; import { Decimal } from '../../../common/value-objects/decimal/decimal'; import { Status } from '../../../common/value-objects/status/status'; import { SalesInvoicesService } from '../sales-invoices/sales-invoices.service'; import { SALES_PAYMENT_STATUSES } from '../shared/sales-fields'; import type { SalesPayment } from './sales-payment'; import { SalesPaymentsRepository } from './sales-payments.repository'; import { SalesPaymentsService } from './sales-payments.service'; describe('SalesPaymentsService', () => { let service: SalesPaymentsService; const repository: jest.Mocked< Pick< SalesPaymentsRepository, | 'list' | 'findById' | 'create' | 'createMany' | 'update' | 'updateStatus' | 'bulkUpdateStatus' | 'delete' | 'bulkDelete' > > = { list: jest.fn(), findById: jest.fn(), create: jest.fn(), createMany: jest.fn(), update: jest.fn(), updateStatus: jest.fn(), bulkUpdateStatus: jest.fn(), delete: jest.fn(), bulkDelete: jest.fn(), }; const salesInvoicesService = { findById: jest.fn(), getTotals: jest.fn(), applyPaymentEffects: jest.fn(), }; const now = DateTime.fromUnixMs(1_700_000_000_000); const sample: SalesPayment = { id: 'sp-1', code: 'SP-20260824-0001', date: now, notes: null, invoices: [ { id: 'alloc-1', invoiceId: 'si-1', invoice: { id: 'si-1', code: 'SI-1' }, amount: Decimal.create('10000'), }, ], images: [], status: Status.create('draft', SALES_PAYMENT_STATUSES), createdAt: now, updatedAt: now, createdBy: 'user-1', updatedBy: 'user-1', createdByUser: { id: 'user-1', username: 'admin' }, updatedByUser: { id: 'user-1', username: 'admin' }, }; beforeEach(async () => { jest.clearAllMocks(); salesInvoicesService.findById.mockResolvedValue({ id: 'si-1' }); const moduleRef: TestingModule = await Test.createTestingModule({ providers: [ SalesPaymentsService, { provide: SalesPaymentsRepository, useValue: repository }, { provide: SalesInvoicesService, useValue: salesInvoicesService }, ], }).compile(); service = moduleRef.get(SalesPaymentsService); }); it('create persists invoice allocations', async () => { repository.create.mockResolvedValue(sample); const result = await service.create({ date: '2026-08-24T10:00:00+07:00', invoices: [{ invoiceId: 'si-1', amount: '10000' }], userId: 'user-1', }); expect(result.code).toBe('SP-20260824-0001'); expect(repository.create).toHaveBeenCalled(); const [arg] = repository.create.mock.calls[0]; expect(arg.invoices[0]?.invoiceId).toBe('si-1'); expect(arg.invoices[0]?.amount.value).toBe('10000.0000'); }); it('update rejects status on PATCH', async () => { await expect( service.update('sp-1', { status: 'approved', userId: 'user-1' }), ).rejects.toBeInstanceOf(BadRequestException); }); it('findById throws when missing', async () => { repository.findById.mockResolvedValue(null); await expect(service.findById('missing')).rejects.toBeInstanceOf( NotFoundException, ); }); it('approving a payment recomputes referenced invoices', async () => { repository.findById.mockResolvedValue({ ...sample, status: Status.create('pending', SALES_PAYMENT_STATUSES), }); salesInvoicesService.getTotals.mockResolvedValue({ total: Decimal.create('25000'), paid: Decimal.create('0'), balance: Decimal.create('25000'), }); repository.updateStatus.mockResolvedValue({ ...sample, status: Status.create('approved', SALES_PAYMENT_STATUSES), }); await service.updateStatus('sp-1', 'approved', 'user-1'); expect(salesInvoicesService.applyPaymentEffects).toHaveBeenCalledWith( 'si-1', 'user-1', ); }); it('rejects approve when allocation would exceed the invoice total', async () => { repository.findById.mockResolvedValue({ ...sample, status: Status.create('pending', SALES_PAYMENT_STATUSES), }); salesInvoicesService.getTotals.mockResolvedValue({ total: Decimal.create('5000'), paid: Decimal.create('0'), balance: Decimal.create('5000'), }); await expect( service.updateStatus('sp-1', 'approved', 'user-1'), ).rejects.toBeInstanceOf(BadRequestException); expect(repository.updateStatus).not.toHaveBeenCalled(); }); });