From 880c2a701875b7a5954070d118e9da68a754dfd3 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Fri, 31 Jul 2026 10:47:16 +0000 Subject: [PATCH] test(transactions): add unit tests for TransactionsService --- src/transactions/transactions.service.spec.ts | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/transactions/transactions.service.spec.ts diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts new file mode 100644 index 0000000..5609beb --- /dev/null +++ b/src/transactions/transactions.service.spec.ts @@ -0,0 +1,172 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TransactionsService } from './transactions.service'; +import { AuditService } from '../audit/audit.service'; +import { OutboxService } from '../events/outbox.service'; + +jest.mock('../db/client', () => ({ + db: { + query: { + transactions: { + findMany: jest.fn(), + findFirst: jest.fn(), + }, + }, + }, +})); + +import { db } from '../db/client'; + +const mockAuditService = { record: jest.fn().mockResolvedValue(undefined) }; +const mockOutboxService = { + record: jest.fn().mockResolvedValue(undefined), + withTransaction: jest.fn().mockImplementation(async (fn: (tx: unknown) => Promise) => fn({})), +}; + +const fakeSession = { + tenantId: 'tenant-1', + workspaceId: 'ws-1', + userId: 'user-1', + correlationId: 'corr-1', +}; + +const fakeTx = { + id: 'tx-uuid-1', + tenantId: 'tenant-1', + organizationId: 'org-uuid-1', + type: 'invoice', + amountMinorUnits: '150000', + currency: 'EUR', + transactionDate: new Date('2026-01-15'), + evidenceStatus: 'missing', + source: 'manual', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + version: 1, +}; + +describe('TransactionsService', () => { + let service: TransactionsService; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TransactionsService, + { provide: AuditService, useValue: mockAuditService }, + { provide: OutboxService, useValue: mockOutboxService }, + ], + }).compile(); + service = module.get(TransactionsService); + }); + + describe('list', () => { + it('returns transactions for the tenant', async () => { + // Arrange + (db.query.transactions.findMany as jest.Mock).mockResolvedValue([fakeTx]); + + // Act + const result = await service.list(fakeSession as any); + + // Assert + expect(result).toEqual([fakeTx]); + }); + + it('filters by organizationId when provided', async () => { + // Arrange + (db.query.transactions.findMany as jest.Mock).mockResolvedValue([fakeTx]); + + // Act + await service.list(fakeSession as any, 'org-uuid-1'); + + // Assert + expect(db.query.transactions.findMany).toHaveBeenCalledTimes(1); + }); + }); + + describe('getById', () => { + it('returns transaction when found', async () => { + // Arrange + (db.query.transactions.findFirst as jest.Mock).mockResolvedValue(fakeTx); + + // Act + const result = await service.getById(fakeSession as any, fakeTx.id); + + // Assert + expect(result).toEqual(fakeTx); + }); + + it('throws NotFoundException for unknown id', async () => { + // Arrange + (db.query.transactions.findFirst as jest.Mock).mockResolvedValue(undefined); + + // Act & Assert + await expect(service.getById(fakeSession as any, 'bad')).rejects.toThrow(NotFoundException); + }); + }); + + describe('create', () => { + it('stores amount as minor units string and emits transaction.created', async () => { + // Arrange + mockOutboxService.withTransaction.mockImplementationOnce( + async (fn: (tx: unknown) => Promise) => { + const mockTx = { + insert: jest.fn().mockReturnValue({ + values: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ ...fakeTx, amountMinorUnits: '150000' }]), + }), + }), + }; + return fn(mockTx); + }, + ); + + // Act + const result = await service.create(fakeSession as any, { + organizationId: 'org-uuid-1', + type: 'invoice', + amountMinorUnits: 150000, + currency: 'EUR', + transactionDate: '2026-01-15', + }); + + // Assert + expect(result.amountMinorUnits).toBe('150000'); + expect(mockOutboxService.record).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ eventType: 'transaction.created' }), + ); + }); + }); + + describe('update', () => { + it('emits transaction.evidence_updated when evidenceStatus changes', async () => { + // Arrange + (db.query.transactions.findFirst as jest.Mock).mockResolvedValue(fakeTx); + mockOutboxService.withTransaction.mockImplementationOnce( + async (fn: (tx: unknown) => Promise) => { + const mockTx = { + update: jest.fn().mockReturnValue({ + set: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ ...fakeTx, evidenceStatus: 'complete' }]), + }), + }), + }), + }; + return fn(mockTx); + }, + ); + + // Act + await service.update(fakeSession as any, fakeTx.id, { evidenceStatus: 'complete' }); + + // Assert + expect(mockOutboxService.record).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ eventType: 'transaction.evidence_updated' }), + ); + }); + }); +});