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

306 lines
11 KiB
JavaScript

/**
* Unit Balance Route Tests
*
* Tests for `/api/plugins/financials/balances` endpoints
* CRITICAL - Frontend hook uses these endpoints
* Uses REAL PostgreSQL database - NO mocks
*/
const request = require('supertest');
const express = require('express');
const balancesRouter = require('../../routes/balances');
const FinancialTestHelper = require('../helpers/testHelper');
describe('Unit Balance Routes', () => {
let app;
let testHelper;
let testSite;
let testBuilding;
let testUnit;
let testUserId;
beforeAll(async () => {
// Create Express app for testing
app = express();
app.use(express.json());
app.use('/api/plugins/financials', balancesRouter);
// Initialize test helper
testHelper = new FinancialTestHelper();
// Create test data
testSite = await testHelper.createTestSite();
testUserId = testHelper.userId;
testBuilding = await testHelper.createTestBuilding(testSite.id);
testUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
});
afterAll(async () => {
// Cleanup handled by global schema manager
});
describe('GET /api/plugins/financials/balances/units/:unitId', () => {
it('should calculate unit balance with real data', async () => {
// Create starting balance
await testHelper.createTestUnitBalance(testUnit.id, testSite.id, 1000);
// Create invoice (adds to balance)
const invoice1 = await testHelper.createTestInvoice(testUnit.id, testSite.id, 500);
// Create payment (reduces balance)
const payment1 = await testHelper.createTestPayment(testUnit.id, testSite.id, 200);
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${testUnit.id}`)
.query({ site_id: testSite.id });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
// Verify balance structure matches frontend expectations
const balance = response.body.data;
expect(balance.unitId).toBe(testUnit.id);
expect(balance.siteId).toBe(testSite.id);
// Should have these fields (matching frontend expectations)
expect(balance.startingBalance).toBeDefined();
expect(balance.monthlyFees).toBeDefined();
expect(balance.serviceInvoices).toBeDefined();
expect(balance.workOrderInvoices).toBeDefined();
expect(balance.totalPayments).toBeDefined();
expect(balance.currentBalance).toBeDefined();
expect(balance.totalCharges).toBeDefined();
expect(balance.lastUpdated).toBeDefined();
// Verify balance calculation is correct
// currentBalance = startingBalance + charges - payments
const calculated = balance.startingBalance + balance.totalCharges - balance.totalPayments;
expect(balance.currentBalance).toBeCloseTo(calculated, 2);
});
it('should return zero balances for unit with no transactions', async () => {
// Create a new unit with no transactions
const freshUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${freshUnit.id}`)
.query({ site_id: testSite.id });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.currentBalance).toBeDefined();
// Should return zero or empty balance structure
});
it('should calculate balance with multiple invoices and payments', async () => {
// Create unit balance starting at 1000
await testHelper.createTestUnitBalance(testSite.id, testBuilding.id, 1000);
// Create multiple invoices
for (let i = 0; i < 3; i++) {
await testHelper.createTestInvoice(testSite.id, testBuilding.id, 500);
}
// Create multiple payments
for (let i = 0; i < 2; i++) {
await testHelper.createTestPayment(testSite.id, testBuilding.id, 300);
}
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${testSite.id}`)
.query({ site_id: testSite.id });
expect(response.status).toBe(200);
const balance = response.body.data;
// Verify calculation
// Expected: 1000 (starting) + 1500 (3 invoices) - 600 (2 payments) = 1900
expect(balance.currentBalance).toBeDefined();
});
it('should handle negative balance (overpayment)', async () => {
const negativeBalanceUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
// Create large payment
await testHelper.createTestPayment(
negativeBalanceUnit.id,
testSite.id,
2000 // Large payment
);
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${negativeBalanceUnit.id}`)
.query({ site_id: testSite.id });
expect(response.status).toBe(200);
const balance = response.body.data;
// Balance can be negative (overpayment)
expect(typeof balance.currentBalance).toBe('number');
});
it('should require site_id parameter', async () => {
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${testUnit.id}`);
// Should handle missing site_id gracefully
expect(response.status).toBe(400);
});
});
describe('POST /api/plugins/financials/balances/units/batch', () => {
it('should return balances for multiple units', async () => {
// Create multiple units
const units = [];
for (let i = 0; i < 3; i++) {
const unit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
await testHelper.createTestUnitBalance(unit.id, testSite.id, 500 + i * 100);
units.push(unit);
}
const unitIds = units.map(u => u.id);
const response = await request(app)
.post('/api/plugins/financials/balances/units/batch')
.send({
unit_ids: unitIds,
site_id: testSite.id
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
// Should return object with unitId as keys
expect(typeof response.body.data).toBe('object');
expect(response.body.data[units[0].id]).toBeDefined();
expect(response.body.data[units[1].id]).toBeDefined();
expect(response.body.data[units[2].id]).toBeDefined();
});
it('should handle empty array', async () => {
const response = await request(app)
.post('/api/plugins/financials/balances/units/batch')
.send({
unit_ids: [],
site_id: testSite.id
});
expect(response.status).toBe(200);
expect(response.body.data).toEqual({});
});
it('should return balances for valid units and skip invalid ones', async () => {
const validUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
await testHelper.createTestUnitBalance(validUnit.id, testSite.id, 500);
const invalidId = '00000000-0000-0000-0000-000000000000';
const response = await request(app)
.post('/api/plugins/financials/balances/units/batch')
.send({
unit_ids: [validUnit.id, invalidId],
site_id: testSite.id
});
expect(response.status).toBe(200);
// Should return balance for valid unit
// Should skip or handle invalid unit
expect(response.body.data[validUnit.id]).toBeDefined();
});
});
describe('Balance Calculation Formula Validation', () => {
it('should calculate: currentBalance = startingBalance + charges - payments', async () => {
const formulaTestUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
const startingBalance = 1000;
await testHelper.createTestUnitBalance(formulaTestUnit.id, testSite.id, startingBalance);
// Add charges (invoices)
const invoice1 = await testHelper.createTestInvoice(formulaTestUnit.id, testSite.id, 500);
const invoice2 = await testHelper.createTestInvoice(formulaTestUnit.id, testSite.id, 300);
// Add payments
const payment1 = await testHelper.createTestPayment(formulaTestUnit.id, testSite.id, 400);
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${formulaTestUnit.id}`)
.query({ site_id: testSite.id });
const balance = response.body.data;
// Formula: startingBalance (1000) + charges (800) - payments (400) = 1400
const expected = startingBalance + 800 - 400; // 1400
expect(balance.currentBalance).toBeCloseTo(expected, 2);
});
it('should include all invoice types in charges', async () => {
const multiInvoiceUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
await testHelper.createTestUnitBalance(multiInvoiceUnit.id, testSite.id, 500);
// Create different types of invoices
await testHelper.createTestInvoice(multiInvoiceUnit.id, testSite.id, 300); // monthly_fee
// TODO: Add service invoices and work order invoices when those features are implemented
// For now, just verify monthly fees are counted
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${multiInvoiceUnit.id}`)
.query({ site_id: testSite.id });
expect(response.body.success).toBe(true);
expect(response.body.data.totalCharges).toBeGreaterThan(0);
});
});
describe('Edge Cases', () => {
it('should handle unit with no balance record (zero balance)', async () => {
const newUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
// No balance record created
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${newUnit.id}`)
.query({ site_id: testSite.id });
expect(response.status).toBe(200);
expect(response.body.data).toBeDefined();
// Should return zero or empty balance
});
it('should handle very large balance values', async () => {
const largeBalanceUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
const largeAmount = 999999.99;
await testHelper.createTestUnitBalance(largeBalanceUnit.id, testSite.id, largeAmount);
const response = await request(app)
.get(`/api/plugins/financials/balances/units/${largeBalanceUnit.id}`)
.query({ site_id: testSite.id });
expect(response.status).toBe(200);
expect(response.body.data.currentBalance).toBeDefined();
});
it('should return consistent balance across multiple requests', async () => {
const consistencyTestUnit = await testHelper.createTestUnit(testSite.id, testBuilding.id);
await testHelper.createTestUnitBalance(consistencyTestUnit.id, testSite.id, 500);
// Get balance multiple times
const response1 = await request(app)
.get(`/api/plugins/financials/balances/units/${consistencyTestUnit.id}`)
.query({ site_id: testSite.id });
const response2 = await request(app)
.get(`/api/plugins/financials/balances/units/${consistencyTestUnit.id}`)
.query({ site_id: testSite.id });
expect(response1.body.data.currentBalance).toBe(response2.body.data.currentBalance);
});
});
});