mirror of
https://github.com/sortedcord/alemno-payments.git
synced 2026-07-21 19:52:50 +05:30
feat: Add API endpoint for managing categories
This commit is contained in:
35
app/api/v1/categories.py
Normal file
35
app/api/v1/categories.py
Normal file
@@ -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),
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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"]
|
||||
|
||||
10
app/db/models/category.py
Normal file
10
app/db/models/category.py
Normal file
@@ -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)
|
||||
26
app/main.py
26
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
|
||||
|
||||
28
app/repositories/category_repository.py
Normal file
28
app/repositories/category_repository.py
Normal file
@@ -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()
|
||||
17
app/schemas/category.py
Normal file
17
app/schemas/category.py
Normal file
@@ -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)
|
||||
26
app/services/category_service.py
Normal file
26
app/services/category_service.py
Normal file
@@ -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
|
||||
@@ -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()
|
||||
|
||||
147
app/worker.py
147
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"
|
||||
|
||||
|
||||
32
tests/api/test_categories_api.py
Normal file
32
tests/api/test_categories_api.py
Normal file
@@ -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"]
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
96
transactions.csv
Normal file
96
transactions.csv
Normal file
@@ -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,
|
||||
|
Reference in New Issue
Block a user