306 lines
11 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
|