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