migrated spr stuff
This commit is contained in:
16
artery/veins/mercadopago/.env.example
Normal file
16
artery/veins/mercadopago/.env.example
Normal file
@@ -0,0 +1,16 @@
|
||||
# MercadoPago (MOCK) Vein Configuration
|
||||
API_PORT=8006
|
||||
|
||||
# Mock data settings
|
||||
MOCK_DATA_PATH=./mock_data
|
||||
|
||||
# Mock behavior
|
||||
ENABLE_RANDOM_DELAYS=true
|
||||
MIN_DELAY_MS=200
|
||||
MAX_DELAY_MS=800
|
||||
|
||||
# Simulate errors
|
||||
ERROR_RATE=0.0 # 0.0 to 1.0 (0% to 100%)
|
||||
|
||||
# Default payment status for testing
|
||||
DEFAULT_PAYMENT_STATUS=approved # approved, pending, rejected
|
||||
263
artery/veins/mercadopago/README.md
Normal file
263
artery/veins/mercadopago/README.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# MercadoPago (MOCK) Vein
|
||||
|
||||
Mock MercadoPago API for testing - simulates payment processing without hitting the real MercadoPago API.
|
||||
|
||||
## Purpose
|
||||
|
||||
Enables testing of MercadoPago integration without:
|
||||
- Creating real payments
|
||||
- Connecting real MercadoPago accounts
|
||||
- Exposing credentials
|
||||
- Consuming API quotas
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Start the mock server
|
||||
python run.py
|
||||
|
||||
# API docs: http://localhost:8006/docs
|
||||
# Health check: http://localhost:8006/health
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy `.env.example` to `.env` and adjust:
|
||||
|
||||
```bash
|
||||
API_PORT=8006 # Server port
|
||||
ENABLE_RANDOM_DELAYS=true # Add realistic delays
|
||||
MIN_DELAY_MS=200 # Minimum delay
|
||||
MAX_DELAY_MS=800 # Maximum delay
|
||||
ERROR_RATE=0.0 # Error rate (0.0 to 1.0)
|
||||
DEFAULT_PAYMENT_STATUS=approved # approved, pending, rejected
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Checkout Pro (Preferences)
|
||||
|
||||
```bash
|
||||
# Create payment link
|
||||
POST /v1/preferences
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"title": "Visita a domicilio",
|
||||
"quantity": 1,
|
||||
"unit_price": 95000,
|
||||
"currency_id": "ARS"
|
||||
}
|
||||
],
|
||||
"external_reference": "SR-12345",
|
||||
"back_urls": {
|
||||
"success": "https://backoffice.amarmascotas.ar/pagos/success/",
|
||||
"pending": "https://backoffice.amarmascotas.ar/pagos/pending/",
|
||||
"failure": "https://backoffice.amarmascotas.ar/pagos/failure/"
|
||||
},
|
||||
"notification_url": "https://backoffice.amarmascotas.ar/payments/mp/webhook/"
|
||||
}
|
||||
|
||||
# Get preference
|
||||
GET /v1/preferences/{preference_id}
|
||||
```
|
||||
|
||||
### Checkout API (Payments)
|
||||
|
||||
```bash
|
||||
# Create payment
|
||||
POST /v1/payments
|
||||
Headers:
|
||||
X-Idempotency-Key: unique-key-123
|
||||
Body:
|
||||
{
|
||||
"transaction_amount": 95000,
|
||||
"description": "Visita a domicilio",
|
||||
"payment_method_id": "visa",
|
||||
"payer": {
|
||||
"email": "test@example.com",
|
||||
"identification": {
|
||||
"type": "DNI",
|
||||
"number": "12345678"
|
||||
}
|
||||
},
|
||||
"application_fee": 45000
|
||||
}
|
||||
|
||||
# Get payment details
|
||||
GET /v1/payments/{payment_id}
|
||||
```
|
||||
|
||||
### OAuth
|
||||
|
||||
```bash
|
||||
# Exchange authorization code for tokens
|
||||
POST /oauth/token
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "APP_ID",
|
||||
"client_secret": "APP_SECRET",
|
||||
"code": "AUTH_CODE"
|
||||
}
|
||||
|
||||
# Refresh access token
|
||||
POST /oauth/token
|
||||
{
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": "APP_ID",
|
||||
"client_secret": "APP_SECRET",
|
||||
"refresh_token": "REFRESH_TOKEN"
|
||||
}
|
||||
```
|
||||
|
||||
### Mock Control
|
||||
|
||||
```bash
|
||||
# Get mock database stats
|
||||
GET /mock/stats
|
||||
|
||||
# Reset mock database
|
||||
GET /mock/reset
|
||||
|
||||
# Update mock configuration
|
||||
POST /mock/config
|
||||
{
|
||||
"default_payment_status": "approved",
|
||||
"error_rate": 0.1
|
||||
}
|
||||
|
||||
# Simulate webhook notification
|
||||
POST /mock/webhook?topic=payment&resource_id=12345
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
All responses include `_mock: "MercadoPago"` to identify mock data:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 123456789,
|
||||
"status": "approved",
|
||||
"transaction_amount": 95000,
|
||||
"_mock": "MercadoPago"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Test Payment Link Creation
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8006"
|
||||
|
||||
# Create preference
|
||||
pref_resp = requests.post(f"{BASE_URL}/v1/preferences", json={
|
||||
"items": [{
|
||||
"title": "Visita a domicilio",
|
||||
"quantity": 1,
|
||||
"unit_price": 95000,
|
||||
"currency_id": "ARS"
|
||||
}],
|
||||
"external_reference": "SR-12345"
|
||||
})
|
||||
pref = pref_resp.json()
|
||||
print(f"Payment link: {pref['init_point']}")
|
||||
```
|
||||
|
||||
### Test Direct Payment with Split
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8006"
|
||||
|
||||
# Create payment with application fee (split)
|
||||
payment_resp = requests.post(
|
||||
f"{BASE_URL}/v1/payments",
|
||||
headers={"X-Idempotency-Key": "unique-123"},
|
||||
json={
|
||||
"transaction_amount": 95000,
|
||||
"description": "Visita a domicilio",
|
||||
"payment_method_id": "visa",
|
||||
"payer": {
|
||||
"email": "test@example.com",
|
||||
"identification": {"type": "DNI", "number": "12345678"}
|
||||
},
|
||||
"application_fee": 45000 # Platform fee
|
||||
}
|
||||
)
|
||||
payment = payment_resp.json()
|
||||
print(f"Payment status: {payment['status']}")
|
||||
print(f"Net amount (for vet): ${payment['net_amount']}")
|
||||
```
|
||||
|
||||
### Test Different Payment Statuses
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8006"
|
||||
|
||||
# Configure mock to return rejected payments
|
||||
requests.post(f"{BASE_URL}/mock/config", json={
|
||||
"default_payment_status": "rejected"
|
||||
})
|
||||
|
||||
# Now all payments will be rejected
|
||||
payment_resp = requests.post(f"{BASE_URL}/v1/payments", json={...})
|
||||
print(payment_resp.json()["status"]) # "rejected"
|
||||
|
||||
# Reset to approved
|
||||
requests.post(f"{BASE_URL}/mock/config", json={
|
||||
"default_payment_status": "approved"
|
||||
})
|
||||
```
|
||||
|
||||
### Test Error Scenarios
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8006"
|
||||
|
||||
# Configure 50% error rate
|
||||
requests.post(f"{BASE_URL}/mock/config", json={
|
||||
"error_rate": 0.5
|
||||
})
|
||||
|
||||
# Half of requests will now fail with 500 error
|
||||
for i in range(10):
|
||||
try:
|
||||
resp = requests.post(f"{BASE_URL}/v1/payments", json={...})
|
||||
print(f"Request {i}: Success")
|
||||
except:
|
||||
print(f"Request {i}: Failed")
|
||||
```
|
||||
|
||||
## Data Generator
|
||||
|
||||
This vein uses the independent `datagen` tool from `ward/tools/datagen/mercadopago.py`.
|
||||
See `ward/tools/datagen/README.md` for data generation details.
|
||||
|
||||
## Integration with Amar Backend
|
||||
|
||||
Point your Amar backend to the mock MercadoPago API:
|
||||
|
||||
```python
|
||||
# settings.py or .env
|
||||
MP_PLATFORM_ACCESS_TOKEN = "mock_token" # Any value works
|
||||
MP_API_BASE_URL = "http://localhost:8006" # Point to mock
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Mock database is in-memory (resets on server restart)
|
||||
- All payment IDs are randomly generated
|
||||
- Payment status can be configured via `/mock/config`
|
||||
- Webhook notifications can be simulated via `/mock/webhook`
|
||||
- OAuth tokens are generated but not validated
|
||||
1
artery/veins/mercadopago/__init__.py
Normal file
1
artery/veins/mercadopago/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""MercadoPago (MOCK) vein - Mock MercadoPago API for testing."""
|
||||
1
artery/veins/mercadopago/api/__init__.py
Normal file
1
artery/veins/mercadopago/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API routes for MercadoPago mock vein."""
|
||||
281
artery/veins/mercadopago/api/routes.py
Normal file
281
artery/veins/mercadopago/api/routes.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""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",
|
||||
}
|
||||
1
artery/veins/mercadopago/core/__init__.py
Normal file
1
artery/veins/mercadopago/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Core logic for MercadoPago mock API."""
|
||||
30
artery/veins/mercadopago/core/config.py
Normal file
30
artery/veins/mercadopago/core/config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Configuration for MercadoPago mock vein."""
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
ENV_FILE = Path(__file__).parent.parent / ".env"
|
||||
|
||||
|
||||
class MercadoPagoMockConfig(BaseSettings):
|
||||
"""Configuration for MercadoPago (MOCK) vein."""
|
||||
|
||||
api_port: int = 8006
|
||||
mock_data_path: str = "./mock_data"
|
||||
|
||||
# Mock behavior
|
||||
enable_random_delays: bool = True
|
||||
min_delay_ms: int = 200
|
||||
max_delay_ms: int = 800
|
||||
error_rate: float = 0.0 # 0.0 to 1.0
|
||||
|
||||
# Default payment status
|
||||
default_payment_status: str = "approved" # approved, pending, rejected
|
||||
|
||||
model_config = {
|
||||
"env_file": ENV_FILE if ENV_FILE.exists() else None,
|
||||
"env_file_encoding": "utf-8",
|
||||
}
|
||||
|
||||
|
||||
settings = MercadoPagoMockConfig()
|
||||
40
artery/veins/mercadopago/main.py
Normal file
40
artery/veins/mercadopago/main.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""MercadoPago (MOCK) Vein - FastAPI app."""
|
||||
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from .api.routes import router
|
||||
from .core.config import settings
|
||||
|
||||
app = FastAPI(
|
||||
title="MercadoPago (MOCK)",
|
||||
description="Mock MercadoPago API for testing - simulates payment processing",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
# Enable CORS for testing from backend
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, specify exact origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Templates for configuration UI
|
||||
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index(request: Request):
|
||||
"""Mock configuration UI."""
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
# Include router at root (matches real MercadoPago API structure)
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=settings.api_port)
|
||||
4
artery/veins/mercadopago/requirements.txt
Normal file
4
artery/veins/mercadopago/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
pydantic>=2.0.0
|
||||
pydantic-settings>=2.0.0
|
||||
18
artery/veins/mercadopago/run.py
Normal file
18
artery/veins/mercadopago/run.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Standalone runner for MercadoPago mock vein."""
|
||||
|
||||
import logging
|
||||
import uvicorn
|
||||
from main import app
|
||||
from core.config import settings
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info(f"Starting MercadoPago (MOCK) vein on port {settings.api_port}")
|
||||
logger.info(f"API docs: http://localhost:{settings.api_port}/docs")
|
||||
logger.info(f"Health check: http://localhost:{settings.api_port}/health")
|
||||
uvicorn.run(app, host="0.0.0.0", port=settings.api_port, reload=True)
|
||||
296
artery/veins/mercadopago/templates/index.html
Normal file
296
artery/veins/mercadopago/templates/index.html
Normal file
@@ -0,0 +1,296 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MercadoPago API (MOCK) - Configuration</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #111827;
|
||||
color: #e5e7eb;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
header {
|
||||
background: #0071f2;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; }
|
||||
.subtitle { opacity: 0.9; font-size: 0.875rem; }
|
||||
.mock-badge {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
color: #0071f2;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.section {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-header {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #f9fafb;
|
||||
}
|
||||
.endpoint-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.endpoint-card {
|
||||
background: #374151;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.endpoint-card:hover { border-color: #0071f2; background: #4b5563; }
|
||||
.endpoint-card.active { border-color: #0071f2; background: #4b5563; }
|
||||
.endpoint-method {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.method-post { background: #10b981; color: white; }
|
||||
.method-get { background: #3b82f6; color: white; }
|
||||
.endpoint-path { font-family: monospace; font-size: 0.875rem; }
|
||||
.endpoint-desc { font-size: 0.75rem; color: #9ca3af; margin-top: 6px; }
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: #f9fafb;
|
||||
}
|
||||
.form-input, .form-textarea, .form-select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #374151;
|
||||
border: 1px solid #4b5563;
|
||||
border-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.form-textarea { min-height: 200px; font-family: monospace; }
|
||||
.form-input:focus, .form-textarea:focus, .form-select:focus {
|
||||
outline: none;
|
||||
border-color: #0071f2;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #0071f2;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { background: #005ac1; }
|
||||
.btn-secondary {
|
||||
background: #4b5563;
|
||||
color: #e5e7eb;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.btn-secondary:hover { background: #6b7280; }
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.status-option {
|
||||
background: #374151;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.status-option:hover { border-color: #0071f2; }
|
||||
.status-option.selected { border-color: #0071f2; background: #4b5563; }
|
||||
.status-name { font-weight: 600; color: #f9fafb; margin-bottom: 4px; }
|
||||
.status-desc { font-size: 0.75rem; color: #9ca3af; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>MercadoPago <span class="mock-badge">MOCK</span></h1>
|
||||
<div class="subtitle">Configure mock payment responses and behavior</div>
|
||||
</header>
|
||||
|
||||
<!-- Payment Status Configuration -->
|
||||
<div class="section">
|
||||
<div class="section-header">Default Payment Status</div>
|
||||
<p style="color: #9ca3af; margin-bottom: 16px;">Choose what status new payments should return:</p>
|
||||
<div class="status-grid">
|
||||
<div class="status-option selected" onclick="selectStatus('approved')">
|
||||
<div class="status-name">Approved</div>
|
||||
<div class="status-desc">Payment successful</div>
|
||||
</div>
|
||||
<div class="status-option" onclick="selectStatus('rejected')">
|
||||
<div class="status-name">Rejected</div>
|
||||
<div class="status-desc">Payment failed</div>
|
||||
</div>
|
||||
<div class="status-option" onclick="selectStatus('pending')">
|
||||
<div class="status-name">Pending</div>
|
||||
<div class="status-desc">Awaiting confirmation</div>
|
||||
</div>
|
||||
<div class="status-option" onclick="selectStatus('in_process')">
|
||||
<div class="status-name">In Process</div>
|
||||
<div class="status-desc">Being processed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint Configuration -->
|
||||
<div class="section">
|
||||
<div class="section-header">Configure Endpoint Responses</div>
|
||||
<div class="endpoint-list">
|
||||
<div class="endpoint-card" onclick="selectEndpoint('POST', '/checkout/preferences', 'preference')">
|
||||
<div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
<span class="endpoint-path">/checkout/preferences</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">Create payment preference (Checkout Pro)</div>
|
||||
</div>
|
||||
<div class="endpoint-card" onclick="selectEndpoint('POST', '/v1/payments', 'payment')">
|
||||
<div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
<span class="endpoint-path">/v1/payments</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">Create payment (Checkout API)</div>
|
||||
</div>
|
||||
<div class="endpoint-card" onclick="selectEndpoint('GET', '/v1/payments/{id}', 'payment_get')">
|
||||
<div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
<span class="endpoint-path">/v1/payments/{id}</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">Get payment details</div>
|
||||
</div>
|
||||
<div class="endpoint-card" onclick="selectEndpoint('POST', '/oauth/token', 'oauth')">
|
||||
<div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
<span class="endpoint-path">/oauth/token</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">OAuth token exchange/refresh</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response Editor -->
|
||||
<div class="section" id="responseEditor" style="display: none;">
|
||||
<div class="section-header">Edit Response</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Endpoint</label>
|
||||
<input class="form-input" id="endpointDisplay" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Mock Response (JSON)</label>
|
||||
<textarea class="form-textarea" id="responseJson" placeholder='{"id": "123456", "status": "approved", "_mock": "MercadoPago"}'></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">HTTP Status Code</label>
|
||||
<input type="number" class="form-input" id="statusCode" value="200">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Delay (ms)</label>
|
||||
<input type="number" class="form-input" id="delay" value="0">
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" onclick="saveResponse()">Save Response</button>
|
||||
<button class="btn btn-secondary" onclick="closeEditor()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Test -->
|
||||
<div class="section">
|
||||
<div class="section-header">Quick Test</div>
|
||||
<p style="color: #9ca3af; margin-bottom: 12px;">Test endpoint URL to hit for configured responses:</p>
|
||||
<div class="form-input" style="background: #374151; user-select: all;">
|
||||
http://localhost:8006/v1/payments
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedEndpoint = null;
|
||||
let selectedPaymentStatus = 'approved';
|
||||
|
||||
function selectStatus(status) {
|
||||
selectedPaymentStatus = status;
|
||||
document.querySelectorAll('.status-option').forEach(opt => opt.classList.remove('selected'));
|
||||
event.currentTarget.classList.add('selected');
|
||||
}
|
||||
|
||||
function selectEndpoint(method, path, type) {
|
||||
selectedEndpoint = {method, path, type};
|
||||
document.querySelectorAll('.endpoint-card').forEach(c => c.classList.remove('active'));
|
||||
event.currentTarget.classList.add('active');
|
||||
document.getElementById('responseEditor').style.display = 'block';
|
||||
document.getElementById('endpointDisplay').value = `${method} ${path}`;
|
||||
document.getElementById('responseJson').value = getDefaultResponse(type);
|
||||
}
|
||||
|
||||
function getDefaultResponse(type) {
|
||||
const defaults = {
|
||||
preference: JSON.stringify({
|
||||
"id": "123456-pref-id",
|
||||
"init_point": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=123456",
|
||||
"sandbox_init_point": "https://sandbox.mercadopago.com.ar/checkout/v1/redirect?pref_id=123456",
|
||||
"_mock": "MercadoPago"
|
||||
}, null, 2),
|
||||
payment: JSON.stringify({
|
||||
"id": 123456,
|
||||
"status": selectedPaymentStatus,
|
||||
"status_detail": selectedPaymentStatus === 'approved' ? 'accredited' : 'cc_rejected_other_reason',
|
||||
"transaction_amount": 1500,
|
||||
"currency_id": "ARS",
|
||||
"_mock": "MercadoPago"
|
||||
}, null, 2),
|
||||
payment_get: JSON.stringify({
|
||||
"id": 123456,
|
||||
"status": "approved",
|
||||
"status_detail": "accredited",
|
||||
"transaction_amount": 1500,
|
||||
"_mock": "MercadoPago"
|
||||
}, null, 2),
|
||||
oauth: JSON.stringify({
|
||||
"access_token": "APP_USR-123456-mock-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 15552000,
|
||||
"refresh_token": "TG-123456-mock-refresh",
|
||||
"_mock": "MercadoPago"
|
||||
}, null, 2)
|
||||
};
|
||||
return defaults[type] || '{}';
|
||||
}
|
||||
|
||||
function saveResponse() {
|
||||
alert('Mock response saved (feature pending implementation)');
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
document.getElementById('responseEditor').style.display = 'none';
|
||||
selectedEndpoint = null;
|
||||
document.querySelectorAll('.endpoint-card').forEach(c => c.classList.remove('active'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user