feat: Add resilience hardening for csv parser

This commit is contained in:
2026-07-03 08:04:14 +05:30
parent a305bba66d
commit be6c95c497
3 changed files with 69 additions and 3 deletions

View File

@@ -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."

View File

@@ -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

View 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