Initial PayGuard fraud detection system implementation

- 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]>
This commit is contained in:
John Patrick TeruelandCopilot committed 2026-04-24 19:47:18 +08:00
commit aa4dd497e2
24 files changed
+2343

No files matched your search

Binary file not shown.
+216
View File
@@ -0,0 +1,216 @@
"""
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"
)