soleprint init commit

This commit is contained in:
buenosairesam
2025-12-24 05:38:37 -03:00
commit 329c401ff5
96 changed files with 11564 additions and 0 deletions

View 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

View File

@@ -0,0 +1 @@
"""Datagen - Test data generator for Amar domain models."""

View 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