/** * Transaction Route Tests * * Tests for `/api/plugins/financials/transactions` endpoints * Uses REAL PostgreSQL database - NO mocks */ const request = require('supertest'); const express = require('express'); const transactionsRouter = require('../../routes/transactions'); const FinancialTestHelper = require('../helpers/testHelper'); describe('Transaction Routes', () => { let app; let testHelper; let testSite; let testUserId; let testAccount1; let testAccount2; beforeAll(async () => { // Create Express app for testing app = express(); app.use(express.json()); app.use('/api/plugins/financials', transactionsRouter); // Initialize test helper testHelper = new FinancialTestHelper(); // Create test data testSite = await testHelper.createTestSite(); testUserId = testHelper.userId; // Create test accounts for double-entry const { chartOfAccountsRepository } = require('../../repositories'); testAccount1 = await chartOfAccountsRepository.create({ site_id: testSite.id, account_code: '11000', account_name: 'Test Asset Account', account_type: 'asset', is_active: true, created_by: testUserId }); testAccount2 = await chartOfAccountsRepository.create({ site_id: testSite.id, account_code: '12000', account_name: 'Test Revenue Account', account_type: 'revenue', is_active: true, created_by: testUserId }); }); afterAll(async () => { // Cleanup handled by global schema manager }); describe('GET /api/plugins/financials/transactions', () => { it('should return all transactions for a site', async () => { const { transactionRepository } = require('../../repositories'); // Create test transactions const transaction1 = await transactionRepository.create({ site_id: testSite.id, transaction_date: '2024-01-15', reference_number: 'REF-001', description: 'Test transaction 1', transaction_type: 'income', amount: 500, currency: 'USD', status: 'approved', created_by: testUserId }); const transaction2 = await transactionRepository.create({ site_id: testSite.id, transaction_date: '2024-01-16', reference_number: 'REF-002', description: 'Test transaction 2', transaction_type: 'expense', amount: 200, currency: 'USD', status: 'pending', created_by: testUserId }); const response = await request(app) .get('/api/plugins/financials/transactions') .query({ site_id: testSite.id }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data.length).toBeGreaterThanOrEqual(2); }); it('should filter transactions by type', async () => { const response = await request(app) .get('/api/plugins/financials/transactions') .query({ site_id: testSite.id, type: 'income' }); expect(response.status).toBe(200); expect(response.body.data.every(t => t.transaction_type === 'income')).toBe(true); }); it('should filter transactions by date range', async () => { const response = await request(app) .get('/api/plugins/financials/transactions') .query({ site_id: testSite.id, start_date: '2024-01-01', end_date: '2024-12-31' }); expect(response.status).toBe(200); expect(response.body.data).toBeInstanceOf(Array); }); }); describe('POST /api/plugins/financials/transactions', () => { it('should create a transaction with double-entry validation', async () => { const transactionData = { site_id: testSite.id, transaction_date: '2024-01-20', reference_number: 'REF-003', description: 'Test double-entry transaction', transaction_type: 'transfer', amount: 1000, currency: 'USD', status: 'pending', created_by: testUserId, // Double-entry lines debit_account_id: testAccount1.id, debit_amount: 1000, credit_account_id: testAccount2.id, credit_amount: 1000 }; const response = await request(app) .post('/api/plugins/financials/transactions') .send(transactionData); expect(response.status).toBe(201); expect(response.body.success).toBe(true); expect(response.body.data).toBeDefined(); expect(response.body.data.transaction_type).toBe('transfer'); }); it('should reject transaction where debits do not equal credits', async () => { const invalidTransaction = { site_id: testSite.id, transaction_date: '2024-01-20', description: 'Unbalanced transaction', transaction_type: 'transfer', amount: 1000, created_by: testUserId, // Unbalanced: debits (1000) != credits (500) debit_account_id: testAccount1.id, debit_amount: 1000, credit_account_id: testAccount2.id, credit_amount: 500 }; const response = await request(app) .post('/api/plugins/financials/transactions') .send(invalidTransaction); // Should reject unbalanced transaction expect(response.status).toBe(400); }); it('should require all mandatory fields', async () => { const incompleteTransaction = { site_id: testSite.id, description: 'Incomplete transaction' // Missing required fields }; const response = await request(app) .post('/api/plugins/financials/transactions') .send(incompleteTransaction); expect(response.status).toBe(400); }); it('should create transaction lines for double-entry', async () => { const transactionData = { site_id: testSite.id, transaction_date: '2024-01-21', reference_number: 'REF-004', description: 'Transaction with lines', transaction_type: 'income', amount: 500, currency: 'USD', status: 'pending', created_by: testUserId, debit_account_id: testAccount1.id, debit_amount: 500, credit_account_id: testAccount2.id, credit_amount: 500 }; const response = await request(app) .post('/api/plugins/financials/transactions') .send(transactionData); expect(response.status).toBe(201); // Transaction lines should be created // (will be verified by checking transaction_lines table) }); }); describe('GET /api/plugins/financials/transactions/:id', () => { let testTransaction; beforeEach(async () => { const { transactionRepository } = require('../../repositories'); testTransaction = await transactionRepository.create({ site_id: testSite.id, transaction_date: '2024-01-25', reference_number: 'REF-005', description: 'Detail test transaction', transaction_type: 'expense', amount: 300, currency: 'USD', status: 'approved', created_by: testUserId }); }); it('should return transaction details', async () => { const response = await request(app) .get(`/api/plugins/financials/transactions/${testTransaction.id}`) .query({ site_id: testSite.id }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.data.id).toBe(testTransaction.id); expect(response.body.data.description).toBe('Detail test transaction'); }); it('should return 404 for non-existent transaction', async () => { const fakeId = '00000000-0000-0000-0000-000000000000'; const response = await request(app) .get(`/api/plugins/financials/transactions/${fakeId}`) .query({ site_id: testSite.id }); expect(response.status).toBe(404); }); }); describe('PUT /api/plugins/financials/transactions/:id/approve', () => { let pendingTransaction; beforeEach(async () => { const { transactionRepository } = require('../../repositories'); pendingTransaction = await transactionRepository.create({ site_id: testSite.id, transaction_date: '2024-01-26', reference_number: 'REF-006', description: 'Pending approval transaction', transaction_type: 'expense', amount: 400, currency: 'USD', status: 'pending', created_by: testUserId }); }); it('should approve a transaction', async () => { const response = await request(app) .put(`/api/plugins/financials/transactions/${pendingTransaction.id}/approve`) .send({ approved_by: testUserId }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.data.status).toBe('approved'); expect(response.body.data.approved_by).toBe(testUserId); expect(response.body.data.approved_at).toBeDefined(); }); it('should return 404 for non-existent transaction', async () => { const fakeId = '00000000-0000-0000-0000-000000000000'; const response = await request(app) .put(`/api/plugins/financials/transactions/${fakeId}/approve`) .send({ approved_by: testUserId }); expect(response.status).toBe(404); }); }); describe('Double-Entry Validation', () => { it('should enforce debits equal credits when creating transaction lines', async () => { const { transactionLineRepository } = require('../../repositories'); // Create transaction first const { transactionRepository } = require('../../repositories'); const transaction = await transactionRepository.create({ site_id: testSite.id, transaction_date: '2024-01-27', reference_number: 'REF-007', description: 'Balance test transaction', transaction_type: 'transfer', amount: 750, currency: 'USD', status: 'pending', created_by: testUserId }); // Create balanced transaction lines const line1 = await transactionLineRepository.create({ transaction_id: transaction.id, account_id: testAccount1.id, debit_amount: 750, credit_amount: 0 }); const line2 = await transactionLineRepository.create({ transaction_id: transaction.id, account_id: testAccount2.id, debit_amount: 0, credit_amount: 750 }); // Verify lines are balanced const allLines = await transactionLineRepository.findByTransactionId(transaction.id); const totalDebits = allLines.reduce((sum, line) => sum + parseFloat(line.debit_amount || 0), 0); const totalCredits = allLines.reduce((sum, line) => sum + parseFloat(line.credit_amount || 0), 0); expect(totalDebits).toBe(totalCredits); expect(totalDebits).toBe(750); }); }); describe('Edge Cases', () => { it('should handle transactions with zero amount', async () => { const { transactionRepository } = require('../../repositories'); const zeroTransaction = await transactionRepository.create({ site_id: testSite.id, transaction_date: '2024-01-28', reference_number: 'REF-008', description: 'Zero amount transaction', transaction_type: 'transfer', amount: 0, currency: 'USD', status: 'pending', created_by: testUserId }); expect(zeroTransaction).toBeDefined(); }); it('should handle transactions with decimal amounts', async () => { const { transactionRepository } = require('../../repositories'); const decimalTransaction = await transactionRepository.create({ site_id: testSite.id, transaction_date: '2024-01-29', reference_number: 'REF-009', description: 'Decimal amount transaction', transaction_type: 'income', amount: 123.45, currency: 'USD', status: 'approved', created_by: testUserId }); expect(decimalTransaction.amount).toBeCloseTo(123.45, 2); }); it('should filter transactions by status', async () => { const response = await request(app) .get('/api/plugins/financials/transactions') .query({ site_id: testSite.id, status: 'approved' }); expect(response.status).toBe(200); expect(response.body.data.every(t => t.status === 'approved')).toBe(true); }); }); });