refactor: separate standalone and managed room configs
- veins → shunts rename - add cfg/standalone/ and cfg/<room>/ structure - remove old data/*.json (moved to cfg/<room>/data/) - update build.py and ctrl scripts
This commit is contained in:
13
artery/shunts/amar/.env.example
Normal file
13
artery/shunts/amar/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Amar (MOCK) Vein Configuration
|
||||
API_PORT=8005
|
||||
|
||||
# Mock data settings
|
||||
MOCK_DATA_PATH=./mock_data
|
||||
|
||||
# Mock behavior
|
||||
ENABLE_RANDOM_DELAYS=true
|
||||
MIN_DELAY_MS=100
|
||||
MAX_DELAY_MS=500
|
||||
|
||||
# Simulate errors
|
||||
ERROR_RATE=0.0 # 0.0 to 1.0 (0% to 100%)
|
||||
173
artery/shunts/amar/README.md
Normal file
173
artery/shunts/amar/README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Amar (MOCK) Vein
|
||||
|
||||
Mock Amar API for testing - returns predictable test data for turnero flow testing.
|
||||
|
||||
## Purpose
|
||||
|
||||
Enables testing of the turnero flow (VET-535-540) without hitting the real Amar backend:
|
||||
- Guest petowner creation (VET-536)
|
||||
- Pet creation (VET-537)
|
||||
- Cart creation and price calculation (VET-538)
|
||||
- Service/category filtering (VET-539, VET-540)
|
||||
- Service request creation
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Start the mock server
|
||||
python run.py
|
||||
|
||||
# API docs: http://localhost:8005/docs
|
||||
# Health check: http://localhost:8005/health
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy `.env.example` to `.env` and adjust:
|
||||
|
||||
```bash
|
||||
API_PORT=8005 # Server port
|
||||
ENABLE_RANDOM_DELAYS=true # Add realistic delays
|
||||
MIN_DELAY_MS=100 # Minimum delay
|
||||
MAX_DELAY_MS=500 # Maximum delay
|
||||
ERROR_RATE=0.0 # Error rate (0.0 to 1.0)
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Turnero Flow (VET-536-540)
|
||||
|
||||
```bash
|
||||
# Create guest petowner (VET-536)
|
||||
POST /api/v1/pet-owners/
|
||||
{
|
||||
"address": "Av. Santa Fe 1234, Palermo"
|
||||
}
|
||||
|
||||
# Create pet (VET-537)
|
||||
POST /api/v1/pets/
|
||||
{
|
||||
"owner_id": 1234,
|
||||
"name": "Luna",
|
||||
"species": "DOG",
|
||||
"age": 3,
|
||||
"age_unit": "years"
|
||||
}
|
||||
|
||||
# Create cart (VET-538)
|
||||
POST /api/v1/cart/
|
||||
{
|
||||
"owner_id": 1234
|
||||
}
|
||||
|
||||
# Add item to cart
|
||||
POST /api/v1/cart/{cart_id}/items/
|
||||
{
|
||||
"service_id": 1,
|
||||
"pet_id": 5678,
|
||||
"quantity": 1
|
||||
}
|
||||
|
||||
# List services (VET-540)
|
||||
GET /api/v1/services/?species=DOG&neighborhood_id=1
|
||||
|
||||
# List categories (VET-539)
|
||||
GET /api/v1/categories/?species=DOG&neighborhood_id=1
|
||||
|
||||
# Create service request
|
||||
POST /solicitudes/service-requests/?cart_id=12345
|
||||
```
|
||||
|
||||
### Mock Control
|
||||
|
||||
```bash
|
||||
# Get mock database stats
|
||||
GET /mock/stats
|
||||
|
||||
# Reset mock database
|
||||
GET /mock/reset
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
All responses include `_mock: true` to identify mock data:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1234,
|
||||
"address": "Av. Santa Fe 1234, Palermo",
|
||||
"is_guest": true,
|
||||
"_mock": true
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Complete Turnero Flow
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8005"
|
||||
|
||||
# Step 1: Create guest petowner
|
||||
owner_resp = requests.post(f"{BASE_URL}/api/v1/pet-owners/", json={
|
||||
"address": "Av. Santa Fe 1234, Palermo"
|
||||
})
|
||||
owner = owner_resp.json()
|
||||
|
||||
# Step 2: Create pet
|
||||
pet_resp = requests.post(f"{BASE_URL}/api/v1/pets/", json={
|
||||
"owner_id": owner["id"],
|
||||
"name": "Luna",
|
||||
"species": "DOG",
|
||||
"age": 3,
|
||||
"age_unit": "years"
|
||||
})
|
||||
pet = pet_resp.json()
|
||||
|
||||
# Step 3: Create cart
|
||||
cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/", json={
|
||||
"owner_id": owner["id"]
|
||||
})
|
||||
cart = cart_resp.json()
|
||||
|
||||
# Step 4: Get available services
|
||||
services_resp = requests.get(
|
||||
f"{BASE_URL}/api/v1/services/",
|
||||
params={"species": "DOG", "neighborhood_id": owner["neighborhood"]["id"]}
|
||||
)
|
||||
services = services_resp.json()
|
||||
|
||||
# Step 5: Add service to cart
|
||||
cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/{cart['id']}/items/", json={
|
||||
"service_id": services[0]["id"],
|
||||
"pet_id": pet["id"],
|
||||
"quantity": 1
|
||||
})
|
||||
cart = cart_resp.json()
|
||||
print(f"Cart total: ${cart['resume']['total']}")
|
||||
|
||||
# Step 6: Create service request
|
||||
request_resp = requests.post(
|
||||
f"{BASE_URL}/solicitudes/service-requests/",
|
||||
params={"cart_id": cart["id"]}
|
||||
)
|
||||
service_request = request_resp.json()
|
||||
print(f"Service request created: {service_request['id']}")
|
||||
```
|
||||
|
||||
## Data Generator
|
||||
|
||||
This vein uses the independent `datagen` tool from `ward/tools/datagen/amar.py`.
|
||||
See `ward/tools/datagen/README.md` for data generation details.
|
||||
|
||||
## Notes
|
||||
|
||||
- Mock database is in-memory (resets on server restart)
|
||||
- Use `/mock/reset` to clear data during testing
|
||||
- All IDs are randomly generated on creation
|
||||
- Multi-pet discounts are automatically calculated
|
||||
1
artery/shunts/amar/__init__.py
Normal file
1
artery/shunts/amar/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Amar (MOCK) vein - Mock Amar API for testing."""
|
||||
1
artery/shunts/amar/api/__init__.py
Normal file
1
artery/shunts/amar/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API routes for Amar mock vein."""
|
||||
302
artery/shunts/amar/api/routes.py
Normal file
302
artery/shunts/amar/api/routes.py
Normal file
@@ -0,0 +1,302 @@
|
||||
"""API routes for Amar (MOCK) vein - Mock Amar API for testing."""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
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.amar import AmarDataGenerator
|
||||
|
||||
from ..core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# In-memory storage for mock data (reset on restart)
|
||||
MOCK_DB = {
|
||||
"petowners": {},
|
||||
"pets": {},
|
||||
"carts": {},
|
||||
"service_requests": {},
|
||||
}
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class CreatePetOwnerRequest(BaseModel):
|
||||
address: str
|
||||
email: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
|
||||
|
||||
class CreatePetRequest(BaseModel):
|
||||
owner_id: int
|
||||
name: str
|
||||
species: str # DOG, CAT
|
||||
age: Optional[int] = None
|
||||
age_unit: str = "years" # years, months
|
||||
|
||||
|
||||
class CreateCartRequest(BaseModel):
|
||||
owner_id: int
|
||||
|
||||
|
||||
class AddCartItemRequest(BaseModel):
|
||||
service_id: int
|
||||
pet_id: int
|
||||
quantity: int = 1
|
||||
|
||||
|
||||
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 failure")
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint."""
|
||||
return {
|
||||
"status": "ok",
|
||||
"vein": "Amar\n(MOCK)",
|
||||
"message": "Mock Amar API for testing",
|
||||
"_mock": True,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/api/v1/pet-owners/")
|
||||
async def create_petowner(request: CreatePetOwnerRequest):
|
||||
"""Create a guest petowner (VET-536)."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
owner = AmarDataGenerator.petowner(
|
||||
address=request.address,
|
||||
is_guest=True,
|
||||
email=request.email,
|
||||
phone=request.phone,
|
||||
)
|
||||
|
||||
# Store in mock DB
|
||||
MOCK_DB["petowners"][owner["id"]] = owner
|
||||
|
||||
owner["_mock"] = True
|
||||
return owner
|
||||
|
||||
|
||||
@router.get("/api/v1/pet-owners/{owner_id}")
|
||||
async def get_petowner(owner_id: int):
|
||||
"""Get petowner by ID."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
owner = MOCK_DB["petowners"].get(owner_id)
|
||||
if not owner:
|
||||
raise HTTPException(404, f"PetOwner {owner_id} not found")
|
||||
|
||||
return owner
|
||||
|
||||
|
||||
@router.post("/api/v1/pets/")
|
||||
async def create_pet(request: CreatePetRequest):
|
||||
"""Create a pet (VET-537)."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
# Verify owner exists
|
||||
if request.owner_id not in MOCK_DB["petowners"]:
|
||||
raise HTTPException(404, f"Owner {request.owner_id} not found")
|
||||
|
||||
pet = AmarDataGenerator.pet(
|
||||
owner_id=request.owner_id,
|
||||
name=request.name,
|
||||
species=request.species,
|
||||
age_value=request.age,
|
||||
age_unit=request.age_unit,
|
||||
)
|
||||
|
||||
# Store in mock DB
|
||||
MOCK_DB["pets"][pet["id"]] = pet
|
||||
|
||||
pet["_mock"] = True
|
||||
return pet
|
||||
|
||||
|
||||
@router.get("/api/v1/pets/{pet_id}")
|
||||
async def get_pet(pet_id: int):
|
||||
"""Get pet by ID."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
pet = MOCK_DB["pets"].get(pet_id)
|
||||
if not pet:
|
||||
raise HTTPException(404, f"Pet {pet_id} not found")
|
||||
|
||||
return pet
|
||||
|
||||
|
||||
@router.post("/api/v1/cart/")
|
||||
async def create_cart(request: CreateCartRequest):
|
||||
"""Create a cart (VET-538)."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
# Verify owner exists
|
||||
if request.owner_id not in MOCK_DB["petowners"]:
|
||||
raise HTTPException(404, f"Owner {request.owner_id} not found")
|
||||
|
||||
cart = AmarDataGenerator.cart(owner_id=request.owner_id)
|
||||
|
||||
# Store in mock DB
|
||||
MOCK_DB["carts"][cart["id"]] = cart
|
||||
|
||||
cart["_mock"] = True
|
||||
return cart
|
||||
|
||||
|
||||
@router.post("/api/v1/cart/{cart_id}/items/")
|
||||
async def add_cart_item(cart_id: int, request: AddCartItemRequest):
|
||||
"""Add item to cart and recalculate summary."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
cart = MOCK_DB["carts"].get(cart_id)
|
||||
if not cart:
|
||||
raise HTTPException(404, f"Cart {cart_id} not found")
|
||||
|
||||
# Get service price
|
||||
services = AmarDataGenerator.SERVICES
|
||||
service = next((s for s in services if s["id"] == request.service_id), None)
|
||||
if not service:
|
||||
raise HTTPException(404, f"Service {request.service_id} not found")
|
||||
|
||||
# Add item
|
||||
item = {
|
||||
"service_id": request.service_id,
|
||||
"service_name": service["name"],
|
||||
"pet_id": request.pet_id,
|
||||
"quantity": request.quantity,
|
||||
"price": service["price"],
|
||||
}
|
||||
|
||||
cart["items"].append(item)
|
||||
|
||||
# Recalculate summary
|
||||
cart = AmarDataGenerator.calculate_cart_summary(cart, cart["items"])
|
||||
MOCK_DB["carts"][cart_id] = cart
|
||||
|
||||
cart["_mock"] = True
|
||||
return cart
|
||||
|
||||
|
||||
@router.get("/api/v1/cart/{cart_id}")
|
||||
async def get_cart(cart_id: int):
|
||||
"""Get cart by ID."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
cart = MOCK_DB["carts"].get(cart_id)
|
||||
if not cart:
|
||||
raise HTTPException(404, f"Cart {cart_id} not found")
|
||||
|
||||
return cart
|
||||
|
||||
|
||||
@router.get("/api/v1/services/")
|
||||
async def list_services(
|
||||
species: Optional[str] = Query(None),
|
||||
neighborhood_id: Optional[int] = Query(None),
|
||||
):
|
||||
"""List available services filtered by species and neighborhood (VET-540)."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
services = AmarDataGenerator.filter_services(
|
||||
species=species,
|
||||
neighborhood_id=neighborhood_id,
|
||||
)
|
||||
|
||||
for service in services:
|
||||
service["_mock"] = True
|
||||
|
||||
return services
|
||||
|
||||
|
||||
@router.get("/api/v1/categories/")
|
||||
async def list_categories(
|
||||
species: Optional[str] = Query(None),
|
||||
neighborhood_id: Optional[int] = Query(None),
|
||||
):
|
||||
"""List categories with available services (VET-539)."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
categories = AmarDataGenerator.filter_categories(
|
||||
species=species,
|
||||
neighborhood_id=neighborhood_id,
|
||||
)
|
||||
|
||||
for category in categories:
|
||||
category["_mock"] = True
|
||||
|
||||
return categories
|
||||
|
||||
|
||||
@router.post("/solicitudes/service-requests/")
|
||||
async def create_service_request(cart_id: int, requested_date: Optional[str] = None):
|
||||
"""Create a service request."""
|
||||
await _mock_delay()
|
||||
_maybe_error()
|
||||
|
||||
cart = MOCK_DB["carts"].get(cart_id)
|
||||
if not cart:
|
||||
raise HTTPException(404, f"Cart {cart_id} not found")
|
||||
|
||||
request = AmarDataGenerator.service_request(
|
||||
cart_id=cart_id,
|
||||
requested_date=requested_date,
|
||||
)
|
||||
|
||||
MOCK_DB["service_requests"][request["id"]] = request
|
||||
|
||||
request["_mock"] = True
|
||||
return request
|
||||
|
||||
|
||||
@router.get("/mock/reset")
|
||||
async def reset_mock_db():
|
||||
"""Reset the mock database (useful for testing)."""
|
||||
MOCK_DB["petowners"].clear()
|
||||
MOCK_DB["pets"].clear()
|
||||
MOCK_DB["carts"].clear()
|
||||
MOCK_DB["service_requests"].clear()
|
||||
|
||||
return {
|
||||
"message": "Mock database reset",
|
||||
"_mock": True,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/mock/stats")
|
||||
async def mock_stats():
|
||||
"""Get mock database statistics."""
|
||||
return {
|
||||
"petowners": len(MOCK_DB["petowners"]),
|
||||
"pets": len(MOCK_DB["pets"]),
|
||||
"carts": len(MOCK_DB["carts"]),
|
||||
"service_requests": len(MOCK_DB["service_requests"]),
|
||||
"_mock": True,
|
||||
}
|
||||
1
artery/shunts/amar/core/__init__.py
Normal file
1
artery/shunts/amar/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Core logic for Amar mock API."""
|
||||
27
artery/shunts/amar/core/config.py
Normal file
27
artery/shunts/amar/core/config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Configuration for Amar mock vein."""
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
ENV_FILE = Path(__file__).parent.parent / ".env"
|
||||
|
||||
|
||||
class AmarMockConfig(BaseSettings):
|
||||
"""Configuration for Amar (MOCK) vein."""
|
||||
|
||||
api_port: int = 8005
|
||||
mock_data_path: str = "./mock_data"
|
||||
|
||||
# Mock behavior
|
||||
enable_random_delays: bool = True
|
||||
min_delay_ms: int = 100
|
||||
max_delay_ms: int = 500
|
||||
error_rate: float = 0.0 # 0.0 to 1.0
|
||||
|
||||
model_config = {
|
||||
"env_file": ENV_FILE if ENV_FILE.exists() else None,
|
||||
"env_file_encoding": "utf-8",
|
||||
}
|
||||
|
||||
|
||||
settings = AmarMockConfig()
|
||||
40
artery/shunts/amar/main.py
Normal file
40
artery/shunts/amar/main.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Amar (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="Amar API (MOCK)",
|
||||
description="Mock Amar API for testing - returns predictable test data",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
# Enable CORS for testing from frontend
|
||||
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 Amar 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/shunts/amar/requirements.txt
Normal file
4
artery/shunts/amar/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/shunts/amar/run.py
Normal file
18
artery/shunts/amar/run.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Standalone runner for Amar 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 Amar (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)
|
||||
298
artery/shunts/amar/templates/index.html
Normal file
298
artery/shunts/amar/templates/index.html
Normal file
@@ -0,0 +1,298 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Amar 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: #f59e0b;
|
||||
color: #111827;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; }
|
||||
.subtitle { opacity: 0.8; font-size: 0.875rem; }
|
||||
.mock-badge {
|
||||
display: inline-block;
|
||||
background: #111827;
|
||||
color: #f59e0b;
|
||||
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: #f59e0b; background: #4b5563; }
|
||||
.endpoint-card.active { border-color: #f59e0b; 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 {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #374151;
|
||||
border: 1px solid #4b5563;
|
||||
border-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
.form-textarea { min-height: 200px; font-family: monospace; }
|
||||
.form-input:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
.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: #f59e0b;
|
||||
color: #111827;
|
||||
}
|
||||
.btn-primary:hover { background: #d97706; }
|
||||
.btn-secondary {
|
||||
background: #4b5563;
|
||||
color: #e5e7eb;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.btn-secondary:hover { background: #6b7280; }
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.stat-card {
|
||||
background: #374151;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.stat-value { font-size: 1.5rem; font-weight: 600; color: #f59e0b; }
|
||||
.stat-label { font-size: 0.75rem; color: #9ca3af; margin-top: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Amar API <span class="mock-badge">MOCK</span></h1>
|
||||
<div class="subtitle">Configure mock responses for Amar backend endpoints</div>
|
||||
</header>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="section">
|
||||
<div class="section-header">Mock Database Stats</div>
|
||||
<div class="stats" id="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">-</div>
|
||||
<div class="stat-label">Pet Owners</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">-</div>
|
||||
<div class="stat-label">Pets</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">-</div>
|
||||
<div class="stat-label">Carts</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">-</div>
|
||||
<div class="stat-label">Requests</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px;">
|
||||
<button class="btn btn-secondary" onclick="resetMock()">Reset Database</button>
|
||||
</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', '/api/v1/pet-owners/', 'petowner')">
|
||||
<div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
<span class="endpoint-path">/api/v1/pet-owners/</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">Create guest pet owner (VET-536)</div>
|
||||
</div>
|
||||
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/pets/', 'pet')">
|
||||
<div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
<span class="endpoint-path">/api/v1/pets/</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">Create pet (VET-537)</div>
|
||||
</div>
|
||||
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/cart/', 'cart')">
|
||||
<div>
|
||||
<span class="endpoint-method method-post">POST</span>
|
||||
<span class="endpoint-path">/api/v1/cart/</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">Create cart (VET-538)</div>
|
||||
</div>
|
||||
<div class="endpoint-card" onclick="selectEndpoint('GET', '/api/v1/services/', 'services')">
|
||||
<div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
<span class="endpoint-path">/api/v1/services/</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">List services (VET-540)</div>
|
||||
</div>
|
||||
<div class="endpoint-card" onclick="selectEndpoint('GET', '/api/v1/categories/', 'categories')">
|
||||
<div>
|
||||
<span class="endpoint-method method-get">GET</span>
|
||||
<span class="endpoint-path">/api/v1/categories/</span>
|
||||
</div>
|
||||
<div class="endpoint-desc">List categories (VET-539)</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": 123, "name": "Luna", "_mock": true}'></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:8005/api/v1/pet-owners/
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedEndpoint = null;
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const resp = await fetch('/mock/stats');
|
||||
const data = await resp.json();
|
||||
document.getElementById('stats').innerHTML = `
|
||||
<div class="stat-card"><div class="stat-value">${data.pet_owners || 0}</div><div class="stat-label">Pet Owners</div></div>
|
||||
<div class="stat-card"><div class="stat-value">${data.pets || 0}</div><div class="stat-label">Pets</div></div>
|
||||
<div class="stat-card"><div class="stat-value">${data.carts || 0}</div><div class="stat-label">Carts</div></div>
|
||||
<div class="stat-card"><div class="stat-value">${data.service_requests || 0}</div><div class="stat-label">Requests</div></div>
|
||||
`;
|
||||
} catch (e) {
|
||||
console.error('Failed to load stats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetMock() {
|
||||
if (confirm('Reset all mock data?')) {
|
||||
await fetch('/mock/reset');
|
||||
loadStats();
|
||||
alert('Mock database reset');
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
petowner: JSON.stringify({"id": 1234, "address": "Av Santa Fe 1234", "is_guest": true, "_mock": true}, null, 2),
|
||||
pet: JSON.stringify({"id": 5678, "name": "Luna", "species": "DOG", "age": 3, "_mock": true}, null, 2),
|
||||
cart: JSON.stringify({"id": 9012, "items": [], "total": 0, "_mock": true}, null, 2),
|
||||
services: JSON.stringify([{"id": 1, "name": "Vacunación", "price": 1500, "_mock": true}], null, 2),
|
||||
categories: JSON.stringify([{"id": 1, "name": "Salud", "services": 5, "_mock": true}], 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'));
|
||||
}
|
||||
|
||||
loadStats();
|
||||
setInterval(loadStats, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
16
artery/shunts/mercadopago/.env.example
Normal file
16
artery/shunts/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/shunts/mercadopago/README.md
Normal file
263
artery/shunts/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/shunts/mercadopago/__init__.py
Normal file
1
artery/shunts/mercadopago/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""MercadoPago (MOCK) vein - Mock MercadoPago API for testing."""
|
||||
1
artery/shunts/mercadopago/api/__init__.py
Normal file
1
artery/shunts/mercadopago/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API routes for MercadoPago mock vein."""
|
||||
281
artery/shunts/mercadopago/api/routes.py
Normal file
281
artery/shunts/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/shunts/mercadopago/core/__init__.py
Normal file
1
artery/shunts/mercadopago/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Core logic for MercadoPago mock API."""
|
||||
30
artery/shunts/mercadopago/core/config.py
Normal file
30
artery/shunts/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/shunts/mercadopago/main.py
Normal file
40
artery/shunts/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/shunts/mercadopago/requirements.txt
Normal file
4
artery/shunts/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/shunts/mercadopago/run.py
Normal file
18
artery/shunts/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/shunts/mercadopago/templates/index.html
Normal file
296
artery/shunts/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