"""API routes for MercadoPago (MOCK) vein - Mock MercadoPago API for testing.""" import asyncio import random from fastapi import APIRouter, HTTPException, Query, Header from fastapi.responses import JSONResponse from typing import Optional, List, Dict, Any from pydantic import BaseModel # Import datagen from ward/tools import sys from pathlib import Path ward_tools_path = Path(__file__).parent.parent.parent.parent.parent / "ward" / "tools" sys.path.insert(0, str(ward_tools_path)) from datagen.mercadopago import MercadoPagoDataGenerator from ..core.config import settings router = APIRouter() # In-memory storage for mock data MOCK_DB = { "preferences": {}, "payments": {}, "merchant_orders": {}, "tokens": {}, } # Request/Response Models class PreferenceItem(BaseModel): title: str quantity: int unit_price: float currency_id: str = "ARS" class CreatePreferenceRequest(BaseModel): items: List[PreferenceItem] external_reference: Optional[str] = None back_urls: Optional[Dict[str, str]] = None notification_url: Optional[str] = None auto_return: str = "approved" class CreatePaymentRequest(BaseModel): transaction_amount: float description: str payment_method_id: str payer: Dict[str, Any] application_fee: Optional[float] = None token: Optional[str] = None installments: Optional[int] = 1 issuer_id: Optional[str] = None async def _mock_delay(): """Add realistic delay if enabled.""" if settings.enable_random_delays: delay_ms = random.randint(settings.min_delay_ms, settings.max_delay_ms) await asyncio.sleep(delay_ms / 1000) def _maybe_error(): """Randomly raise an error based on error_rate.""" if random.random() < settings.error_rate: raise HTTPException(500, "Mock error: Simulated MercadoPago failure") @router.get("/health") async def health(): """Health check endpoint.""" return { "status": "ok", "vein": "MercadoPago\n(MOCK)", "message": "Mock MercadoPago API for testing", "_mock": "MercadoPago", } @router.post("/v1/preferences") async def create_preference(request: CreatePreferenceRequest): """Create a Checkout Pro preference (payment link).""" await _mock_delay() _maybe_error() # Calculate total from items total = sum(item.unit_price * item.quantity for item in request.items) # Generate preference preference = MercadoPagoDataGenerator.preference( description=request.items[0].title if request.items else "Payment", total=total, external_reference=request.external_reference, ) # Store in mock DB MOCK_DB["preferences"][preference["id"]] = preference return preference @router.get("/v1/preferences/{preference_id}") async def get_preference(preference_id: str): """Get preference by ID.""" await _mock_delay() _maybe_error() preference = MOCK_DB["preferences"].get(preference_id) if not preference: raise HTTPException(404, {"message": "Preference not found", "error": "not_found", "status": 404}) return preference @router.post("/v1/payments") async def create_payment( request: CreatePaymentRequest, x_idempotency_key: Optional[str] = Header(None), ): """Create a payment (Checkout API/Bricks).""" await _mock_delay() _maybe_error() # Use configured default status or random status = settings.default_payment_status if status not in ["approved", "pending", "rejected"]: status = random.choice(["approved", "pending", "rejected"]) # Generate payment payment = MercadoPagoDataGenerator.payment( transaction_amount=request.transaction_amount, description=request.description, status=status, application_fee=request.application_fee, ) # Override with request payment method payment["payment_method_id"] = request.payment_method_id payment["payer"] = request.payer # Store in mock DB MOCK_DB["payments"][payment["id"]] = payment return payment @router.get("/v1/payments/{payment_id}") async def get_payment(payment_id: int): """Get payment details.""" await _mock_delay() _maybe_error() payment = MOCK_DB["payments"].get(payment_id) if not payment: raise HTTPException(404, {"message": "Payment not found", "error": "not_found", "status": 404}) return payment @router.get("/v1/merchant_orders/{order_id}") async def get_merchant_order(order_id: int): """Get merchant order details.""" await _mock_delay() _maybe_error() order = MOCK_DB["merchant_orders"].get(order_id) if not order: # Generate on-the-fly if not found order = MercadoPagoDataGenerator.merchant_order( preference_id=f"pref_{order_id}", total=100000, paid_amount=100000, ) order["id"] = order_id MOCK_DB["merchant_orders"][order_id] = order return order @router.post("/oauth/token") async def oauth_token( grant_type: str = "refresh_token", client_id: str = None, client_secret: str = None, refresh_token: str = None, code: str = None, ): """OAuth token exchange/refresh.""" await _mock_delay() _maybe_error() if grant_type == "refresh_token": if not refresh_token: raise HTTPException(400, {"error": "invalid_request", "error_description": "refresh_token is required"}) # Generate new tokens token_data = MercadoPagoDataGenerator.oauth_token() MOCK_DB["tokens"][token_data["access_token"]] = token_data return token_data elif grant_type == "authorization_code": if not code: raise HTTPException(400, {"error": "invalid_request", "error_description": "code is required"}) # Generate tokens from code token_data = MercadoPagoDataGenerator.oauth_token() MOCK_DB["tokens"][token_data["access_token"]] = token_data return token_data else: raise HTTPException(400, {"error": "unsupported_grant_type"}) @router.post("/mock/webhook") async def simulate_webhook( topic: str = "payment", resource_id: str = None, ): """Simulate a webhook notification (for testing).""" await _mock_delay() if not resource_id: raise HTTPException(400, "resource_id is required") notification = MercadoPagoDataGenerator.webhook_notification( topic=topic, resource_id=resource_id, ) return notification @router.get("/mock/reset") async def reset_mock_db(): """Reset the mock database.""" MOCK_DB["preferences"].clear() MOCK_DB["payments"].clear() MOCK_DB["merchant_orders"].clear() MOCK_DB["tokens"].clear() return { "message": "Mock database reset", "_mock": "MercadoPago", } @router.get("/mock/stats") async def mock_stats(): """Get mock database statistics.""" return { "preferences": len(MOCK_DB["preferences"]), "payments": len(MOCK_DB["payments"]), "merchant_orders": len(MOCK_DB["merchant_orders"]), "tokens": len(MOCK_DB["tokens"]), "_mock": "MercadoPago", } @router.post("/mock/config") async def update_mock_config( default_payment_status: Optional[str] = None, error_rate: Optional[float] = None, ): """Update mock configuration (for testing different scenarios).""" if default_payment_status: if default_payment_status not in ["approved", "pending", "rejected", "in_process", "cancelled"]: raise HTTPException(400, "Invalid payment status") settings.default_payment_status = default_payment_status if error_rate is not None: if not (0 <= error_rate <= 1): raise HTTPException(400, "error_rate must be between 0 and 1") settings.error_rate = error_rate return { "default_payment_status": settings.default_payment_status, "error_rate": settings.error_rate, "_mock": "MercadoPago", }