Files
WealthySmart/backend/tests/test_agent_tools.py
Carlos Escalante 8ceed0ab2e
All checks were successful
Deploy to VPS / test (push) Successful in 1m43s
Deploy to VPS / deploy (push) Successful in 27s
Add individual salary deposit agent tool
2026-07-14 22:41:53 -06:00

287 lines
8.0 KiB
Python

"""Agent tool coverage: session binding, JSON-safe output (no Decimal leaks),
and the batched queries introduced in Phase 2."""
import json
from datetime import date, datetime
from decimal import Decimal
import pytest
from app.agent import tools
from app.agent.tools import reset_session, set_session
from app.models.models import (
Account,
AccountType,
Bank,
Currency,
MunicipalReceipt,
PensionSnapshot,
Transaction,
TransactionType,
WaterMeterReading,
)
@pytest.fixture()
def bound_session(session):
token = set_session(session)
yield session
reset_session(token)
def make_snapshot(fund: Bank, start: date, end: date, saldo: str) -> PensionSnapshot:
zero = Decimal("0")
return PensionSnapshot(
fund=fund,
contract_number="C-1",
period_start=start,
period_end=end,
saldo_anterior=zero,
aportes=zero,
rendimientos=zero,
retiros=zero,
traslados=zero,
comision=zero,
correccion=zero,
bonificacion=zero,
saldo_final=Decimal(saldo),
source_filename="t.pdf",
)
def make_receipt(period: str) -> MunicipalReceipt:
zero = Decimal("0")
return MunicipalReceipt(
receipt_date=date(2026, 4, 1),
due_date=date(2026, 4, 15),
period=period,
account="A1",
finca="F1",
holder_name="x",
holder_cedula="x",
holder_address="x",
subtotal=Decimal("100"),
interests=zero,
iva=Decimal("13"),
total=Decimal("113"),
source_filename="r.pdf",
)
def test_unbound_session_raises(session):
with pytest.raises(RuntimeError, match="not bound to agent context"):
tools.get_accounts()
def test_accounts_output_is_json_safe(bound_session):
bound_session.add(
Account(
bank=Bank.BAC,
currency=Currency.CRC,
label="Main",
balance=Decimal("1234.56"),
account_type=AccountType.BANK,
)
)
bound_session.commit()
out = tools.get_accounts()
json.dumps(out) # would raise on Decimal leakage
assert out[0]["balance"] == 1234.56
def test_pension_latest_only_per_fund(bound_session):
bound_session.add(
make_snapshot(Bank.FCL, date(2026, 1, 1), date(2026, 1, 31), "100")
)
bound_session.add(
make_snapshot(Bank.FCL, date(2026, 2, 1), date(2026, 2, 28), "200")
)
bound_session.add(
make_snapshot(Bank.ROP, date(2026, 1, 1), date(2026, 1, 31), "900")
)
bound_session.commit()
out = tools.get_pension_snapshots(latest_only=True)
json.dumps(out)
assert len(out) == 2
by_fund = {o["fund"]: o for o in out}
assert by_fund["FCL"]["saldo_final"] == 200.0 # latest period wins
assert by_fund["ROP"]["saldo_final"] == 900.0
full = tools.get_pension_snapshots(latest_only=False)
assert len(full) == 3
def test_municipal_receipts_batched_consumption(bound_session):
r1, r2 = make_receipt("2026-03"), make_receipt("2026-04")
r2.account = "A2"
bound_session.add(r1)
bound_session.add(r2)
bound_session.commit()
bound_session.refresh(r1)
bound_session.refresh(r2)
for rid, m3 in ((r1.id, "10.5"), (r1.id, "4.5"), (r2.id, "7")):
bound_session.add(
WaterMeterReading(
meter_id="7335",
period=f"p{m3}",
consumption_m3=Decimal(m3),
receipt_id=rid,
)
)
bound_session.commit()
out = tools.get_municipal_receipts()
json.dumps(out)
by_account = {o["account"]: o for o in out}
assert by_account["A1"]["water_consumption_m3"] == 15.0
assert by_account["A2"]["water_consumption_m3"] == 7.0
def test_recent_transactions_json_safe(bound_session):
bound_session.add(
Transaction(
amount=Decimal("42.42"),
merchant="M",
date=datetime(2026, 4, 1),
)
)
bound_session.commit()
out = tools.get_recent_transactions()
json.dumps(out)
assert out[0]["amount"] == 42.42
def test_recent_transactions_exclude_future_cuotas(bound_session):
from datetime import timedelta
from app.timeutil import today_cr
today = datetime.combine(today_cr(), datetime.min.time())
bound_session.add(
Transaction(amount=Decimal("10"), merchant="BILLED", date=today)
)
bound_session.add(
Transaction(
amount=Decimal("20"),
merchant="FUTURE CUOTA:03/06",
date=today + timedelta(days=45),
)
)
bound_session.commit()
merchants = [t["merchant"] for t in tools.get_recent_transactions()]
assert "BILLED" in merchants
assert "FUTURE CUOTA:03/06" not in merchants
def test_salary_deposits_returns_individual_filtered_rows(bound_session):
bound_session.add_all(
[
Transaction(
amount=Decimal("1500"),
merchant="SALARY JULY",
date=datetime(2026, 7, 14, 9),
transaction_type=TransactionType.SALARY,
reference="PAY-2026-07",
),
Transaction(
amount=Decimal("1400"),
merchant="SALARY JUNE",
date=datetime(2026, 6, 14, 9),
transaction_type=TransactionType.SALARY,
),
Transaction(
amount=Decimal("999"),
merchant="PURCHASE",
date=datetime(2026, 7, 14, 10),
),
]
)
bound_session.commit()
out = tools.get_salary_deposits(
start_date="2026-07-14", end_date="2026-07-15"
)
json.dumps(out)
assert out == [
{
"id": out[0]["id"],
"date": "2026-07-14T09:00:00",
"amount": 1500.0,
"currency": "CRC",
"merchant": "SALARY JULY",
"source": "CREDIT_CARD",
"bank": "BAC",
"transaction_type": "SALARY",
"reference": "PAY-2026-07",
"notes": None,
}
]
def test_get_current_date_is_costa_rica_and_cycle_consistent():
from app.timeutil import today_cr
out = tools.get_current_date()
today = today_cr()
assert out["date"] == today.isoformat()
# The active cycle must contain today: [start, end)
start, end = out["cycle_range"]
assert start <= out["date"] < end
assert start.endswith("-18") and end.endswith("-18")
def test_daily_spending_day_scoped(bound_session):
from datetime import timedelta
from app.timeutil import today_cr
today = datetime.combine(today_cr(), datetime.min.time())
bound_session.add(
Transaction(amount=Decimal("100"), merchant="TODAY-A", date=today.replace(hour=9))
)
bound_session.add(
Transaction(amount=Decimal("50"), merchant="TODAY-B", date=today.replace(hour=15))
)
bound_session.add(
Transaction(
amount=Decimal("999"),
merchant="FUTURE CUOTA",
date=today + timedelta(days=40),
)
)
bound_session.commit()
out = tools.get_daily_spending(
start_date=today.date().isoformat(),
end_date=(today.date() + timedelta(days=60)).isoformat(),
)
assert len(out) == 1
assert out[0]["date"] == today.date().isoformat()
assert out[0]["total_crc"] == 150.0
assert out[0]["count"] == 2
def test_get_current_date_respects_client_timezone():
from datetime import datetime, timezone as tz
from app.timeutil import (
client_tz,
reset_client_timezone,
set_client_timezone,
)
token = set_client_timezone("Asia/Tokyo")
try:
out = tools.get_current_date()
assert out["timezone"] == "Asia/Tokyo"
assert out["date"] == datetime.now(client_tz()).date().isoformat()
finally:
reset_client_timezone(token)
# Unknown names are ignored — CR fallback stays active.
assert set_client_timezone("Not/AZone") is None
assert set_client_timezone(None) is None
assert tools.get_current_date()["timezone"] == "UTC-06:00"