plugin-financials/__tests__/routes/transactions.test.js
2025-11-03 13:51:33 +02:00

389 lines
12 KiB
JavaScript

/**
* 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);
});
});
});