389 lines
12 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
|