soleprint init commit
This commit is contained in:
164
station/tools/datagen/README.md
Normal file
164
station/tools/datagen/README.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Datagen - Test Data Generator
|
||||
|
||||
Pluggable test data generators for various domain models and external APIs.
|
||||
|
||||
## Purpose
|
||||
|
||||
- Generate realistic test data for Amar domain models
|
||||
- Generate mock API responses for external services (MercadoPago, etc.)
|
||||
- Can be plugged into any nest (test suites, mock veins, seeders)
|
||||
- Domain-agnostic and reusable
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
datagen/
|
||||
├── __init__.py
|
||||
├── amar.py # Amar domain models (petowner, pet, cart, etc.)
|
||||
├── mercadopago.py # MercadoPago API responses
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### In Tests
|
||||
|
||||
```python
|
||||
from ward.tools.datagen.amar import AmarDataGenerator
|
||||
|
||||
def test_petowner_creation():
|
||||
owner_data = AmarDataGenerator.petowner(address="Av. Corrientes 1234")
|
||||
assert owner_data["address"] == "Av. Corrientes 1234"
|
||||
```
|
||||
|
||||
### In Mock Veins
|
||||
|
||||
```python
|
||||
from ward.tools.datagen.mercadopago import MercadoPagoDataGenerator
|
||||
|
||||
@router.post("/v1/preferences")
|
||||
async def create_preference(request: dict):
|
||||
# Generate mock response
|
||||
return MercadoPagoDataGenerator.preference(
|
||||
description=request["items"][0]["title"],
|
||||
total=request["items"][0]["unit_price"],
|
||||
)
|
||||
```
|
||||
|
||||
### In Seeders
|
||||
|
||||
```python
|
||||
from ward.tools.datagen.amar import AmarDataGenerator
|
||||
|
||||
# Create 10 test pet owners
|
||||
for i in range(10):
|
||||
owner = AmarDataGenerator.petowner(is_guest=False)
|
||||
# Save to database...
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Pluggable**: Can be used anywhere, not tied to specific frameworks
|
||||
2. **Realistic**: Generated data matches real-world patterns
|
||||
3. **Flexible**: Override any field via `**overrides` parameter
|
||||
4. **Domain-focused**: Each generator focuses on a specific domain
|
||||
5. **Stateless**: Pure functions, no global state
|
||||
|
||||
## Generators
|
||||
|
||||
### AmarDataGenerator (amar.py)
|
||||
|
||||
Generates data for Amar platform:
|
||||
|
||||
- `petowner()` - Pet owners (guest and registered)
|
||||
- `pet()` - Pets with species, age, etc.
|
||||
- `cart()` - Shopping carts
|
||||
- `service_request()` - Service requests
|
||||
- `filter_services()` - Service filtering by species/neighborhood
|
||||
- `filter_categories()` - Category filtering
|
||||
- `calculate_cart_summary()` - Cart totals with discounts
|
||||
|
||||
### MercadoPagoDataGenerator (mercadopago.py)
|
||||
|
||||
Generates MercadoPago API responses:
|
||||
|
||||
- `preference()` - Checkout Pro preference
|
||||
- `payment()` - Payment (Checkout API/Bricks)
|
||||
- `merchant_order()` - Merchant order
|
||||
- `oauth_token()` - OAuth token exchange
|
||||
- `webhook_notification()` - Webhook payloads
|
||||
|
||||
## Examples
|
||||
|
||||
### Generate a complete turnero flow
|
||||
|
||||
```python
|
||||
from ward.tools.datagen.amar import AmarDataGenerator
|
||||
|
||||
# Step 1: Guest pet owner
|
||||
owner = AmarDataGenerator.petowner(
|
||||
address="Av. Santa Fe 1234, Palermo",
|
||||
is_guest=True
|
||||
)
|
||||
|
||||
# Step 2: Pet
|
||||
pet = AmarDataGenerator.pet(
|
||||
owner_id=owner["id"],
|
||||
name="Luna",
|
||||
species="DOG",
|
||||
age_value=3,
|
||||
age_unit="years"
|
||||
)
|
||||
|
||||
# Step 3: Cart
|
||||
cart = AmarDataGenerator.cart(owner_id=owner["id"])
|
||||
|
||||
# Step 4: Add services to cart
|
||||
services = AmarDataGenerator.filter_services(
|
||||
species="DOG",
|
||||
neighborhood_id=owner["neighborhood"]["id"]
|
||||
)
|
||||
|
||||
cart_with_items = AmarDataGenerator.calculate_cart_summary(
|
||||
cart,
|
||||
items=[
|
||||
{"service_id": services[0]["id"], "price": services[0]["price"], "quantity": 1, "pet_id": pet["id"]},
|
||||
]
|
||||
)
|
||||
|
||||
# Step 5: Service request
|
||||
request = AmarDataGenerator.service_request(cart_id=cart["id"])
|
||||
```
|
||||
|
||||
### Generate a payment flow
|
||||
|
||||
```python
|
||||
from ward.tools.datagen.mercadopago import MercadoPagoDataGenerator
|
||||
|
||||
# Create preference
|
||||
pref = MercadoPagoDataGenerator.preference(
|
||||
description="Visita a domicilio",
|
||||
total=95000,
|
||||
external_reference="SR-12345"
|
||||
)
|
||||
|
||||
# Simulate payment
|
||||
payment = MercadoPagoDataGenerator.payment(
|
||||
transaction_amount=95000,
|
||||
description="Visita a domicilio",
|
||||
status="approved",
|
||||
application_fee=45000 # Platform fee (split payment)
|
||||
)
|
||||
|
||||
# Webhook notification
|
||||
webhook = MercadoPagoDataGenerator.webhook_notification(
|
||||
topic="payment",
|
||||
resource_id=str(payment["id"])
|
||||
)
|
||||
```
|
||||
|
||||
## Future Generators
|
||||
|
||||
- `google.py` - Google API responses (Calendar, Sheets)
|
||||
- `whatsapp.py` - WhatsApp API responses
|
||||
- `slack.py` - Slack API responses
|
||||
1
station/tools/datagen/__init__.py
Normal file
1
station/tools/datagen/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Datagen - Test data generator for Amar domain models."""
|
||||
255
station/tools/datagen/amar.py
Normal file
255
station/tools/datagen/amar.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""Data generator for Amar domain models - can be plugged into any nest."""
|
||||
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
|
||||
class AmarDataGenerator:
|
||||
"""Generates realistic test data for Amar domain models."""
|
||||
|
||||
# Sample data pools
|
||||
FIRST_NAMES = ["Lucas", "María", "Juan", "Carolina", "Diego", "Valentina", "Martín", "Sofía", "Mateo", "Emma"]
|
||||
LAST_NAMES = ["González", "Rodríguez", "Pérez", "García", "Martínez", "López", "Fernández", "Sánchez"]
|
||||
|
||||
PET_NAMES = ["Luna", "Max", "Bella", "Rocky", "Coco", "Toby", "Mia", "Charlie", "Lola", "Simba"]
|
||||
PET_SPECIES = [
|
||||
{"id": 1, "name": "Perro", "code": "DOG"},
|
||||
{"id": 2, "name": "Gato", "code": "CAT"}
|
||||
]
|
||||
|
||||
NEIGHBORHOODS = [
|
||||
{"id": 1, "name": "Palermo", "has_coverage": True, "zone": "CABA"},
|
||||
{"id": 2, "name": "Recoleta", "has_coverage": True, "zone": "CABA"},
|
||||
{"id": 3, "name": "Belgrano", "has_coverage": True, "zone": "CABA"},
|
||||
{"id": 4, "name": "Caballito", "has_coverage": True, "zone": "CABA"},
|
||||
{"id": 5, "name": "Mataderos", "has_coverage": False, "zone": "CABA"},
|
||||
{"id": 6, "name": "Villa Urquiza", "has_coverage": True, "zone": "CABA"},
|
||||
]
|
||||
|
||||
SERVICE_CATEGORIES = [
|
||||
{"id": 1, "name": "Consultas", "description": "Consultas veterinarias"},
|
||||
{"id": 2, "name": "Vacunación", "description": "Vacunas y antiparasitarios"},
|
||||
{"id": 3, "name": "Estudios", "description": "Análisis y estudios clínicos"},
|
||||
{"id": 4, "name": "Videollamada", "description": "Consultas por videollamada"},
|
||||
]
|
||||
|
||||
SERVICES = [
|
||||
{"id": 1, "category_id": 1, "name": "Consulta general clínica programada", "price": 95000, "species": ["DOG", "CAT"]},
|
||||
{"id": 2, "category_id": 2, "name": "Vacuna Antirrábica", "price": 7000, "species": ["DOG", "CAT"]},
|
||||
{"id": 3, "category_id": 2, "name": "Sextuple", "price": 12000, "species": ["DOG"]},
|
||||
{"id": 4, "category_id": 2, "name": "Triple Felina", "price": 11000, "species": ["CAT"]},
|
||||
{"id": 5, "category_id": 3, "name": "Análisis de sangre", "price": 25000, "species": ["DOG", "CAT"]},
|
||||
{"id": 6, "category_id": 3, "name": "Ecografía", "price": 35000, "species": ["DOG", "CAT"]},
|
||||
{"id": 7, "category_id": 4, "name": "Consulta por videollamada", "price": 15000, "species": ["DOG", "CAT"]},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def petowner(cls, address: str = None, is_guest: bool = True, **overrides) -> Dict[str, Any]:
|
||||
"""Generate a petowner.
|
||||
|
||||
Args:
|
||||
address: Owner address
|
||||
is_guest: Whether this is a guest user
|
||||
**overrides: Override any fields
|
||||
"""
|
||||
owner_id = overrides.get("id", random.randint(1000, 9999))
|
||||
first_name = overrides.get("first_name", random.choice(cls.FIRST_NAMES) if not is_guest else "")
|
||||
last_name = overrides.get("last_name", random.choice(cls.LAST_NAMES) if not is_guest else "")
|
||||
address = address or f"{random.choice(['Av.', 'Calle'])} {random.choice(['Corrientes', 'Santa Fe', 'Córdoba', 'Rivadavia'])} {random.randint(1000, 5000)}"
|
||||
|
||||
# Determine neighborhood from address or random
|
||||
neighborhood = overrides.get("neighborhood", random.choice([n for n in cls.NEIGHBORHOODS if n["has_coverage"]]))
|
||||
|
||||
data = {
|
||||
"id": owner_id,
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
"email": f"guest_{owner_id}@amarmascotas.ar" if is_guest else f"{first_name.lower()}.{last_name.lower()}@example.com",
|
||||
"phone": f"+54911{random.randint(10000000, 99999999)}",
|
||||
"address": address,
|
||||
"neighborhood": neighborhood,
|
||||
"is_guest": is_guest,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# Apply overrides
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def pet(cls, owner_id: int, name: str = None, species: str = "DOG", age_value: int = None, age_unit: str = "years", **overrides) -> Dict[str, Any]:
|
||||
"""Generate a pet.
|
||||
|
||||
Args:
|
||||
owner_id: Owner ID
|
||||
name: Pet name
|
||||
species: Pet species code (DOG, CAT)
|
||||
age_value: Age value
|
||||
age_unit: Age unit (years, months)
|
||||
**overrides: Override any fields
|
||||
"""
|
||||
species_data = next((s for s in cls.PET_SPECIES if s["code"] == species.upper()), cls.PET_SPECIES[0])
|
||||
age = age_value or random.randint(1, 10)
|
||||
name = name or random.choice(cls.PET_NAMES)
|
||||
|
||||
data = {
|
||||
"id": random.randint(1000, 9999),
|
||||
"owner_id": owner_id,
|
||||
"name": name,
|
||||
"species": species_data,
|
||||
"age": age,
|
||||
"age_unit": age_unit,
|
||||
"age_in_months": age if age_unit == "months" else age * 12,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def cart(cls, owner_id: int, **overrides) -> Dict[str, Any]:
|
||||
"""Generate an empty cart.
|
||||
|
||||
Args:
|
||||
owner_id: Owner ID
|
||||
**overrides: Override any fields
|
||||
"""
|
||||
cart_id = overrides.get("id", random.randint(10000, 99999))
|
||||
|
||||
data = {
|
||||
"id": cart_id,
|
||||
"owner_id": owner_id,
|
||||
"items": [],
|
||||
"resume": {
|
||||
"subtotal": 0.0,
|
||||
"discounts": 0.0,
|
||||
"total": 0.0,
|
||||
},
|
||||
"resume_items": [],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def filter_services(cls, species: str = None, neighborhood_id: int = None) -> List[Dict[str, Any]]:
|
||||
"""Filter services by species and neighborhood coverage.
|
||||
|
||||
Args:
|
||||
species: Species code to filter by (DOG, CAT)
|
||||
neighborhood_id: Neighborhood ID for coverage check
|
||||
"""
|
||||
services = cls.SERVICES.copy()
|
||||
|
||||
if species:
|
||||
species_code = species.upper()
|
||||
services = [s for s in services if species_code in s["species"]]
|
||||
|
||||
# Neighborhood coverage - only videollamada if no coverage
|
||||
if neighborhood_id:
|
||||
neighborhood = next((n for n in cls.NEIGHBORHOODS if n["id"] == neighborhood_id), None)
|
||||
if not neighborhood or not neighborhood.get("has_coverage"):
|
||||
services = [s for s in services if s["category_id"] == 4] # Only videollamada
|
||||
|
||||
return [
|
||||
{
|
||||
"id": s["id"],
|
||||
"name": s["name"],
|
||||
"category_id": s["category_id"],
|
||||
"price": s["price"],
|
||||
"currency": "ARS",
|
||||
"available": True,
|
||||
}
|
||||
for s in services
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def filter_categories(cls, species: str = None, neighborhood_id: int = None) -> List[Dict[str, Any]]:
|
||||
"""Filter categories that have available services.
|
||||
|
||||
Args:
|
||||
species: Species code to filter by
|
||||
neighborhood_id: Neighborhood ID for coverage check
|
||||
"""
|
||||
available_services = cls.filter_services(species, neighborhood_id)
|
||||
service_category_ids = {s["category_id"] for s in available_services}
|
||||
|
||||
return [
|
||||
{
|
||||
"id": cat["id"],
|
||||
"name": cat["name"],
|
||||
"description": cat["description"],
|
||||
"service_count": len([s for s in available_services if s["category_id"] == cat["id"]]),
|
||||
}
|
||||
for cat in cls.SERVICE_CATEGORIES
|
||||
if cat["id"] in service_category_ids
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def calculate_cart_summary(cls, cart: Dict[str, Any], items: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Calculate cart totals with discounts and splits.
|
||||
|
||||
Args:
|
||||
cart: Cart data
|
||||
items: List of cart items with price and quantity
|
||||
"""
|
||||
subtotal = sum(item.get("price", 0) * item.get("quantity", 1) for item in items)
|
||||
|
||||
# Multi-pet discount
|
||||
pet_count = len({item.get("pet_id") for item in items if item.get("pet_id")})
|
||||
discount_rate = 0.0
|
||||
if pet_count >= 2:
|
||||
discount_rate = 0.10 # 10% for 2+ pets
|
||||
|
||||
discount_amount = subtotal * discount_rate
|
||||
total = subtotal - discount_amount
|
||||
|
||||
resume_items = [
|
||||
{"concept": "SUBTOTAL", "amount": subtotal},
|
||||
]
|
||||
|
||||
if discount_amount > 0:
|
||||
resume_items.append({"concept": "DESCUENTO MULTIMASCOTA", "amount": -discount_amount})
|
||||
|
||||
# Calculate vet honorarios (52% of total in real system)
|
||||
honorarios = total * 0.52
|
||||
resume_items.append({"concept": "HONORARIOS", "amount": honorarios})
|
||||
resume_items.append({"concept": "TOTAL", "amount": total})
|
||||
|
||||
return {
|
||||
**cart,
|
||||
"items": items,
|
||||
"resume": {
|
||||
"subtotal": round(subtotal, 2),
|
||||
"discounts": round(discount_amount, 2),
|
||||
"total": round(total, 2),
|
||||
},
|
||||
"resume_items": resume_items,
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def service_request(cls, cart_id: int, requested_date: str = None, **overrides) -> Dict[str, Any]:
|
||||
"""Generate a service request.
|
||||
|
||||
Args:
|
||||
cart_id: Cart ID
|
||||
requested_date: ISO format date string
|
||||
**overrides: Override any fields
|
||||
"""
|
||||
request_id = overrides.get("id", random.randint(100000, 999999))
|
||||
requested_date = requested_date or (datetime.now() + timedelta(days=random.randint(1, 7))).isoformat()
|
||||
|
||||
data = {
|
||||
"id": request_id,
|
||||
"cart_id": cart_id,
|
||||
"requested_date": requested_date,
|
||||
"state": "PENDING",
|
||||
"veterinarian": None,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
data.update(overrides)
|
||||
return data
|
||||
Reference in New Issue
Block a user