- Complete dual-layer architecture for fraud detection - Phase 1: Docker Compose infrastructure (Kafka, Zookeeper, Redis) - Phase 2: Mock ML models (Layer 1 fast, Layer 2 deep) - Phase 3: FastAPI transaction ingestion with JWT auth - Phase 4: Kafka consumer pipeline with feature extraction - Phase 5: Test transaction producer with demo scenarios - Phase 6: Clean code structure and comprehensive documentation - Phase 7: End-to-end validation and demo readiness Demonstrates key system design patterns: - Microservices architecture with decoupled components - Asynchronous event processing via Kafka - Tiered decision-making (fast path vs deep analysis) - Resilience patterns (circuit breaker for graceful degradation) - Behavioral profiling with Redis caching Ready for academic demonstration with clear pipeline observability. Co-authored-by: Copilot <[email protected]>
217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
"""
|
|
PayGuard Transaction Ingestion API
|
|
|
|
Responsibilities:
|
|
- Authenticate incoming requests using JWT
|
|
- Validate transaction data structure
|
|
- Publish transactions to Kafka asynchronously
|
|
- Return immediately without waiting for fraud analysis (demonstrates decoupling)
|
|
|
|
Academic Point:
|
|
The API does not process transactions directly. It simply validates identity and
|
|
hands off responsibility to the message queue. This separation of concerns is
|
|
fundamental to scalable microservices architecture.
|
|
"""
|
|
|
|
from fastapi import FastAPI, Depends, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from pydantic import BaseModel
|
|
from jose import jwt, JWTError
|
|
from kafka import KafkaProducer
|
|
import json
|
|
import uuid
|
|
import time
|
|
import logging
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CONFIGURATION
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
# Secret key for JWT signing (for demo purposes only)
|
|
SECRET_KEY = "payguard-demo-secret-key-change-in-production"
|
|
ALGORITHM = "HS256"
|
|
|
|
# Kafka bootstrap server
|
|
KAFKA_BROKER = "localhost:9092"
|
|
KAFKA_TOPIC = "transactions"
|
|
|
|
# Logging setup
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s [%(levelname)s] %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# MODELS
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class TransactionRequest(BaseModel):
|
|
"""Request body for transaction submission"""
|
|
amount: float
|
|
merchant: str
|
|
description: str = ""
|
|
|
|
class TransactionResponse(BaseModel):
|
|
"""Response after transaction is accepted"""
|
|
status: str
|
|
transaction_id: str
|
|
message: str
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# KAFKA PRODUCER
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
kafka_producer = KafkaProducer(
|
|
bootstrap_servers=KAFKA_BROKER,
|
|
value_serializer=lambda v: json.dumps(v).encode('utf-8'),
|
|
acks='all',
|
|
retries=3
|
|
)
|
|
|
|
logger.info(f"[KAFKA PRODUCER] Initialized, broker={KAFKA_BROKER}, topic={KAFKA_TOPIC}")
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# AUTHENTICATION
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
security = HTTPBearer()
|
|
|
|
def verify_jwt(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
|
|
"""
|
|
Verify JWT token and extract user information.
|
|
|
|
Academic Point: In a real system, this would validate against a user database
|
|
and use proper token refresh mechanisms. For demo, we accept any valid JWT.
|
|
"""
|
|
try:
|
|
token = credentials.credentials
|
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
user_id = payload.get("sub")
|
|
|
|
if user_id is None:
|
|
logger.warning("[JWT AUTH] Token missing 'sub' claim")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token: missing user ID"
|
|
)
|
|
|
|
logger.info(f"[JWT AUTH] ✓ Token verified for user={user_id}")
|
|
return {"user_id": user_id}
|
|
|
|
except JWTError as e:
|
|
logger.warning(f"[JWT AUTH] ✗ Token validation failed: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid or expired token"
|
|
)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# API APPLICATION
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
app = FastAPI(
|
|
title="PayGuard Fraud Detection API",
|
|
description="Transaction ingestion layer for fraud detection pipeline",
|
|
version="1.0.0"
|
|
)
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""Health check endpoint"""
|
|
return {"status": "healthy", "service": "payguard-api"}
|
|
|
|
@app.post("/transaction", response_model=TransactionResponse)
|
|
async def submit_transaction(
|
|
request: TransactionRequest,
|
|
user: dict = Depends(verify_jwt)
|
|
) -> TransactionResponse:
|
|
"""
|
|
Accept a transaction and publish it to Kafka for async processing.
|
|
|
|
Returns immediately without waiting for fraud analysis.
|
|
This is a key architectural pattern: decoupling ingestion from processing.
|
|
"""
|
|
|
|
user_id = user["user_id"]
|
|
transaction_id = str(uuid.uuid4())
|
|
timestamp = time.time()
|
|
|
|
# Build transaction event
|
|
event = {
|
|
"transaction_id": transaction_id,
|
|
"user_id": user_id,
|
|
"amount": request.amount,
|
|
"merchant": request.merchant,
|
|
"description": request.description,
|
|
"timestamp": timestamp
|
|
}
|
|
|
|
logger.info(
|
|
f"[TRANSACTION INTAKE] Received from {user_id}: "
|
|
f"${request.amount:.2f} at {request.merchant}"
|
|
)
|
|
|
|
# Publish to Kafka
|
|
try:
|
|
future = kafka_producer.send(KAFKA_TOPIC, event)
|
|
future.get(timeout=10) # Wait for confirmation
|
|
|
|
logger.info(
|
|
f"[KAFKA PUBLISH] ✓ Transaction {transaction_id} "
|
|
f"published to topic '{KAFKA_TOPIC}'"
|
|
)
|
|
|
|
return TransactionResponse(
|
|
status="accepted",
|
|
transaction_id=transaction_id,
|
|
message="Transaction accepted. Fraud analysis in progress."
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[KAFKA PUBLISH] ✗ Failed to publish: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="Message broker unavailable. Please try again."
|
|
)
|
|
|
|
@app.post("/token")
|
|
async def get_demo_token(user_id: str):
|
|
"""
|
|
Generate a demo JWT token for testing.
|
|
|
|
In production, this would validate credentials against a user database.
|
|
For demo purposes, accept any user_id.
|
|
"""
|
|
token = jwt.encode({"sub": user_id}, SECRET_KEY, algorithm=ALGORITHM)
|
|
logger.info(f"[JWT GENERATION] Generated token for user={user_id}")
|
|
return {"access_token": token, "token_type": "bearer"}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# STARTUP / SHUTDOWN
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
logger.info("="*80)
|
|
logger.info("PayGuard Transaction Ingestion API - STARTUP")
|
|
logger.info("="*80)
|
|
logger.info(f"Kafka Broker: {KAFKA_BROKER}")
|
|
logger.info(f"Topic: {KAFKA_TOPIC}")
|
|
logger.info("Listening for authenticated transaction requests...")
|
|
logger.info("="*80)
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
logger.info("Shutting down API...")
|
|
kafka_producer.close()
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
app,
|
|
host="0.0.0.0",
|
|
port=8000,
|
|
log_level="info"
|
|
)
|