diff --git a/app/api/v1/categories.py b/app/api/v1/categories.py new file mode 100644 index 0000000..27f4ac4 --- /dev/null +++ b/app/api/v1/categories.py @@ -0,0 +1,35 @@ +from typing import List +from fastapi import APIRouter, Depends, HTTPException, status +from app.core.dependencies import get_category_service +from app.core.exceptions import CategoryAlreadyExistsException +from app.schemas.category import CategoryCreate, CategoryResponse +from app.services.category_service import CategoryService + +router = APIRouter(prefix="/categories", tags=["categories"]) + + +@router.get("", response_model=List[CategoryResponse]) +async def list_categories( + service: CategoryService = Depends(get_category_service), +): + """ + Returns the list of all available categories ordered alphabetically. + """ + return await service.list_categories() + + +@router.post("", response_model=CategoryResponse, status_code=status.HTTP_201_CREATED) +async def create_category( + category_in: CategoryCreate, + service: CategoryService = Depends(get_category_service), +): + """ + Creates a new custom category. The name must be unique. + """ + try: + return await service.create_category(name=category_in.name) + except CategoryAlreadyExistsException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) diff --git a/app/core/dependencies.py b/app/core/dependencies.py index 9e5290d..7775a2a 100644 --- a/app/core/dependencies.py +++ b/app/core/dependencies.py @@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.db.session import get_db from app.repositories.job_repository import JobRepository from app.services.job_service import JobService +from app.services.category_service import CategoryService def get_job_repository(db: AsyncSession = Depends(get_db)) -> JobRepository: @@ -13,3 +14,8 @@ def get_job_repository(db: AsyncSession = Depends(get_db)) -> JobRepository: def get_job_service(repo: JobRepository = Depends(get_job_repository)) -> JobService: """Dependency to retrieve JobService""" return JobService(repo) + + +def get_category_service(db: AsyncSession = Depends(get_db)) -> CategoryService: + """Dependency to retrieve CategoryService""" + return CategoryService(db) diff --git a/app/core/exceptions.py b/app/core/exceptions.py index 96accdd..6490e60 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -18,3 +18,11 @@ class InvalidCSVException(AlemnoException): def __init__(self, detail: str): self.detail = detail super().__init__(detail) + + +class CategoryAlreadyExistsException(AlemnoException): + """Raised when a category already exists""" + + def __init__(self, name: str): + self.name = name + super().__init__(f"Category '{name}' already exists.") diff --git a/app/db/models/__init__.py b/app/db/models/__init__.py index c2e18bd..bd89a1b 100644 --- a/app/db/models/__init__.py +++ b/app/db/models/__init__.py @@ -2,5 +2,6 @@ from app.db.base import Base from app.db.models.job import Job from app.db.models.transaction import Transaction from app.db.models.job_summary import JobSummary +from app.db.models.category import Category -__all__ = ["Base", "Job", "Transaction", "JobSummary"] +__all__ = ["Base", "Job", "Transaction", "JobSummary", "Category"] diff --git a/app/db/models/category.py b/app/db/models/category.py new file mode 100644 index 0000000..f04d85d --- /dev/null +++ b/app/db/models/category.py @@ -0,0 +1,10 @@ +from sqlalchemy import Integer, String +from sqlalchemy.orm import Mapped, mapped_column +from app.db.base import Base + + +class Category(Base): + __tablename__ = "categories" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) diff --git a/app/main.py b/app/main.py index 84136df..550daf3 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api.v1.jobs import router as jobs_router +from app.api.v1.categories import router as categories_router from app.core.config import settings from app.db.base import Base from app.db.session import engine @@ -12,6 +13,28 @@ async def lifespan(app: FastAPI): # Startup: Create tables if they don't exist async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) + + # Seed default categories if empty + from app.db.session import AsyncSessionLocal + from app.db.models.category import Category + from sqlalchemy import select + + async with AsyncSessionLocal() as session: + result = await session.execute(select(Category)) + if not result.scalars().first(): + defaults = [ + Category(name="Food"), + Category(name="Shopping"), + Category(name="Travel"), + Category(name="Transport"), + Category(name="Utilities"), + Category(name="Cash Withdrawal"), + Category(name="Entertainment"), + Category(name="Other"), + ] + session.add_all(defaults) + await session.commit() + yield # Shutdown: Clean up pool await engine.dispose() @@ -32,8 +55,9 @@ app.add_middleware( allow_headers=["*"], ) -# Include API Router +# Include API Routers app.include_router(jobs_router, prefix=settings.API_V1_STR) +app.include_router(categories_router, prefix=settings.API_V1_STR) # Root healthcheck endpoint diff --git a/app/repositories/category_repository.py b/app/repositories/category_repository.py new file mode 100644 index 0000000..ebaa405 --- /dev/null +++ b/app/repositories/category_repository.py @@ -0,0 +1,28 @@ +from typing import List, Optional +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.db.models.category import Category + + +class CategoryRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def list_all(self) -> List[Category]: + """List all category records ordered alphabetically by name""" + query = select(Category).order_by(Category.name.asc()) + result = await self.db.execute(query) + return list(result.scalars().all()) + + async def create(self, name: str) -> Category: + """Create a new category record""" + category = Category(name=name) + self.db.add(category) + await self.db.flush() + return category + + async def get_by_name(self, name: str) -> Optional[Category]: + """Fetch a category record by name (case-insensitive)""" + query = select(Category).where(Category.name.ilike(name)) + result = await self.db.execute(query) + return result.scalar_one_or_none() diff --git a/app/schemas/category.py b/app/schemas/category.py new file mode 100644 index 0000000..165d6f1 --- /dev/null +++ b/app/schemas/category.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, Field, ConfigDict + + +class CategoryBase(BaseModel): + name: str = Field( + ..., description="Unique category name", min_length=1, max_length=100 + ) + + +class CategoryCreate(CategoryBase): + pass + + +class CategoryResponse(CategoryBase): + id: int + + model_config = ConfigDict(from_attributes=True) diff --git a/app/services/category_service.py b/app/services/category_service.py new file mode 100644 index 0000000..4416426 --- /dev/null +++ b/app/services/category_service.py @@ -0,0 +1,26 @@ +from typing import List +from sqlalchemy.ext.asyncio import AsyncSession +from app.repositories.category_repository import CategoryRepository +from app.db.models.category import Category +from app.core.exceptions import CategoryAlreadyExistsException + + +class CategoryService: + def __init__(self, db: AsyncSession): + self.db = db + self.repo = CategoryRepository(db) + + async def list_categories(self) -> List[Category]: + """Returns all categories alphabetical list""" + return await self.repo.list_all() + + async def create_category(self, name: str) -> Category: + """Creates a new category, validating uniqueness first""" + name_clean = name.strip() + existing = await self.repo.get_by_name(name_clean) + if existing: + raise CategoryAlreadyExistsException(name_clean) + + category = await self.repo.create(name_clean) + await self.db.commit() + return category diff --git a/app/utils/csv_parser.py b/app/utils/csv_parser.py index 4e2fc19..550c112 100644 --- a/app/utils/csv_parser.py +++ b/app/utils/csv_parser.py @@ -83,6 +83,7 @@ def parse_and_validate_csv(content: bytes) -> List[Dict[str, Any]]: # 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() diff --git a/app/worker.py b/app/worker.py index cccb771..18ad425 100644 --- a/app/worker.py +++ b/app/worker.py @@ -24,7 +24,7 @@ class TransactionClassification(BaseModel): description="The unique transaction ID (txn_id) from the input." ) category: str = Field( - description="Assigned category, must be one of: Food, Shopping, Travel, Transport, Utilities, Cash Withdrawal, Entertainment, or Other." + description="Assigned category matching one of the allowed categories list." ) @@ -35,6 +35,7 @@ class BatchClassificationResponse(BaseModel): async def classify_transactions_batch( client: genai.Client, batch_txns: List[Dict[str, Any]], + allowed_categories: List[str], ) -> Dict[str, Dict[str, Any]]: """ Calls the Gemini model to classify a batch of transactions. @@ -55,8 +56,7 @@ async def classify_transactions_batch( input_prompt = ( "Classify the following financial transactions. For each transaction, match its txn_id " - "and assign one of the allowed categories: Food, Shopping, Travel, Transport, Utilities, " - "Cash Withdrawal, Entertainment, or Other based on the merchant name and details:\n\n" + f"and assign one of the allowed categories: {', '.join(allowed_categories)} based on the merchant name and details:\n\n" f"{json.dumps(prompt_data, indent=2)}" ) @@ -146,6 +146,9 @@ def process_transaction_job(job_id: str, transactions: List[Dict[str, Any]]): async def _process_job_async(job_id: str, transactions: List[Dict[str, Any]]): + from app.db.session import engine + + await engine.dispose() async with AsyncSessionLocal() as db: repo = JobRepository(db) job = await repo.get_by_id(job_id) @@ -215,6 +218,28 @@ async def _process_job_async(job_id: str, transactions: List[Dict[str, Any]]): llm_results = {} api_key = settings.GEMINI_API_KEY or os.environ.get("GEMINI_API_KEY") if uncategorized_txns: + # Load allowed categories from the database categories table + from app.db.models.category import Category + + try: + res_cats = await db.execute(select(Category.name)) + allowed_categories = [r[0] for r in res_cats.all()] + except Exception as db_err: + logger.warning(f"Could not load categories from database: {db_err}") + allowed_categories = [] + + if not allowed_categories: + allowed_categories = [ + "Food", + "Shopping", + "Travel", + "Transport", + "Utilities", + "Cash Withdrawal", + "Entertainment", + "Other", + ] + if api_key: logger.info( f"Found {len(uncategorized_txns)} uncategorized transactions. Initializing GenAI client..." @@ -226,7 +251,7 @@ async def _process_job_async(job_id: str, transactions: List[Dict[str, Any]]): for i in range(0, len(uncategorized_txns), batch_size): batch = uncategorized_txns[i : i + batch_size] batch_results = await classify_transactions_batch( - client, batch + client, batch, allowed_categories ) llm_results.update(batch_results) except Exception as e: @@ -247,58 +272,92 @@ async def _process_job_async(job_id: str, transactions: List[Dict[str, Any]]): # Mock categorization mapping based on merchant name keywords for t in uncategorized_txns: merchant_lower = t["merchant"].lower() - cat = "Other" - if any( - kw in merchant_lower - for kw in ["swiggy", "zomato", "restaurant", "cafe", "food"] + cat = ( + "Other" + if "Other" in allowed_categories + else ( + allowed_categories[0] if allowed_categories else "Other" + ) + ) + if ( + any( + kw in merchant_lower + for kw in [ + "swiggy", + "zomato", + "restaurant", + "cafe", + "food", + ] + ) + and "Food" in allowed_categories ): cat = "Food" - elif any( - kw in merchant_lower - for kw in ["ola", "uber", "taxi", "metro", "train"] + elif ( + any( + kw in merchant_lower + for kw in ["ola", "uber", "taxi", "metro", "train"] + ) + and "Transport" in allowed_categories ): cat = "Transport" - elif any( - kw in merchant_lower - for kw in [ - "amazon", - "flipkart", - "shopping", - "store", - "supermarket", - ] + elif ( + any( + kw in merchant_lower + for kw in [ + "amazon", + "flipkart", + "shopping", + "store", + "supermarket", + ] + ) + and "Shopping" in allowed_categories ): cat = "Shopping" - elif any( - kw in merchant_lower - for kw in ["air", "flight", "hotel", "travel", "irctc"] + elif ( + any( + kw in merchant_lower + for kw in ["air", "flight", "hotel", "travel", "irctc"] + ) + and "Travel" in allowed_categories ): cat = "Travel" - elif any( - kw in merchant_lower - for kw in [ - "electric", - "power", - "water", - "bill", - "utility", - "utilities", - ] + elif ( + any( + kw in merchant_lower + for kw in [ + "electric", + "power", + "water", + "bill", + "utility", + "utilities", + ] + ) + and "Utilities" in allowed_categories ): cat = "Utilities" - elif any( - kw in merchant_lower for kw in ["atm", "cash", "withdrawal"] + elif ( + any( + kw in merchant_lower + for kw in ["atm", "cash", "withdrawal"] + ) + and "Cash Withdrawal" in allowed_categories ): cat = "Cash Withdrawal" - elif any( - kw in merchant_lower - for kw in [ - "netflix", - "spotify", - "movie", - "show", - "entertainment", - ] + elif ( + any( + kw in merchant_lower + for kw in [ + "netflix", + "spotify", + "movie", + "show", + "entertainment", + ] + ) + and "Entertainment" in allowed_categories ): cat = "Entertainment" diff --git a/tests/api/test_categories_api.py b/tests/api/test_categories_api.py new file mode 100644 index 0000000..059268c --- /dev/null +++ b/tests/api/test_categories_api.py @@ -0,0 +1,32 @@ +import pytest +from httpx import AsyncClient + +pytestmark = pytest.mark.asyncio + + +async def test_get_categories_seeded_defaults(client: AsyncClient): + # Retrieve categories and verify the default list is present (since lifespan seeds them) + response = await client.get("/api/v1/categories") + assert response.status_code == 200 + categories = response.json() + assert len(categories) >= 8 + + names = [c["name"] for c in categories] + assert "Food" in names + assert "Shopping" in names + assert "Travel" in names + assert "Other" in names + + +async def test_create_and_duplicate_category(client: AsyncClient): + # 1. Create a new category + response = await client.post("/api/v1/categories", json={"name": "Medical"}) + assert response.status_code == 201 + data = response.json() + assert "id" in data + assert data["name"] == "Medical" + + # 2. Try creating the same category (duplicate, case-insensitive check) + dup_response = await client.post("/api/v1/categories", json={"name": "medical"}) + assert dup_response.status_code == 400 + assert "already exists" in dup_response.json()["detail"] diff --git a/tests/conftest.py b/tests/conftest.py index 4815923..c8edb6f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,24 @@ def initialize_test_db(): await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) + # Seed categories in test DB + from sqlalchemy.ext.asyncio import AsyncSession + from app.db.models.category import Category + + async with AsyncSession(test_engine) as session: + defaults = [ + Category(name="Food"), + Category(name="Shopping"), + Category(name="Travel"), + Category(name="Transport"), + Category(name="Utilities"), + Category(name="Cash Withdrawal"), + Category(name="Entertainment"), + Category(name="Other"), + ] + session.add_all(defaults) + await session.commit() + async def teardown_db(): async with test_engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) diff --git a/tests/services/test_llm_classification.py b/tests/services/test_llm_classification.py index e550e46..e2e4153 100644 --- a/tests/services/test_llm_classification.py +++ b/tests/services/test_llm_classification.py @@ -18,7 +18,9 @@ async def test_classify_transactions_batch_retry_success(): {"txn_id": "TX2", "merchant": "Amazon", "amount": 1200.0}, ] - results = await classify_transactions_batch(mock_client, batch_txns) + results = await classify_transactions_batch( + mock_client, batch_txns, ["Food", "Shopping", "Other"] + ) assert len(results) == 2 assert results["TX1"]["category"] == "Food" assert results["TX1"]["failed"] is False @@ -37,7 +39,9 @@ async def test_classify_transactions_batch_retry_failure(): # Patch sleep to make retry test run instantly with patch("asyncio.sleep", return_value=None): - results = await classify_transactions_batch(mock_client, batch_txns) + results = await classify_transactions_batch( + mock_client, batch_txns, ["Food", "Shopping", "Other"] + ) assert len(results) == 1 assert results["TX1"]["category"] == "Other" diff --git a/transactions.csv b/transactions.csv new file mode 100644 index 0000000..a1b7347 --- /dev/null +++ b/transactions.csv @@ -0,0 +1,96 @@ +txn_id,date,merchant,amount,currency,status,category,account_id,notes +TXN1065,04-09-2024,Flipkart,10882.55,INR,SUCCESS,Shopping,ACC003,Refund expected +TXN1054,2024/02/05,Swiggy,$11325.79,INR,success,Food,ACC004, +TXN1021,17-02-2024,Zomato,2536.35,USD,SUCCESS,Food,ACC001,Verified +TXN1045,07-05-2024,Amazon,6874.1,INR,failed,Shopping,ACC004,SUSPICIOUS +TXN1076,14-08-2024,Flipkart,2763.26,INR,SUCCESS,Shopping,ACC004,Duplicate? +TXN1029,19-05-2024,Flipkart,9092.21,INR,FAILED,Shopping,ACC003, +TXN1004,04-07-2024,Jio Recharge,2924.71,INR,SUCCESS,Utilities,ACC001,Verified +TXN1002,2024/01/04,HDFC ATM,10487.18,INR,FAILED,Cash Withdrawal,ACC004, +TXN1000,23-11-2024,Dentist Appointment Fee,423.91,INR,failed,,ACC004, +TXN1073,16-05-2024,HDFC ATM,1117.58,INR,failed,Cash Withdrawal,ACC004, +TXN1019,26-11-2024,Amazon,956.16,INR,success,Shopping,ACC004,Refund expected +TXN1006,24-03-2024,IRCTC,5722.86,INR,FAILED,Travel,ACC001,Refund expected +TXN1014,2024/09/05,Amazon,14670.87,INR,failed,Shopping,ACC005, +TXN1009,11-03-2024,MakeMyTrip,7428.06,USD,success,Travel,ACC004,SUSPICIOUS +TXN1066,2024/06/03,Swiggy,10634.88,INR,FAILED,Food,ACC002, +TXN1071,09-10-2024,Amazon,2390.16,INR,SUCCESS,Shopping,ACC003, +TXN1044,16-03-2024,Swiggy,13395.26,INR,success,Food,ACC005,Verified +TXN1047,2024/09/23,IRCTC,9419.89,INR,FAILED,Travel,ACC005, +TXN1068,03-02-2024,Ola,7401.61,inr,failed,Transport,ACC001,Refund expected +TXN1077,23-02-2024,HDFC ATM,4980.56,INR,failed,,ACC001,Duplicate? +TXN1060,22-05-2024,IRCTC,3695.76,INR,SUCCESS,Travel,ACC003,Refund expected +TXN1009,11-03-2024,MakeMyTrip,7428.06,USD,success,Travel,ACC004,SUSPICIOUS +TXN1033,13-10-2024,Ola,14608.86,inr,success,Transport,ACC002,Verified +TXN1059,29-11-2024,Flipkart,9257.89,INR,failed,Shopping,ACC002, +TXN1034,19-06-2024,Ola,12043.42,inr,SUCCESS,Transport,ACC001, +TXN1062,27-07-2024,IRCTC,8349.88,INR,PENDING,Travel,ACC001,Duplicate? +TXN1042,20-09-2024,Jio Recharge,5061.06,INR,success,Utilities,ACC002, +TXN1023,2024/10/23,MakeMyTrip,961.32,USD,failed,Travel,ACC005, +TXN1011,16-05-2024,BookMyShow,1717.7,INR,FAILED,Entertainment,ACC002,SUSPICIOUS +,25-06-2024,Jio Recharge,4004.59,INR,PENDING,,ACC003, +TXN1020,22-05-2024,IRCTC,3784.61,INR,failed,,ACC005, +TXN1064,20-08-2024,Flipkart,4634.01,INR,failed,Shopping,ACC002,Refund expected +TXN1025,02-08-2024,Jio Recharge,8500.14,INR,FAILED,Utilities,ACC004,Refund expected +TXN1069,01-08-2024,Jio Recharge,8962.1,INR,PENDING,Utilities,ACC005,Verified +TXN1024,05-02-2024,Jio Recharge,1066.01,INR,failed,Utilities,ACC005, +TXN1005,21-11-2024,BookMyShow,2481.68,INR,SUCCESS,Entertainment,ACC002,SUSPICIOUS +TXN1018,30-08-2024,Ola,2896.63,inr,PENDING,Transport,ACC001, +TXN1032,06-03-2024,Swiggy,4658.46,INR,FAILED,Food,ACC002, +TXN1028,30-11-2024,MakeMyTrip,166.96,USD,success,Travel,ACC001, +TXN1070,13-11-2024,Swiggy,9163.98,INR,success,Food,ACC001,Refund expected +TXN1043,31-08-2024,Jio Recharge,12753.58,INR,success,Utilities,ACC004,Verified +TXN1079,12-09-2024,IRCTC,11411.86,INR,PENDING,Travel,ACC004, +TXN1008,25-11-2024,IRCTC,2185.93,INR,failed,Travel,ACC005,Verified +TXN1035,12-04-2024,IRCTC,5277.39,INR,success,Travel,ACC003, +TXN1072,06-02-2024,Zomato,13862.47,USD,success,Food,ACC005,Refund expected +TXN1039,15-11-2024,BookMyShow,9967.64,INR,PENDING,Entertainment,ACC003,SUSPICIOUS +TXN1079,12-09-2024,IRCTC,11411.86,INR,PENDING,Travel,ACC004, +TXN1038,07-04-2024,Flipkart,713.58,INR,success,Shopping,ACC001, +TXN1026,2024/06/02,IRCTC,4776.85,INR,failed,Travel,ACC001,SUSPICIOUS +TXN1040,03-08-2024,Ola,10175.9,inr,failed,Transport,ACC003,Duplicate? +TXN1058,2024/06/23,Swiggy,12751.16,INR,PENDING,Food,ACC004,Refund expected +,2024/04/21,Zomato,7605.06,USD,failed,Food,ACC004,SUSPICIOUS +TXN1022,20-08-2024,Flipkart,6373.96,INR,PENDING,Shopping,ACC003,Refund expected +TXN1075,16-02-2024,Zomato,14430.57,USD,FAILED,Food,ACC002,Verified +TXN1012,2024/11/16,Flipkart,12632.96,INR,success,Shopping,ACC005, +TXN1074,04-06-2024,Ola,14143.01,inr,SUCCESS,Transport,ACC003,SUSPICIOUS +TXN1063,20-12-2024,Zomato,4627.78,USD,PENDING,Food,ACC005,Duplicate? +TXN1013,07-09-2024,Swiggy,1722.42,INR,success,,ACC005, +TXN1057,03-06-2024,Jio Recharge,$12092.64,INR,failed,Utilities,ACC003, +TXN1010,14-07-2024,Jio Recharge,14942.01,INR,SUCCESS,Utilities,ACC001, +TXN1046,15-09-2024,Ola,12467.01,inr,PENDING,Transport,ACC002, +TXN1017,17-01-2024,BookMyShow,1109.32,INR,success,Entertainment,ACC005,SUSPICIOUS +TXN1050,02-08-2024,MakeMyTrip,11225.36,USD,success,Travel,ACC004, +TXN1078,2024/02/29,Ola,12980.41,inr,PENDING,Transport,ACC002,SUSPICIOUS +TXN1041,23-10-2024,Jio Recharge,9837.85,INR,PENDING,Utilities,ACC002, +TXN1035,12-04-2024,IRCTC,5277.39,INR,success,Travel,ACC003, +TXN1037,15-05-2024,Swiggy,1670.62,INR,failed,Food,ACC001,Verified +TXN2003,2024-07-15,IRCTC,193647.29,INR,SUCCESS,,ACC002, +TXN1048,2024/04/28,Ola,10424.55,inr,failed,Transport,ACC001,Refund expected +TXN1016,25-04-2024,Amazon,5104.38,INR,SUCCESS,Shopping,ACC001,Refund expected +TXN1053,13-07-2024,Flipkart,11292.55,INR,SUCCESS,Shopping,ACC001,Duplicate? +TXN1033,13-10-2024,Ola,14608.86,inr,success,Transport,ACC002,Verified +TXN1031,14-10-2024,Swiggy,1722.51,INR,SUCCESS,Food,ACC005,SUSPICIOUS +TXN1000_DUP,23-11-2024,Amazon,423.91,INR,failed,,ACC004, +TXN2002,2024-07-15,Ola,91185.1,INR,SUCCESS,,ACC001, +TXN1055,2024/04/21,IRCTC,10507.0,INR,SUCCESS,Travel,ACC002,Duplicate? +TXN1036,22-05-2024,BookMyShow,9640.15,INR,SUCCESS,Entertainment,ACC003,Refund expected +TXN2001,2024-07-15,Flipkart,146100.68,INR,SUCCESS,,ACC005, +,20-11-2024,Ola,12448.75,inr,FAILED,Transport,ACC004,SUSPICIOUS +TXN1007,10-06-2024,Ola,4052.73,inr,FAILED,Transport,ACC004,Verified +TXN1015,18-04-2024,MakeMyTrip,11341.21,USD,FAILED,Travel,ACC005,Verified +TXN2004,2024-07-15,IRCTC,191918.37,INR,SUCCESS,,ACC003, +TXN1067,08-09-2024,Amazon,13097.16,INR,success,Shopping,ACC003, +TXN1020,22-05-2024,IRCTC,3784.61,INR,failed,,ACC005, +TXN1042,20-09-2024,Jio Recharge,5061.06,INR,success,Utilities,ACC002, +TXN1016,25-04-2024,Amazon,5104.38,INR,SUCCESS,Shopping,ACC001,Refund expected +TXN1061,30-06-2024,Flipkart,5138.07,INR,SUCCESS,Shopping,ACC002, +,18-07-2024,Jio Recharge,14193.63,INR,PENDING,,ACC003,Duplicate? +TXN1027,15-05-2024,HDFC ATM,14002.23,INR,success,Cash Withdrawal,ACC005, +TXN1051,08-12-2024,Ola,10876.51,inr,failed,,ACC004,SUSPICIOUS +TXN1052,14-01-2024,Amazon,9659.11,INR,PENDING,Shopping,ACC004,Duplicate? +TXN1044,16-03-2024,Swiggy,13395.26,INR,success,Food,ACC005,Verified +TXN1029,19-05-2024,Flipkart,9092.21,INR,FAILED,Shopping,ACC003, +TXN2000,2024-07-15,Jio Recharge,175917.65,INR,SUCCESS,,ACC002, +TXN1056,14-01-2024,Flipkart,8658.16,INR,success,,ACC002,