mirror of
https://github.com/sortedcord/alemno-payments.git
synced 2026-07-22 04:02:49 +05:30
feat: Add resilience hardening for csv parser
This commit is contained in:
@@ -75,14 +75,24 @@ def parse_and_validate_csv(content: bytes) -> List[Dict[str, Any]]:
|
||||
else None
|
||||
)
|
||||
|
||||
if not all([txn_id, date_str, amount_str, merchant]):
|
||||
if not all([date_str, amount_str, merchant]):
|
||||
raise InvalidCSVException(
|
||||
f"Row {line_num}: Empty values are not allowed in required fields"
|
||||
f"Row {line_num}: Empty values are not allowed in required fields (date, amount, merchant)"
|
||||
)
|
||||
|
||||
# Auto-generate txn_id if missing or empty
|
||||
if not txn_id or not txn_id.strip():
|
||||
import uuid
|
||||
txn_id = f"GEN_{uuid.uuid4().hex[:8].upper()}"
|
||||
else:
|
||||
txn_id = txn_id.strip()
|
||||
|
||||
# Validate amount
|
||||
cleaned_amount_str = (
|
||||
amount_str.strip().replace("$", "").replace("₹", "").replace(",", "")
|
||||
)
|
||||
try:
|
||||
amount = float(amount_str.strip())
|
||||
amount = float(cleaned_amount_str)
|
||||
except ValueError as e:
|
||||
raise InvalidCSVException(
|
||||
f"Row {line_num}: Invalid amount '{amount_str}'. Must be a number."
|
||||
|
||||
@@ -52,3 +52,15 @@ def test_parse_invalid_date():
|
||||
with pytest.raises(InvalidCSVException) as exc_info:
|
||||
parse_and_validate_csv(invalid_csv.encode("utf-8"))
|
||||
assert "Invalid date format" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_parse_amount_with_symbols():
|
||||
csv_data = (
|
||||
"txn_id,date,amount,merchant\n"
|
||||
"TX1001,2026-06-25,$120.50,Weekly grocery shop\n"
|
||||
'TX1002,2026-06-26,"₹1,500.00",Weekly grocery shop\n'
|
||||
)
|
||||
result = parse_and_validate_csv(csv_data.encode("utf-8"))
|
||||
assert len(result) == 2
|
||||
assert result[0]["amount"] == 120.50
|
||||
assert result[1]["amount"] == 1500.00
|
||||
|
||||
44
tests/services/test_llm_classification.py
Normal file
44
tests/services/test_llm_classification.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
from app.worker import classify_transactions_batch
|
||||
from google import genai
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
async def test_classify_transactions_batch_retry_success():
|
||||
# Mock GenAI client and interaction creation
|
||||
mock_client = MagicMock(spec=genai.Client)
|
||||
mock_interaction = MagicMock()
|
||||
mock_interaction.output_text = '{"classifications": [{"txn_id": "TX1", "category": "Food"}, {"txn_id": "TX2", "category": "Shopping"}]}'
|
||||
mock_client.interactions.create.return_value = mock_interaction
|
||||
|
||||
batch_txns = [
|
||||
{"txn_id": "TX1", "merchant": "Swiggy", "amount": 150.0},
|
||||
{"txn_id": "TX2", "merchant": "Amazon", "amount": 1200.0},
|
||||
]
|
||||
|
||||
results = await classify_transactions_batch(mock_client, batch_txns)
|
||||
assert len(results) == 2
|
||||
assert results["TX1"]["category"] == "Food"
|
||||
assert results["TX1"]["failed"] is False
|
||||
assert results["TX2"]["category"] == "Shopping"
|
||||
assert results["TX2"]["failed"] is False
|
||||
|
||||
|
||||
async def test_classify_transactions_batch_retry_failure():
|
||||
mock_client = MagicMock(spec=genai.Client)
|
||||
# Simulate API call failure (e.g. invalid API key or connection error)
|
||||
mock_client.interactions.create.side_effect = Exception("API Key Error")
|
||||
|
||||
batch_txns = [
|
||||
{"txn_id": "TX1", "merchant": "Swiggy", "amount": 150.0},
|
||||
]
|
||||
|
||||
# Patch sleep to make retry test run instantly
|
||||
with patch("asyncio.sleep", return_value=None):
|
||||
results = await classify_transactions_batch(mock_client, batch_txns)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results["TX1"]["category"] == "Other"
|
||||
assert results["TX1"]["failed"] is True
|
||||
Reference in New Issue
Block a user