major restructure
This commit is contained in:
13
cfg/amar/artery/shunts/amar/.env.example
Normal file
13
cfg/amar/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
cfg/amar/artery/shunts/amar/README.md
Normal file
173
cfg/amar/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
cfg/amar/artery/shunts/amar/__init__.py
Normal file
1
cfg/amar/artery/shunts/amar/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Amar (MOCK) vein - Mock Amar API for testing."""
|
||||
1
cfg/amar/artery/shunts/amar/api/__init__.py
Normal file
1
cfg/amar/artery/shunts/amar/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API routes for Amar mock vein."""
|
||||
302
cfg/amar/artery/shunts/amar/api/routes.py
Normal file
302
cfg/amar/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
cfg/amar/artery/shunts/amar/core/__init__.py
Normal file
1
cfg/amar/artery/shunts/amar/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Core logic for Amar mock API."""
|
||||
27
cfg/amar/artery/shunts/amar/core/config.py
Normal file
27
cfg/amar/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
cfg/amar/artery/shunts/amar/main.py
Normal file
40
cfg/amar/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
cfg/amar/artery/shunts/amar/requirements.txt
Normal file
4
cfg/amar/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
cfg/amar/artery/shunts/amar/run.py
Normal file
18
cfg/amar/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
cfg/amar/artery/shunts/amar/templates/index.html
Normal file
298
cfg/amar/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>
|
||||
Reference in New Issue
Block a user