mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 09:08:46 +02:00
Add individual salary deposit agent tool
This commit is contained in:
@@ -35,6 +35,9 @@ How to answer:
|
|||||||
"ayer", "el martes", a specific date) need get_daily_spending with that
|
"ayer", "el martes", a specific date) need get_daily_spending with that
|
||||||
day's bounds — cycle or category summaries have NO per-day data, so never
|
day's bounds — cycle or category summaries have NO per-day data, so never
|
||||||
answer a day question from them.
|
answer a day question from them.
|
||||||
|
- For a question about a specific salary deposit or its amount (including
|
||||||
|
"cuánto recibí hoy"), call get_salary_deposits with the relevant date range;
|
||||||
|
get_salary_summary is aggregate-only and cannot answer it.
|
||||||
- Answer like an analyst, not a calculator. An aggregate number on its own
|
- Answer like an analyst, not a calculator. An aggregate number on its own
|
||||||
is not an answer — show what it is made of, right away, without waiting
|
is not an answer — show what it is made of, right away, without waiting
|
||||||
to be asked:
|
to be asked:
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ from app.services.exchange_rate import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_session_ctx: contextvars.ContextVar[Session] = contextvars.ContextVar("agent_session")
|
_session_ctx: contextvars.ContextVar[Session] = contextvars.ContextVar("agent_session")
|
||||||
|
SALARY_TRANSACTION_TYPES = (TransactionType.SALARY, TransactionType.DEPOSITO)
|
||||||
|
|
||||||
|
|
||||||
def set_session(session: Session) -> contextvars.Token:
|
def set_session(session: Session) -> contextvars.Token:
|
||||||
@@ -339,7 +340,7 @@ def get_salary_summary() -> dict:
|
|||||||
func.count(),
|
func.count(),
|
||||||
func.coalesce(func.sum(amount_crc), 0),
|
func.coalesce(func.sum(amount_crc), 0),
|
||||||
func.max(Transaction.date),
|
func.max(Transaction.date),
|
||||||
).where(Transaction.transaction_type == TransactionType.SALARY)
|
).where(col(Transaction.transaction_type).in_(SALARY_TRANSACTION_TYPES))
|
||||||
).first()
|
).first()
|
||||||
count = row[0] if row else 0
|
count = row[0] if row else 0
|
||||||
total = float(row[1]) if row else 0.0
|
total = float(row[1]) if row else 0.0
|
||||||
@@ -347,6 +348,44 @@ def get_salary_summary() -> dict:
|
|||||||
return {"count": count, "total_crc": total, "latest_date": latest}
|
return {"count": count, "total_crc": total, "latest_date": latest}
|
||||||
|
|
||||||
|
|
||||||
|
def get_salary_deposits(
|
||||||
|
limit: Annotated[int, Field(ge=1, le=100, description="How many deposits to return")] = 20,
|
||||||
|
start_date: Annotated[
|
||||||
|
Optional[str], Field(description="ISO date lower bound, inclusive")
|
||||||
|
] = None,
|
||||||
|
end_date: Annotated[
|
||||||
|
Optional[str], Field(description="ISO date upper bound, exclusive")
|
||||||
|
] = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Individual salary deposits, newest first. Use this for questions about
|
||||||
|
a specific salary or a day/range (for example, "cuánto recibí hoy").
|
||||||
|
Amounts remain in their recorded currency; use the currency field when
|
||||||
|
presenting them."""
|
||||||
|
q = select(Transaction).where(
|
||||||
|
col(Transaction.transaction_type).in_(SALARY_TRANSACTION_TYPES)
|
||||||
|
)
|
||||||
|
if start_date:
|
||||||
|
q = q.where(Transaction.date >= datetime.fromisoformat(start_date))
|
||||||
|
if end_date:
|
||||||
|
q = q.where(Transaction.date < datetime.fromisoformat(end_date))
|
||||||
|
q = q.order_by(col(Transaction.date).desc(), col(Transaction.id).desc()).limit(limit)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": t.id,
|
||||||
|
"date": t.date.isoformat(),
|
||||||
|
"amount": float(t.amount),
|
||||||
|
"currency": t.currency.value,
|
||||||
|
"merchant": t.merchant,
|
||||||
|
"source": t.source.value,
|
||||||
|
"bank": t.bank.value,
|
||||||
|
"transaction_type": t.transaction_type.value,
|
||||||
|
"reference": t.reference,
|
||||||
|
"notes": t.notes,
|
||||||
|
}
|
||||||
|
for t in _s().exec(q).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_municipal_receipts(
|
def get_municipal_receipts(
|
||||||
limit: Annotated[int, Field(ge=1, le=50)] = 12,
|
limit: Annotated[int, Field(ge=1, le=50)] = 12,
|
||||||
offset: Annotated[int, Field(ge=0, description="Skip the N most recent")] = 0,
|
offset: Annotated[int, Field(ge=0, description="Skip the N most recent")] = 0,
|
||||||
@@ -567,6 +606,7 @@ TOOLS = [
|
|||||||
list_recurring_items,
|
list_recurring_items,
|
||||||
get_pension_snapshots,
|
get_pension_snapshots,
|
||||||
get_salary_summary,
|
get_salary_summary,
|
||||||
|
get_salary_deposits,
|
||||||
get_municipal_receipts,
|
get_municipal_receipts,
|
||||||
get_analytics_by_category,
|
get_analytics_by_category,
|
||||||
get_monthly_trend,
|
get_monthly_trend,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from app.models.models import (
|
|||||||
MunicipalReceipt,
|
MunicipalReceipt,
|
||||||
PensionSnapshot,
|
PensionSnapshot,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
TransactionType,
|
||||||
WaterMeterReading,
|
WaterMeterReading,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -173,6 +174,52 @@ def test_recent_transactions_exclude_future_cuotas(bound_session):
|
|||||||
assert "FUTURE CUOTA:03/06" not 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():
|
def test_get_current_date_is_costa_rica_and_cycle_consistent():
|
||||||
from app.timeutil import today_cr
|
from app.timeutil import today_cr
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user