mirror of
https://github.com/sortedcord/alemno-payments.git
synced 2026-07-22 04:02:49 +05:30
refactor: Add JobSummary and Transaction models, client to get currency exchange rate
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
12
tests/services/test_exchange_rate.py
Normal file
12
tests/services/test_exchange_rate.py
Normal 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
|
||||
Reference in New Issue
Block a user