import { LedgerService } from '@/ledger/transactions/ledger-service'; import { PaymentRepository } from '@/repositories/payment-repository'; import { PaymentTransaction } from '@/models/payment'; import { MockLedgerAdapter } from '@/ledger/mock/mock-ledger-adapter'; import { TestHelpers } from '../../utils/test-helpers'; import { LedgerAdapter } from '@/ledger/adapter/types'; describe('LedgerService', () => { let ledgerService: LedgerService; let paymentRepository: PaymentRepository; let mockAdapter: LedgerAdapter; let testPayment: PaymentTransaction; beforeAll(async () => { paymentRepository = new PaymentRepository(); mockAdapter = new MockLedgerAdapter(); ledgerService = new LedgerService(paymentRepository, mockAdapter); }); beforeEach(async () => { await TestHelpers.cleanDatabase(); const operator = await TestHelpers.createTestOperator('TEST_LEDGER', 'MAKER' as any); const paymentRequest = TestHelpers.createTestPaymentRequest(); const paymentId = await paymentRepository.create( paymentRequest, operator.id, `TEST-LEDGER-${Date.now()}` ); const payment = await paymentRepository.findById(paymentId); if (!payment) { throw new Error('Failed to create test payment'); } testPayment = payment; }); afterAll(async () => { await TestHelpers.cleanDatabase(); }); describe('debitAndReserve', () => { it('should debit and reserve funds for payment', async () => { const transactionId = await ledgerService.debitAndReserve(testPayment); expect(transactionId).toBeDefined(); expect(typeof transactionId).toBe('string'); const updatedPayment = await paymentRepository.findById(testPayment.id); expect(updatedPayment?.internalTransactionId).toBe(transactionId); }); it('should return existing transaction ID if already posted', async () => { // First reservation const transactionId1 = await ledgerService.debitAndReserve(testPayment); // Second attempt should return same transaction ID const paymentWithTxn = await paymentRepository.findById(testPayment.id); const transactionId2 = await ledgerService.debitAndReserve(paymentWithTxn!); expect(transactionId2).toBe(transactionId1); }); it('should fail if insufficient funds', async () => { const largePayment: PaymentTransaction = { ...testPayment, amount: 10000000, // Very large amount }; // Mock adapter should throw error for insufficient funds await expect( ledgerService.debitAndReserve(largePayment) ).rejects.toThrow(); }); it('should update payment status after reservation', async () => { await ledgerService.debitAndReserve(testPayment); const updatedPayment = await paymentRepository.findById(testPayment.id); expect(updatedPayment?.internalTransactionId).toBeDefined(); }); }); describe('releaseReserve', () => { it('should release reserved funds', async () => { // First reserve funds await ledgerService.debitAndReserve(testPayment); // Then release await ledgerService.releaseReserve(testPayment.id); // Should complete without error expect(true).toBe(true); }); it('should handle payment without transaction ID gracefully', async () => { // Payment without internal transaction ID await expect( ledgerService.releaseReserve(testPayment.id) ).resolves.not.toThrow(); }); it('should fail if payment not found', async () => { await expect( ledgerService.releaseReserve('non-existent-payment-id') ).rejects.toThrow('Payment not found'); }); }); describe('getTransaction', () => { it('should retrieve transaction by ID', async () => { const transactionId = await ledgerService.debitAndReserve(testPayment); const transaction = await ledgerService.getTransaction(transactionId); expect(transaction).toBeDefined(); }); it('should return null for non-existent transaction', async () => { const transaction = await ledgerService.getTransaction('non-existent-txn-id'); expect(transaction).toBeNull(); }); }); });