import os import uuid from datetime import date import pytest from fastapi.testclient import TestClient # Ensure required env vars for app import/config os.environ.setdefault("SECRET_KEY", "x" * 32) os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/delphi_test.sqlite") from app.main import app # noqa: E402 from app.auth.security import get_current_user # noqa: E402 from tests.helpers import assert_validation_error, assert_http_error # noqa: E402 @pytest.fixture(scope="module") def client(): # Override auth to bypass JWT for these tests class _User: def __init__(self): self.id = "test" self.username = "tester" self.is_admin = True self.is_active = True app.dependency_overrides[get_current_user] = lambda: _User() try: yield TestClient(app) finally: app.dependency_overrides.pop(get_current_user, None) def _create_customer(client: TestClient) -> str: customer_id = f"LEDGER-CUST-{uuid.uuid4().hex[:8]}" payload = {"id": customer_id, "last": "LedgerOwner", "email": "owner@example.com"} resp = client.post("/api/customers/", json=payload) assert resp.status_code == 200 return customer_id def _create_file(client: TestClient, owner_id: str) -> str: file_no = f"L-{uuid.uuid4().hex[:6]}" payload = { "file_no": file_no, "id": owner_id, "regarding": "Ledger matter", "empl_num": "E01", "file_type": "CIVIL", "opened": date.today().isoformat(), "status": "ACTIVE", "rate_per_hour": 100.0, "memo": "Created by pytest", } resp = client.post("/api/files/", json=payload) assert resp.status_code == 200 return file_no def test_ledger_create_validation_errors(client: TestClient): owner_id = _create_customer(client) file_no = _create_file(client, owner_id) # Missing required fields resp = client.post("/api/financial/ledger/", json={}) assert_validation_error(resp, "file_no") # Wrong types for amount/date bad = { "file_no": file_no, "date": "not-a-date", "t_code": "TIME", "t_type": "2", "empl_num": "E01", "quantity": 1.5, "rate": 100.0, "amount": "one hundred", "billed": "N", "note": "Invalid types", } resp = client.post("/api/financial/ledger/", json=bad) assert_validation_error(resp, "date") assert_validation_error(resp, "amount") # Query param validation on list endpoint resp = client.get(f"/api/financial/ledger/{file_no}?limit=0") assert_validation_error(resp, "limit") resp = client.get(f"/api/financial/ledger/{file_no}?skip=-1") assert_validation_error(resp, "skip") def test_ledger_404s_for_missing_file_and_entries(client: TestClient): # Create against missing file payload = { "file_no": "NOFILE-123", "date": date.today().isoformat(), "t_code": "TIME", "t_type": "2", "empl_num": "E01", "quantity": 1.0, "rate": 100.0, "amount": 100.0, "billed": "N", "note": "Should 404", } resp = client.post("/api/financial/ledger/", json=payload) assert_http_error(resp, 404, "File not found") # Update/delete missing entry id resp = client.put("/api/financial/ledger/9999999", json={"amount": 10}) assert_http_error(resp, 404, "Ledger entry not found") resp = client.delete("/api/financial/ledger/9999999") assert_http_error(resp, 404, "Ledger entry not found") # Report and quick endpoints on missing file resp = client.get("/api/financial/reports/NOFILE-123") assert_http_error(resp, 404, "File not found") resp = client.post("/api/financial/time-entry/quick", params={"file_no": "NOFILE-123", "hours": 1.0, "description": "x"}) assert_http_error(resp, 404, "File not found") resp = client.post("/api/financial/payments/", params={"file_no": "NOFILE-123", "amount": 10.0}) assert_http_error(resp, 404, "File not found") resp = client.post("/api/financial/expenses/", params={"file_no": "NOFILE-123", "amount": 10.0, "description": "x"}) assert_http_error(resp, 404, "File not found") # Bill entries with no matching ids (body expects a raw JSON array) resp = client.post("/api/financial/bill-entries", json=[999999]) assert_http_error(resp, 404, "No entries found") def test_ledger_totals_update_after_crud(client: TestClient): owner_id = _create_customer(client) file_no = _create_file(client, owner_id) # Baseline resp = client.get(f"/api/files/{file_no}") assert resp.status_code == 200 file_data = resp.json() assert file_data["total_charges"] == 0 assert file_data["amount_owing"] == 0 # 1) Create hourly time entry (t_type "2") t_payload = { "file_no": file_no, "date": date.today().isoformat(), "t_code": "TIME", "t_type": "2", "empl_num": "E01", "quantity": 2.0, "rate": 100.0, "amount": 200.0, "billed": "N", "note": "Work", } resp = client.post("/api/financial/ledger/", json=t_payload) assert resp.status_code == 200 time_entry = resp.json() resp = client.get(f"/api/files/{file_no}") f = resp.json() assert f["hours"] == 2.0 assert f["hourly_fees"] == 200.0 assert f["total_charges"] == 200.0 assert f["amount_owing"] == 200.0 # 2) Create disbursement (t_type "4") amount 50 d_payload = { "file_no": file_no, "date": date.today().isoformat(), "t_code": "MISC", "t_type": "4", "empl_num": "E01", "quantity": 0.0, "rate": 0.0, "amount": 50.0, "billed": "N", "note": "Expense", } resp = client.post("/api/financial/ledger/", json=d_payload) assert resp.status_code == 200 disb_entry = resp.json() resp = client.get(f"/api/files/{file_no}") f = resp.json() assert f["disbursements"] == 50.0 assert f["total_charges"] == 250.0 assert f["amount_owing"] == 250.0 # 3) Create credit/payment (t_type "5") amount 100 c_payload = { "file_no": file_no, "date": date.today().isoformat(), "t_code": "PMT", "t_type": "5", "empl_num": "E01", "quantity": 0.0, "rate": 0.0, "amount": 100.0, "billed": "Y", "note": "Payment", } resp = client.post("/api/financial/ledger/", json=c_payload) assert resp.status_code == 200 credit_entry = resp.json() resp = client.get(f"/api/files/{file_no}") f = resp.json() assert f["credit_bal"] == 100.0 assert f["amount_owing"] == 150.0 # 4) Trust deposit (t_type "1") amount 80 trust_payload = { "file_no": file_no, "date": date.today().isoformat(), "t_code": "TRUST", "t_type": "1", "empl_num": "E01", "quantity": 0.0, "rate": 0.0, "amount": 80.0, "billed": "Y", "note": "Trust deposit", } resp = client.post("/api/financial/ledger/", json=trust_payload) assert resp.status_code == 200 trust_entry = resp.json() resp = client.get(f"/api/files/{file_no}") f = resp.json() assert f["trust_bal"] == 80.0 assert f["transferable"] == 80.0 # 5) Update credit entry to 200 resp = client.put(f"/api/financial/ledger/{credit_entry['id']}", json={"amount": 200.0}) assert resp.status_code == 200 resp = client.get(f"/api/files/{file_no}") f = resp.json() assert f["credit_bal"] == 200.0 assert f["amount_owing"] == 50.0 assert f["transferable"] == 50.0 # 6) Delete trust deposit, transferable should drop to 0 resp = client.delete(f"/api/financial/ledger/{trust_entry['id']}") assert resp.status_code == 200 resp = client.get(f"/api/files/{file_no}") f = resp.json() assert f["trust_bal"] == 0.0 assert f["transferable"] == 0.0 # 7) Verify report totals consistency resp = client.get(f"/api/financial/reports/{file_no}") assert resp.status_code == 200 report = resp.json() assert report["total_hours"] == 2.0 assert report["total_hourly_fees"] == 200.0 assert report["total_disbursements"] == 50.0 assert report["total_credits"] == 200.0 assert report["total_charges"] == 250.0 assert report["amount_owing"] == 50.0