refactor: Add JobSummary and Transaction models, client to get currency exchange rate

This commit is contained in:
2026-06-29 20:57:06 +05:30
parent 2e556173d4
commit f3727af697
14 changed files with 518 additions and 150 deletions

View File

@@ -12,9 +12,9 @@ celery_app.conf.task_always_eager = True
async def test_upload_csv_success(client: AsyncClient):
csv_content = (
"transaction_id,date,amount,category,description\n"
"TX1001,2026-06-25,120.50,Groceries,Weekly grocery shop\n"
"TX1002,06/26/2026,15000.00,Electronics,New Laptop\n" # This will be flagged as anomaly (> $5,000)
"txn_id,date,amount,merchant,currency,category,account\n"
"TX1001,2026-06-25,120.50,Weekly grocery shop,INR,Groceries,ACC01\n"
"TX1002,06/26/2026,6000.00,New Laptop,USD,Electronics,ACC02\n" # Anomaly (> $5,000 USD equivalent)
)
files = {
@@ -43,10 +43,15 @@ async def test_upload_csv_success(client: AsyncClient):
status_data = status_response.json()
assert status_data["id"] == job_id
assert status_data["status"] == "completed"
assert status_data["summary"]["total_rows"] == 2
assert status_data["summary"]["total_amount"] == 15120.50
assert status_data["summary"]["anomalies_count"] == 1
assert status_data["summary"]["top_category"] == "Electronics"
summary = status_data["summary"]
assert summary is not None
assert summary["anomaly_count"] == 1
assert (
summary["total_spend_usd"] > 6000.0
) # TX1002 is $6,000, TX1001 is INR 120.50 ($1.45)
assert "New Laptop" in summary["top_merchants"]
assert summary["risk_level"] == "Medium" # 1 anomaly
# 3. Get Job Results
results_response = await client.get(f"/api/v1/jobs/{job_id}/results")
@@ -55,20 +60,24 @@ async def test_upload_csv_success(client: AsyncClient):
assert results_data["id"] == job_id
assert results_data["status"] == "completed"
results = results_data["results"]
assert len(results["cleaned_transactions"]) == 2
assert len(results["flagged_anomalies"]) == 1
assert results["flagged_anomalies"][0]["transaction_id"] == "TX1002"
assert results["spend_breakdown"]["Groceries"]["total_spend"] == 120.50
assert "Successfully parsed and processed" in results["narrative_summary"]
transactions_list = results_data["transactions"]
assert len(transactions_list) == 2
# Assert TX1002 is flagged as an anomaly
laptop_txn = next(t for t in transactions_list if t["txn_id"] == "TX1002")
assert laptop_txn["is_anomaly"] is True
assert "High value transaction" in laptop_txn["anomaly_reason"]
assert laptop_txn["llm_category"] == "Electronics"
assert laptop_txn["llm_failed"] is False
grocery_txn = next(t for t in transactions_list if t["txn_id"] == "TX1001")
assert grocery_txn["is_anomaly"] is False
assert grocery_txn["currency"] == "INR"
async def test_upload_invalid_csv(client: AsyncClient):
# Invalid CSV structure (missing required column)
csv_content = (
"transaction_id,amount,category,description\n"
"TX1001,120.50,Groceries,Weekly grocery shop\n"
)
# Invalid CSV structure (missing required column: merchant)
csv_content = "txn_id,date,amount\nTX1001,2026-06-25,120.50\n"
files = {
"file": (
"transactions.csv",

View File

@@ -1,6 +1,9 @@
from datetime import date
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.job_repository import JobRepository
from app.db.models.transaction import Transaction
from app.db.models.job_summary import JobSummary
# Set up asyncio marker
pytestmark = pytest.mark.asyncio
@@ -22,36 +25,60 @@ async def test_create_and_get_job(db_session: AsyncSession):
assert fetched_job.filename == "test_transactions.csv"
async def test_update_job(db_session: AsyncSession):
async def test_update_job_and_add_relations(db_session: AsyncSession):
repo = JobRepository(db_session)
job = await repo.create(filename="update_test.csv")
# Update status and count
updated = await repo.update(
job, status="completed", row_count=10, summary={"total": 100}
job, status="completed", row_count_raw=10, row_count_clean=8
)
assert updated.status == "completed"
assert updated.row_count == 10
assert updated.summary == {"total": 100}
assert updated.row_count_raw == 10
assert updated.row_count_clean == 8
# Create & add summary
summary = JobSummary(
job_id=job.id,
total_spend_inr=15000.0,
total_spend_usd=180.72,
top_merchants={"Merchant A": 100.0},
anomaly_count=1,
narrative="Good",
risk_level="Low",
)
await repo.add_summary(summary)
# Create & add transaction
txn = Transaction(
job_id=job.id,
txn_id="TX001",
date=date(2026, 6, 25),
merchant="Merchant A",
amount=100.0,
currency="USD",
status="cleaned",
is_anomaly=False,
)
await repo.add_transactions([txn])
# Retrieve to verify persistence
fetched = await repo.get_by_id(job.id)
assert fetched.status == "completed"
assert fetched.row_count == 10
assert fetched.row_count_raw == 10
assert fetched.row_count_clean == 8
assert fetched.summary is not None
assert fetched.summary.total_spend_usd == 180.72
assert len(fetched.transactions) == 1
assert fetched.transactions[0].txn_id == "TX001"
async def test_list_jobs(db_session: AsyncSession):
repo = JobRepository(db_session)
# Clear existing if any
await repo.create(filename="list_1.csv")
await repo.create(filename="list_2.csv")
jobs = await repo.list_all()
# At least the ones we created in this test session
assert len(jobs) >= 2
pending_jobs = await repo.list_all(status="pending")
assert len(pending_jobs) >= 2
completed_jobs = await repo.list_all(status="completed")
assert len(completed_jobs) == 1 # From the update test above

View File

@@ -5,23 +5,30 @@ from app.utils.csv_parser import parse_and_validate_csv
def test_parse_valid_csv():
valid_csv = (
"transaction_id,date,amount,category,description\n"
"TX1001,2026-06-25,120.50,Groceries,Weekly grocery shop\n"
"TX1002,06/26/2026,15.00,Coffee,Morning latte\n"
"transaction_id,date,amount,category,description,currency,account\n"
"TX1001,2026-06-25,120.50,Groceries,Weekly grocery shop,INR,ACC01\n"
"TX1002,06/26/2026,15.00,Coffee,Morning latte,USD,ACC02\n"
)
result = parse_and_validate_csv(valid_csv.encode("utf-8"))
assert len(result) == 2
assert result[0]["transaction_id"] == "TX1001"
assert result[0]["txn_id"] == "TX1001"
assert result[0]["amount"] == 120.50
assert result[0]["date"] == "2026-06-25"
assert result[1]["transaction_id"] == "TX1002"
assert result[0]["merchant"] == "Weekly grocery shop"
assert result[0]["currency"] == "INR"
assert result[0]["account_id"] == "ACC01"
assert result[1]["txn_id"] == "TX1002"
assert result[1]["amount"] == 15.00
assert result[1]["date"] == "2026-06-26"
assert result[1]["merchant"] == "Morning latte"
assert result[1]["currency"] == "USD"
assert result[1]["account_id"] == "ACC02"
def test_parse_missing_headers():
invalid_csv = (
"transaction_id,amount,category,description\n"
"txn_id,amount,category,description\n"
"TX1001,120.50,Groceries,Weekly grocery shop\n"
)
with pytest.raises(InvalidCSVException) as exc_info:
@@ -31,8 +38,7 @@ def test_parse_missing_headers():
def test_parse_invalid_amount():
invalid_csv = (
"transaction_id,date,amount,category,description\n"
"TX1001,2026-06-25,abc,Groceries,Weekly grocery shop\n"
"txn_id,date,amount,merchant\nTX1001,2026-06-25,abc,Weekly grocery shop\n"
)
with pytest.raises(InvalidCSVException) as exc_info:
parse_and_validate_csv(invalid_csv.encode("utf-8"))
@@ -41,8 +47,7 @@ def test_parse_invalid_amount():
def test_parse_invalid_date():
invalid_csv = (
"transaction_id,date,amount,category,description\n"
"TX1001,invalid-date,120.50,Groceries,Weekly grocery shop\n"
"txn_id,date,amount,merchant\nTX1001,invalid-date,120.50,Weekly grocery shop\n"
)
with pytest.raises(InvalidCSVException) as exc_info:
parse_and_validate_csv(invalid_csv.encode("utf-8"))

View File

@@ -0,0 +1,12 @@
import pytest
from app.clients.exchange_rate_client import ExchangeRateClient
pytestmark = pytest.mark.asyncio
async def test_get_usd_to_inr_rate():
client = ExchangeRateClient()
rate = await client.get_usd_to_inr_rate()
assert isinstance(rate, float)
# The rate should be positive (either the fetched rate or the default fallback of 83.0)
assert rate > 0.0