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:
buenosairesam
2026-01-02 17:09:58 -03:00
parent 46dc78db0e
commit 9e5cbbad1f
57 changed files with 1788 additions and 150 deletions

View 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%)

View 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

View File

@@ -0,0 +1 @@
"""Amar (MOCK) vein - Mock Amar API for testing."""

View File

@@ -0,0 +1 @@
"""API routes for Amar mock vein."""

View 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,
}

View File

@@ -0,0 +1 @@
"""Core logic for Amar mock API."""

View 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()

View 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)

View 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
View 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)

View 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>

View 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

View 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

View File

@@ -0,0 +1 @@
"""MercadoPago (MOCK) vein - Mock MercadoPago API for testing."""

View File

@@ -0,0 +1 @@
"""API routes for MercadoPago mock vein."""

View 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",
}

View File

@@ -0,0 +1 @@
"""Core logic for MercadoPago mock API."""

View 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()

View 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)

View File

@@ -0,0 +1,4 @@
fastapi>=0.104.0
uvicorn>=0.24.0
pydantic>=2.0.0
pydantic-settings>=2.0.0

View 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)

View 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>