spr migrated books, and tester

This commit is contained in:
buenosairesam
2025-12-31 09:07:27 -03:00
parent 21b8eab3cb
commit cccc6b5a93
136 changed files with 15763 additions and 472 deletions

56
atlas/.dockerignore Normal file
View File

@@ -0,0 +1,56 @@
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.so
# Virtual environments
venv/
env/
.venv/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
.DS_Store
# Git
.git/
.gitignore
.gitattributes
# Environment files
.env
.env.*
!.env.example
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Build artifacts
dist/
build/
*.egg-info/
# Data and logs
data/
*.log
*.db
*.sqlite
*.sqlite3
# Documentation
*.md
!README.md
# Node (for frontend)
node_modules/
npm-debug.log
yarn-error.log
.next/

3
atlas/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
def
__pycache__
drive

116
atlas/CLAUDE.md Normal file
View File

@@ -0,0 +1,116 @@
# ALBUM: Documentation System
## Purpose
Documentation, templates, and data. Composed into books.
### Book Types
**Single-larder book** (template: null)
- Book and larder are interchangeable
- Just static content served directly
- Listed in books.json, NOT in larders.json
**Templated book** (template: {...})
- Template defines the structure
- Larder must contain only elements matching that template
- Template validates/constrains the larder content
- Shows a styled landing page linking to template and larder
### Larders vs Books
- **Larders** = buckets that connect to other systems (e.g., drive, external data sources)
- **Books** = standalone content, may be single-larder or template+larder
- Single-larder books are listed as books, not larders
### Templated Book Structure
A templated book must contain a `template/` folder with the template definition inside:
```
book/{book-slug}/
├── template/ # REQUIRED for templated books
│ └── {template-files}
├── {larder-name}/ # The actual data (marked with .larder file)
│ └── .larder
├── index.html # Larder browser (used at /book/{slug}/larder/)
└── detail.html # Detail view template (if needed)
```
Example:
```
book/feature-form-samples/
├── template/ # Template definition
│ └── plantilla-flujo.md
├── feature-form/ # Larder data (constrained by template)
│ ├── .larder
│ ├── pet-owner/
│ ├── veterinarian/
│ └── backoffice/
├── index.html # Larder browser
└── detail.html # Detail renderer
```
Routes for templated books:
- `/book/{slug}/` → Landing page (template + larder links)
- `/book/{slug}/template/` → Template definition
- `/book/{slug}/larder/` → Larder browser
## Components
### Template (Patterns)
**Gherkin/BDD**
- Status: Pending
- Goal: .feature files, simple templates for non-tech
**Index Templates**
- Status: Pending
- Goal: HTML generators for indexes
### Vault (Data)
**drive/**
- Status: Downloaded
- Contents: Company drive (identity, marketing, ops, supply, finance, clients, pitches)
### Book (Composed Docs)
**drive-index**
- Status: Priority
- Goal: Two indexes (internal full, public privacy-conscious)
**flow-docs**
- Status: Pending
- Goal: User flow documentation (pet owner, vet, ops)
## Upward Report
```
ALBUM: Drive index priority. Template + vault → book composition defined.
```
## Priority
1. Drive index book (HTML from vault/drive)
2. One gherkin template example
3. Flow documentation structure
## Vault Contents (vault/drive)
- 01.Identidad Amar Mascotas
- 02. Marketing contenidos
- 03. Marketing Growth
- 05. ATC - Operaciones
- 06. Supply (vetes-labo-clinicas)
- 07. Finanzas y contabilidad
- Clientes - ventas - devoluciones
- Pitch Decks - Presentaciones
## Deployment
- **URL**: https://album.mcrn.ar
- **Port**: 12002
- **Service**: `systemctl status album`
FastAPI app serving documentation. Currently serves index.html at root, expandable for book browsing.
## Upstream (for main pawprint thread)
This is now a separate repo. See pawprint/UPSTREAM.md for merge notes.

118
atlas/book-template.html Normal file
View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ book.title }} · Album</title>
<style>
* { box-sizing: border-box; }
html { background: #0a0a0a; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 960px;
margin: 0 auto;
padding: 2rem 1rem;
line-height: 1.6;
color: #e5e5e5;
background: #15803d;
}
header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.logo { width: 64px; height: 64px; color: white; }
h1 { font-size: 2rem; margin: 0; color: white; }
.tagline {
color: rgba(255,255,255,0.85);
margin-bottom: 2rem;
border-bottom: 1px solid rgba(255,255,255,0.3);
padding-bottom: 2rem;
}
section {
background: white;
padding: 1.5rem;
margin: 1.5rem 0;
border-radius: 12px;
color: #1a1a1a;
}
section h2 {
margin: 0 0 1rem 0;
font-size: 1.2rem;
color: #15803d;
}
.composition {
background: #f0fdf4;
border: 2px solid #15803d;
padding: 1rem;
border-radius: 12px;
}
.composition h3 { margin: 0 0 0.75rem 0; font-size: 1.1rem; color: #15803d; }
.components {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.component {
background: white;
border: 1px solid #bbf7d0;
padding: 1.5rem;
border-radius: 8px;
text-decoration: none;
color: #1a1a1a;
transition: all 0.15s;
display: block;
}
.component:hover {
border-color: #15803d;
box-shadow: 0 4px 12px rgba(21, 128, 61, 0.15);
}
.component h4 { margin: 0 0 0.5rem 0; font-size: 1rem; color: #15803d; }
.component p { margin: 0; font-size: 0.9rem; color: #666; }
.component .arrow { float: right; color: #15803d; font-size: 1.2rem; }
footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(255,255,255,0.3);
font-size: 0.85rem;
color: rgba(255,255,255,0.7);
}
footer a { color: white; text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<header>
<svg class="logo" viewBox="0 0 48 48" fill="currentColor">
<path d="M4 8 C4 8 12 6 24 10 L24 42 C12 38 4 40 4 40 Z" opacity="0.3"/>
<path d="M44 8 C44 8 36 6 24 10 L24 42 C36 38 44 40 44 40 Z" opacity="0.5"/>
<path d="M4 8 C4 8 12 6 24 10 M44 8 C44 8 36 6 24 10" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M4 40 C4 40 12 38 24 42 M44 40 C44 40 36 38 24 42" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="24" y1="10" x2="24" y2="42" stroke="currentColor" stroke-width="2"/>
</svg>
<h1>{{ book.title }}</h1>
</header>
<p class="tagline">Templated book</p>
<section>
<div class="composition">
<h3>{{ book.title }}</h3>
<div class="components">
<a href="/book/{{ book.slug }}/template/" class="component">
<span class="arrow">&rarr;</span>
<h4>{{ book.template.title }}</h4>
</a>
<a href="/book/{{ book.slug }}/larder/" class="component">
<span class="arrow">&rarr;</span>
<h4>{{ book.larder.title }}</h4>
</a>
</div>
</div>
</section>
<footer>
<a href="/">&larr; Album</a>
</footer>
</body>
</html>

View File

@@ -0,0 +1,184 @@
digraph BackendArchitecture {
// Graph settings
rankdir=TB
compound=true
splines=ortho
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11]
edge [fontname="Helvetica", fontsize=9]
// Color scheme
// Django Core: #092E20 (dark green)
// Mascotas: #4A90D9 (blue)
// Productos: #50C878 (green)
// Solicitudes: #FF6B6B (coral)
// Common: #9B59B6 (purple)
// External: #F39C12 (orange)
// Payments: #E74C3C (red)
label="AMAR Mascotas - Backend Architecture (Django)\n\n"
labelloc="t"
fontsize=16
fontname="Helvetica-Bold"
// Django Core Cluster
subgraph cluster_django_core {
label="Django Core"
style="rounded,filled"
fillcolor="#E8F5E9"
color="#2E7D32"
auth_user [label="auth.User\n(Django Auth)", fillcolor="#C8E6C9"]
django_admin [label="Django Admin\nInterface", fillcolor="#C8E6C9"]
drf [label="Django REST\nFramework", fillcolor="#C8E6C9"]
jwt_auth [label="JWT Authentication\n(SimpleJWT)", fillcolor="#C8E6C9"]
}
// Mascotas App Cluster
subgraph cluster_mascotas {
label="mascotas (Pets & Veterinarians)"
style="rounded,filled"
fillcolor="#E3F2FD"
color="#1565C0"
petowner [label="PetOwner\n(Cliente/Tutor)", fillcolor="#BBDEFB"]
pet [label="Pet\n(Mascota)", fillcolor="#BBDEFB"]
veterinarian [label="Veterinarian\n(Profesional)", fillcolor="#BBDEFB"]
vetvisit [label="VetVisit\n(Consulta)", fillcolor="#BBDEFB"]
vetvisitreport [label="VetVisitReport\n(Informe Clinico)", fillcolor="#BBDEFB"]
availability [label="Availability /\nUnavailability", fillcolor="#BBDEFB"]
pet_health [label="PetVaccine /\nPetStudy", fillcolor="#BBDEFB"]
}
// Productos App Cluster
subgraph cluster_productos {
label="productos (Services & Pricing)"
style="rounded,filled"
fillcolor="#E8F5E9"
color="#2E7D32"
service [label="Service\n(Servicio)", fillcolor="#C8E6C9"]
category [label="Category / Group\n(Categorias)", fillcolor="#C8E6C9"]
prices [label="Prices\n(Precios)", fillcolor="#C8E6C9"]
discounts [label="Discounts\n(Descuentos)", fillcolor="#C8E6C9"]
cart [label="Cart / CartItem\n(Carrito)", fillcolor="#C8E6C9"]
combo [label="ServiceCombo\n(Paquetes)", fillcolor="#C8E6C9"]
}
// Solicitudes App Cluster
subgraph cluster_solicitudes {
label="solicitudes (Service Requests)"
style="rounded,filled"
fillcolor="#FFEBEE"
color="#C62828"
servicerequest [label="ServiceRequest\n(Solicitud)", fillcolor="#FFCDD2"]
statehistory [label="StateHistory\n(Historial)", fillcolor="#FFCDD2"]
vetasked [label="VeterinarianAsked\n(Vet Consultado)", fillcolor="#FFCDD2"]
reminders [label="Reminders\n(Recordatorios)", fillcolor="#FFCDD2"]
}
// Common App Cluster
subgraph cluster_common {
label="common (Shared Models)"
style="rounded,filled"
fillcolor="#F3E5F5"
color="#7B1FA2"
campaign [label="Campaign\n(Marketing)", fillcolor="#E1BEE7"]
tag [label="Tag\n(Etiquetas)", fillcolor="#E1BEE7"]
specialty [label="Specialty\n(Especialidades)", fillcolor="#E1BEE7"]
medication [label="Medication\n(Medicamentos)", fillcolor="#E1BEE7"]
breed [label="PetBreed / Vaccine\n/ Study", fillcolor="#E1BEE7"]
neighborhood [label="Neighborhood /\nProvince / Locality", fillcolor="#E1BEE7"]
turnfee [label="IndividualTurnFee\nGroup", fillcolor="#E1BEE7"]
}
// Payments App Cluster
subgraph cluster_payments {
label="payments (Payment Processing)"
style="rounded,filled"
fillcolor="#FCE4EC"
color="#AD1457"
mercadopago [label="MercadoPago\nAccount", fillcolor="#F8BBD9"]
mpnotification [label="MP Notification\n(Webhooks)", fillcolor="#F8BBD9"]
}
// External Integrations Cluster
subgraph cluster_external {
label="External Integrations"
style="rounded,filled"
fillcolor="#FFF3E0"
color="#E65100"
google_cal [label="Google Calendar\n(Agenda)", fillcolor="#FFE0B2"]
google_sheets [label="Google Sheets\n(Exports)", fillcolor="#FFE0B2"]
mercately [label="Mercately\n(WhatsApp)", fillcolor="#FFE0B2"]
afip [label="AFIP\n(Facturacion)", fillcolor="#FFE0B2"]
celery [label="Celery\n(Async Tasks)", fillcolor="#FFE0B2"]
}
// AFIP Integration
subgraph cluster_afip {
label="django_afip (Invoicing)"
style="rounded,filled"
fillcolor="#FFFDE7"
color="#F9A825"
receipt [label="Receipt\n(Comprobante)", fillcolor="#FFF9C4"]
taxpayer [label="TaxPayer\n(Contribuyente)", fillcolor="#FFF9C4"]
pos [label="PointOfSales\n(Punto de Venta)", fillcolor="#FFF9C4"]
}
// Relationships - Core
auth_user -> petowner [label="1:1 optional", color="#666"]
auth_user -> veterinarian [label="1:1", color="#666"]
drf -> jwt_auth [style=dashed, color="#999"]
// Relationships - Mascotas
petowner -> pet [label="1:N", color="#1565C0"]
petowner -> servicerequest [label="1:N", color="#1565C0"]
pet -> vetvisit [label="N:M", color="#1565C0"]
pet -> pet_health [label="1:N", color="#1565C0"]
veterinarian -> vetvisit [label="1:N", color="#1565C0"]
veterinarian -> availability [label="1:N", color="#1565C0"]
vetvisit -> vetvisitreport [label="1:N", color="#1565C0"]
vetvisit -> servicerequest [label="1:1", style=dashed, color="#1565C0"]
// Relationships - Productos
service -> category [label="N:1", color="#2E7D32"]
service -> prices [label="1:N", color="#2E7D32"]
service -> discounts [label="1:N", color="#2E7D32"]
cart -> petowner [label="N:1", color="#2E7D32"]
cart -> veterinarian [label="N:1 optional", color="#2E7D32", style=dashed]
service -> cart [label="via CartItem", color="#2E7D32"]
combo -> service [label="contains", color="#2E7D32"]
// Relationships - Solicitudes
servicerequest -> cart [label="1:1", color="#C62828"]
servicerequest -> statehistory [label="1:N", color="#C62828"]
servicerequest -> vetasked [label="1:N", color="#C62828"]
vetasked -> reminders [label="1:N", color="#C62828"]
veterinarian -> vetasked [label="N:1", color="#C62828"]
// Relationships - Common
petowner -> neighborhood [label="N:1", color="#7B1FA2"]
veterinarian -> specialty [label="N:M", color="#7B1FA2"]
veterinarian -> neighborhood [label="N:M coverage", color="#7B1FA2"]
servicerequest -> campaign [label="N:1 optional", color="#7B1FA2", style=dashed]
servicerequest -> tag [label="N:M", color="#7B1FA2"]
vetvisitreport -> medication [label="references", color="#7B1FA2", style=dashed]
veterinarian -> turnfee [label="N:M", color="#7B1FA2"]
// Relationships - Payments & External
servicerequest -> mercadopago [label="payment", color="#AD1457"]
mpnotification -> servicerequest [label="confirms", color="#AD1457"]
vetvisit -> google_cal [label="sync", color="#E65100", style=dashed]
servicerequest -> mercately [label="notify", color="#E65100", style=dashed]
reminders -> celery [label="scheduled", color="#E65100", style=dashed]
// AFIP relationships
vetvisit -> receipt [label="1:1 optional", color="#F9A825", style=dashed]
receipt -> taxpayer [label="N:1", color="#F9A825"]
receipt -> pos [label="N:1", color="#F9A825"]
}

View File

@@ -0,0 +1,585 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: BackendArchitecture Pages: 1 -->
<svg width="1559pt" height="935pt"
viewBox="0.00 0.00 1559.00 935.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 931.4)">
<title>BackendArchitecture</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-931.4 1555,-931.4 1555,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="775.5" y="-908.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Backend Architecture (Django)</text>
<g id="clust1" class="cluster">
<title>cluster_django_core</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M169,-496.4C169,-496.4 449,-496.4 449,-496.4 455,-496.4 461,-502.4 461,-508.4 461,-508.4 461,-726 461,-726 461,-732 455,-738 449,-738 449,-738 169,-738 169,-738 163,-738 157,-732 157,-726 157,-726 157,-508.4 157,-508.4 157,-502.4 163,-496.4 169,-496.4"/>
<text xml:space="preserve" text-anchor="middle" x="309" y="-718.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Django Core</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_mascotas</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M481,-143.2C481,-143.2 733,-143.2 733,-143.2 739,-143.2 745,-149.2 745,-155.2 745,-155.2 745,-590.8 745,-590.8 745,-596.8 739,-602.8 733,-602.8 733,-602.8 481,-602.8 481,-602.8 475,-602.8 469,-596.8 469,-590.8 469,-590.8 469,-155.2 469,-155.2 469,-149.2 475,-143.2 481,-143.2"/>
<text xml:space="preserve" text-anchor="middle" x="607" y="-583.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">mascotas (Pets &amp; Veterinarians)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_productos</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M765,-496.4C765,-496.4 1141,-496.4 1141,-496.4 1147,-496.4 1153,-502.4 1153,-508.4 1153,-508.4 1153,-861.2 1153,-861.2 1153,-867.2 1147,-873.2 1141,-873.2 1141,-873.2 765,-873.2 765,-873.2 759,-873.2 753,-867.2 753,-861.2 753,-861.2 753,-508.4 753,-508.4 753,-502.4 759,-496.4 765,-496.4"/>
<text xml:space="preserve" text-anchor="middle" x="953" y="-854" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">productos (Services &amp; Pricing)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_solicitudes</title>
<path fill="#ffebee" stroke="#c62828" d="M1084,-143.2C1084,-143.2 1290,-143.2 1290,-143.2 1296,-143.2 1302,-149.2 1302,-155.2 1302,-155.2 1302,-455.6 1302,-455.6 1302,-461.6 1296,-467.6 1290,-467.6 1290,-467.6 1084,-467.6 1084,-467.6 1078,-467.6 1072,-461.6 1072,-455.6 1072,-455.6 1072,-155.2 1072,-155.2 1072,-149.2 1078,-143.2 1084,-143.2"/>
<text xml:space="preserve" text-anchor="middle" x="1187" y="-448.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">solicitudes (Service Requests)</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_common</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M20,-8C20,-8 774,-8 774,-8 780,-8 786,-14 786,-20 786,-20 786,-102.4 786,-102.4 786,-108.4 780,-114.4 774,-114.4 774,-114.4 20,-114.4 20,-114.4 14,-114.4 8,-108.4 8,-102.4 8,-102.4 8,-20 8,-20 8,-14 14,-8 20,-8"/>
<text xml:space="preserve" text-anchor="middle" x="397" y="-95.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">common (Shared Models)</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_payments</title>
<path fill="#fce4ec" stroke="#ad1457" d="M1173,-496.4C1173,-496.4 1395,-496.4 1395,-496.4 1401,-496.4 1407,-502.4 1407,-508.4 1407,-508.4 1407,-590.8 1407,-590.8 1407,-596.8 1401,-602.8 1395,-602.8 1395,-602.8 1173,-602.8 1173,-602.8 1167,-602.8 1161,-596.8 1161,-590.8 1161,-590.8 1161,-508.4 1161,-508.4 1161,-502.4 1167,-496.4 1173,-496.4"/>
<text xml:space="preserve" text-anchor="middle" x="1284" y="-583.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">payments (Payment Processing)</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_external</title>
<path fill="#fff3e0" stroke="#e65100" d="M1039,-8C1039,-8 1531,-8 1531,-8 1537,-8 1543,-14 1543,-20 1543,-20 1543,-102.4 1543,-102.4 1543,-108.4 1537,-114.4 1531,-114.4 1531,-114.4 1039,-114.4 1039,-114.4 1033,-114.4 1027,-108.4 1027,-102.4 1027,-102.4 1027,-20 1027,-20 1027,-14 1033,-8 1039,-8"/>
<text xml:space="preserve" text-anchor="middle" x="1285" y="-95.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External Integrations</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_afip</title>
<path fill="#fffde7" stroke="#f9a825" d="M806,-8C806,-8 1007,-8 1007,-8 1013,-8 1019,-14 1019,-20 1019,-20 1019,-237.6 1019,-237.6 1019,-243.6 1013,-249.6 1007,-249.6 1007,-249.6 806,-249.6 806,-249.6 800,-249.6 794,-243.6 794,-237.6 794,-237.6 794,-20 794,-20 794,-14 800,-8 806,-8"/>
<text xml:space="preserve" text-anchor="middle" x="906.5" y="-230.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">django_afip (Invoicing)</text>
</g>
<!-- auth_user -->
<g id="node1" class="node">
<title>auth_user</title>
<path fill="#c8e6c9" stroke="black" d="M440.93,-675.6C440.93,-675.6 381.07,-675.6 381.07,-675.6 375.07,-675.6 369.07,-669.6 369.07,-663.6 369.07,-663.6 369.07,-651.6 369.07,-651.6 369.07,-645.6 375.07,-639.6 381.07,-639.6 381.07,-639.6 440.93,-639.6 440.93,-639.6 446.93,-639.6 452.93,-645.6 452.93,-651.6 452.93,-651.6 452.93,-663.6 452.93,-663.6 452.93,-669.6 446.93,-675.6 440.93,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="411" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">auth.User</text>
<text xml:space="preserve" text-anchor="middle" x="411" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">(Django Auth)</text>
</g>
<!-- petowner -->
<g id="node5" class="node">
<title>petowner</title>
<path fill="#bbdefb" stroke="black" d="M725.15,-540.4C725.15,-540.4 662.85,-540.4 662.85,-540.4 656.85,-540.4 650.85,-534.4 650.85,-528.4 650.85,-528.4 650.85,-516.4 650.85,-516.4 650.85,-510.4 656.85,-504.4 662.85,-504.4 662.85,-504.4 725.15,-504.4 725.15,-504.4 731.15,-504.4 737.15,-510.4 737.15,-516.4 737.15,-516.4 737.15,-528.4 737.15,-528.4 737.15,-534.4 731.15,-540.4 725.15,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="694" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">PetOwner</text>
<text xml:space="preserve" text-anchor="middle" x="694" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Cliente/Tutor)</text>
</g>
<!-- auth_user&#45;&gt;petowner -->
<g id="edge1" class="edge">
<title>auth_user&#45;&gt;petowner</title>
<path fill="none" stroke="#666666" d="M424.94,-639.31C424.94,-602.3 424.94,-522 424.94,-522 424.94,-522 638.96,-522 638.96,-522"/>
<polygon fill="#666666" stroke="#666666" points="638.96,-525.5 648.96,-522 638.96,-518.5 638.96,-525.5"/>
<text xml:space="preserve" text-anchor="middle" x="645.27" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:1 optional</text>
</g>
<!-- veterinarian -->
<g id="node7" class="node">
<title>veterinarian</title>
<path fill="#bbdefb" stroke="black" d="M544.18,-405.2C544.18,-405.2 489.82,-405.2 489.82,-405.2 483.82,-405.2 477.82,-399.2 477.82,-393.2 477.82,-393.2 477.82,-381.2 477.82,-381.2 477.82,-375.2 483.82,-369.2 489.82,-369.2 489.82,-369.2 544.18,-369.2 544.18,-369.2 550.18,-369.2 556.18,-375.2 556.18,-381.2 556.18,-381.2 556.18,-393.2 556.18,-393.2 556.18,-399.2 550.18,-405.2 544.18,-405.2"/>
<text xml:space="preserve" text-anchor="middle" x="517" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">Veterinarian</text>
<text xml:space="preserve" text-anchor="middle" x="517" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Profesional)</text>
</g>
<!-- auth_user&#45;&gt;veterinarian -->
<g id="edge2" class="edge">
<title>auth_user&#45;&gt;veterinarian</title>
<path fill="none" stroke="#666666" d="M453.39,-664C477.9,-664 503.94,-664 503.94,-664 503.94,-664 503.94,-416.88 503.94,-416.88"/>
<polygon fill="#666666" stroke="#666666" points="507.44,-416.88 503.94,-406.88 500.44,-416.88 507.44,-416.88"/>
<text xml:space="preserve" text-anchor="middle" x="470.26" y="-519.7" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
</g>
<!-- django_admin -->
<g id="node2" class="node">
<title>django_admin</title>
<path fill="#c8e6c9" stroke="black" d="M339.55,-675.6C339.55,-675.6 278.45,-675.6 278.45,-675.6 272.45,-675.6 266.45,-669.6 266.45,-663.6 266.45,-663.6 266.45,-651.6 266.45,-651.6 266.45,-645.6 272.45,-639.6 278.45,-639.6 278.45,-639.6 339.55,-639.6 339.55,-639.6 345.55,-639.6 351.55,-645.6 351.55,-651.6 351.55,-651.6 351.55,-663.6 351.55,-663.6 351.55,-669.6 345.55,-675.6 339.55,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="309" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Django Admin</text>
<text xml:space="preserve" text-anchor="middle" x="309" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">Interface</text>
</g>
<!-- drf -->
<g id="node3" class="node">
<title>drf</title>
<path fill="#c8e6c9" stroke="black" d="M236.63,-675.6C236.63,-675.6 177.37,-675.6 177.37,-675.6 171.37,-675.6 165.37,-669.6 165.37,-663.6 165.37,-663.6 165.37,-651.6 165.37,-651.6 165.37,-645.6 171.37,-639.6 177.37,-639.6 177.37,-639.6 236.63,-639.6 236.63,-639.6 242.63,-639.6 248.63,-645.6 248.63,-651.6 248.63,-651.6 248.63,-663.6 248.63,-663.6 248.63,-669.6 242.63,-675.6 236.63,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="207" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Django REST</text>
<text xml:space="preserve" text-anchor="middle" x="207" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">Framework</text>
</g>
<!-- jwt_auth -->
<g id="node4" class="node">
<title>jwt_auth</title>
<path fill="#c8e6c9" stroke="black" d="M264.69,-540.4C264.69,-540.4 177.31,-540.4 177.31,-540.4 171.31,-540.4 165.31,-534.4 165.31,-528.4 165.31,-528.4 165.31,-516.4 165.31,-516.4 165.31,-510.4 171.31,-504.4 177.31,-504.4 177.31,-504.4 264.69,-504.4 264.69,-504.4 270.69,-504.4 276.69,-510.4 276.69,-516.4 276.69,-516.4 276.69,-528.4 276.69,-528.4 276.69,-534.4 270.69,-540.4 264.69,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="221" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">JWT Authentication</text>
<text xml:space="preserve" text-anchor="middle" x="221" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(SimpleJWT)</text>
</g>
<!-- drf&#45;&gt;jwt_auth -->
<g id="edge3" class="edge">
<title>drf&#45;&gt;jwt_auth</title>
<path fill="none" stroke="#999999" stroke-dasharray="5,2" d="M207,-639.37C207,-639.37 207,-552.29 207,-552.29"/>
<polygon fill="#999999" stroke="#999999" points="210.5,-552.29 207,-542.29 203.5,-552.29 210.5,-552.29"/>
</g>
<!-- pet -->
<g id="node6" class="node">
<title>pet</title>
<path fill="#bbdefb" stroke="black" d="M714.45,-405.2C714.45,-405.2 673.55,-405.2 673.55,-405.2 667.55,-405.2 661.55,-399.2 661.55,-393.2 661.55,-393.2 661.55,-381.2 661.55,-381.2 661.55,-375.2 667.55,-369.2 673.55,-369.2 673.55,-369.2 714.45,-369.2 714.45,-369.2 720.45,-369.2 726.45,-375.2 726.45,-381.2 726.45,-381.2 726.45,-393.2 726.45,-393.2 726.45,-399.2 720.45,-405.2 714.45,-405.2"/>
<text xml:space="preserve" text-anchor="middle" x="694" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">Pet</text>
<text xml:space="preserve" text-anchor="middle" x="694" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Mascota)</text>
</g>
<!-- petowner&#45;&gt;pet -->
<g id="edge4" class="edge">
<title>petowner&#45;&gt;pet</title>
<path fill="none" stroke="#1565c0" d="M694,-504.17C694,-504.17 694,-417.09 694,-417.09"/>
<polygon fill="#1565c0" stroke="#1565c0" points="697.5,-417.09 694,-407.09 690.5,-417.09 697.5,-417.09"/>
<text xml:space="preserve" text-anchor="middle" x="701" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- servicerequest -->
<g id="node18" class="node">
<title>servicerequest</title>
<path fill="#ffcdd2" stroke="black" d="M1163.82,-405.2C1163.82,-405.2 1094.18,-405.2 1094.18,-405.2 1088.18,-405.2 1082.18,-399.2 1082.18,-393.2 1082.18,-393.2 1082.18,-381.2 1082.18,-381.2 1082.18,-375.2 1088.18,-369.2 1094.18,-369.2 1094.18,-369.2 1163.82,-369.2 1163.82,-369.2 1169.82,-369.2 1175.82,-375.2 1175.82,-381.2 1175.82,-381.2 1175.82,-393.2 1175.82,-393.2 1175.82,-399.2 1169.82,-405.2 1163.82,-405.2"/>
<text xml:space="preserve" text-anchor="middle" x="1129" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">ServiceRequest</text>
<text xml:space="preserve" text-anchor="middle" x="1129" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Solicitud)</text>
</g>
<!-- petowner&#45;&gt;servicerequest -->
<g id="edge5" class="edge">
<title>petowner&#45;&gt;servicerequest</title>
<path fill="none" stroke="#1565c0" d="M729.12,-504.02C729.12,-468.96 729.12,-396 729.12,-396 729.12,-396 1070.44,-396 1070.44,-396"/>
<polygon fill="#1565c0" stroke="#1565c0" points="1070.44,-399.5 1080.44,-396 1070.44,-392.5 1070.44,-399.5"/>
<text xml:space="preserve" text-anchor="middle" x="1005" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- neighborhood -->
<g id="node27" class="node">
<title>neighborhood</title>
<path fill="#e1bee7" stroke="black" d="M109.63,-52C109.63,-52 28.37,-52 28.37,-52 22.37,-52 16.37,-46 16.37,-40 16.37,-40 16.37,-28 16.37,-28 16.37,-22 22.37,-16 28.37,-16 28.37,-16 109.63,-16 109.63,-16 115.63,-16 121.63,-22 121.63,-28 121.63,-28 121.63,-40 121.63,-40 121.63,-46 115.63,-52 109.63,-52"/>
<text xml:space="preserve" text-anchor="middle" x="69" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Neighborhood /</text>
<text xml:space="preserve" text-anchor="middle" x="69" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">Province / Locality</text>
</g>
<!-- petowner&#45;&gt;neighborhood -->
<g id="edge24" class="edge">
<title>petowner&#45;&gt;neighborhood</title>
<path fill="none" stroke="#7b1fa2" d="M655.77,-504.11C655.77,-441.12 655.77,-237 655.77,-237 655.77,-237 86.54,-237 86.54,-237 86.54,-237 86.54,-63.89 86.54,-63.89"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="90.04,-63.89 86.54,-53.89 83.04,-63.89 90.04,-63.89"/>
<text xml:space="preserve" text-anchor="middle" x="86" y="-301.7" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- vetvisit -->
<g id="node8" class="node">
<title>vetvisit</title>
<path fill="#bbdefb" stroke="black" d="M725.37,-322.4C725.37,-322.4 682.63,-322.4 682.63,-322.4 676.63,-322.4 670.63,-316.4 670.63,-310.4 670.63,-310.4 670.63,-298.4 670.63,-298.4 670.63,-292.4 676.63,-286.4 682.63,-286.4 682.63,-286.4 725.37,-286.4 725.37,-286.4 731.37,-286.4 737.37,-292.4 737.37,-298.4 737.37,-298.4 737.37,-310.4 737.37,-310.4 737.37,-316.4 731.37,-322.4 725.37,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="704" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">VetVisit</text>
<text xml:space="preserve" text-anchor="middle" x="704" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Consulta)</text>
</g>
<!-- pet&#45;&gt;vetvisit -->
<g id="edge6" class="edge">
<title>pet&#45;&gt;vetvisit</title>
<path fill="none" stroke="#1565c0" d="M698.54,-368.82C698.54,-368.82 698.54,-334.24 698.54,-334.24"/>
<polygon fill="#1565c0" stroke="#1565c0" points="702.04,-334.24 698.54,-324.24 695.04,-334.24 702.04,-334.24"/>
<text xml:space="preserve" text-anchor="middle" x="710.25" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- pet_health -->
<g id="node11" class="node">
<title>pet_health</title>
<path fill="#bbdefb" stroke="black" d="M640.88,-322.4C640.88,-322.4 587.12,-322.4 587.12,-322.4 581.12,-322.4 575.12,-316.4 575.12,-310.4 575.12,-310.4 575.12,-298.4 575.12,-298.4 575.12,-292.4 581.12,-286.4 587.12,-286.4 587.12,-286.4 640.88,-286.4 640.88,-286.4 646.88,-286.4 652.88,-292.4 652.88,-298.4 652.88,-298.4 652.88,-310.4 652.88,-310.4 652.88,-316.4 646.88,-322.4 640.88,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="614" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">PetVaccine /</text>
<text xml:space="preserve" text-anchor="middle" x="614" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">PetStudy</text>
</g>
<!-- pet&#45;&gt;pet_health -->
<g id="edge7" class="edge">
<title>pet&#45;&gt;pet_health</title>
<path fill="none" stroke="#1565c0" d="M666.09,-368.72C666.09,-342.59 666.09,-298 666.09,-298 666.09,-298 664.73,-298 664.73,-298"/>
<polygon fill="#1565c0" stroke="#1565c0" points="664.73,-294.5 654.73,-298 664.73,-301.5 664.73,-294.5"/>
<text xml:space="preserve" text-anchor="middle" x="623" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- veterinarian&#45;&gt;vetvisit -->
<g id="edge8" class="edge">
<title>veterinarian&#45;&gt;vetvisit</title>
<path fill="none" stroke="#1565c0" d="M556.46,-387C598.38,-387 658.66,-387 658.66,-387 658.66,-387 658.66,-310 658.66,-310 658.66,-310 659.85,-310 659.85,-310"/>
<polygon fill="#1565c0" stroke="#1565c0" points="659.06,-313.5 669.06,-310 659.06,-306.5 659.06,-313.5"/>
<text xml:space="preserve" text-anchor="middle" x="668" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- availability -->
<g id="node10" class="node">
<title>availability</title>
<path fill="#bbdefb" stroke="black" d="M545.4,-322.4C545.4,-322.4 488.6,-322.4 488.6,-322.4 482.6,-322.4 476.6,-316.4 476.6,-310.4 476.6,-310.4 476.6,-298.4 476.6,-298.4 476.6,-292.4 482.6,-286.4 488.6,-286.4 488.6,-286.4 545.4,-286.4 545.4,-286.4 551.4,-286.4 557.4,-292.4 557.4,-298.4 557.4,-298.4 557.4,-310.4 557.4,-310.4 557.4,-316.4 551.4,-322.4 545.4,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="517" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">Availability /</text>
<text xml:space="preserve" text-anchor="middle" x="517" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">Unavailability</text>
</g>
<!-- veterinarian&#45;&gt;availability -->
<g id="edge9" class="edge">
<title>veterinarian&#45;&gt;availability</title>
<path fill="none" stroke="#1565c0" d="M503.94,-368.82C503.94,-368.82 503.94,-334.24 503.94,-334.24"/>
<polygon fill="#1565c0" stroke="#1565c0" points="507.44,-334.24 503.94,-324.24 500.44,-334.24 507.44,-334.24"/>
<text xml:space="preserve" text-anchor="middle" x="524" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- vetasked -->
<g id="node20" class="node">
<title>vetasked</title>
<path fill="#ffcdd2" stroke="black" d="M1173.94,-322.4C1173.94,-322.4 1092.06,-322.4 1092.06,-322.4 1086.06,-322.4 1080.06,-316.4 1080.06,-310.4 1080.06,-310.4 1080.06,-298.4 1080.06,-298.4 1080.06,-292.4 1086.06,-286.4 1092.06,-286.4 1092.06,-286.4 1173.94,-286.4 1173.94,-286.4 1179.94,-286.4 1185.94,-292.4 1185.94,-298.4 1185.94,-298.4 1185.94,-310.4 1185.94,-310.4 1185.94,-316.4 1179.94,-322.4 1173.94,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="1133" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">VeterinarianAsked</text>
<text xml:space="preserve" text-anchor="middle" x="1133" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Vet Consultado)</text>
</g>
<!-- veterinarian&#45;&gt;vetasked -->
<g id="edge23" class="edge">
<title>veterinarian&#45;&gt;vetasked</title>
<path fill="none" stroke="#c62828" d="M530.06,-368.88C530.06,-354.89 530.06,-338 530.06,-338 530.06,-338 1129,-338 1129,-338 1129,-338 1129,-334.29 1129,-334.29"/>
<polygon fill="#c62828" stroke="#c62828" points="1132.5,-334.29 1129,-324.29 1125.5,-334.29 1132.5,-334.29"/>
<text xml:space="preserve" text-anchor="middle" x="959" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- specialty -->
<g id="node24" class="node">
<title>specialty</title>
<path fill="#e1bee7" stroke="black" d="M224.66,-52C224.66,-52 151.34,-52 151.34,-52 145.34,-52 139.34,-46 139.34,-40 139.34,-40 139.34,-28 139.34,-28 139.34,-22 145.34,-16 151.34,-16 151.34,-16 224.66,-16 224.66,-16 230.66,-16 236.66,-22 236.66,-28 236.66,-28 236.66,-40 236.66,-40 236.66,-46 230.66,-52 224.66,-52"/>
<text xml:space="preserve" text-anchor="middle" x="188" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Specialty</text>
<text xml:space="preserve" text-anchor="middle" x="188" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Especialidades)</text>
</g>
<!-- veterinarian&#45;&gt;specialty -->
<g id="edge25" class="edge">
<title>veterinarian&#45;&gt;specialty</title>
<path fill="none" stroke="#7b1fa2" d="M477.62,-387C392.9,-387 200.99,-387 200.99,-387 200.99,-387 200.99,-64 200.99,-64"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="204.49,-64 200.99,-54 197.49,-64 204.49,-64"/>
<text xml:space="preserve" text-anchor="middle" x="289.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- veterinarian&#45;&gt;neighborhood -->
<g id="edge26" class="edge">
<title>veterinarian&#45;&gt;neighborhood</title>
<path fill="none" stroke="#7b1fa2" d="M477.71,-396C365.47,-396 51.46,-396 51.46,-396 51.46,-396 51.46,-63.96 51.46,-63.96"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="54.96,-63.96 51.46,-53.96 47.96,-63.96 54.96,-63.96"/>
<text xml:space="preserve" text-anchor="middle" x="158.01" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M coverage</text>
</g>
<!-- turnfee -->
<g id="node28" class="node">
<title>turnfee</title>
<path fill="#e1bee7" stroke="black" d="M347.02,-52C347.02,-52 266.98,-52 266.98,-52 260.98,-52 254.98,-46 254.98,-40 254.98,-40 254.98,-28 254.98,-28 254.98,-22 260.98,-16 266.98,-16 266.98,-16 347.02,-16 347.02,-16 353.02,-16 359.02,-22 359.02,-28 359.02,-28 359.02,-40 359.02,-40 359.02,-46 353.02,-52 347.02,-52"/>
<text xml:space="preserve" text-anchor="middle" x="307" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">IndividualTurnFee</text>
<text xml:space="preserve" text-anchor="middle" x="307" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">Group</text>
</g>
<!-- veterinarian&#45;&gt;turnfee -->
<g id="edge30" class="edge">
<title>veterinarian&#45;&gt;turnfee</title>
<path fill="none" stroke="#7b1fa2" d="M477.44,-378C429.56,-378 355.29,-378 355.29,-378 355.29,-378 355.29,-64.01 355.29,-64.01"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="358.79,-64.01 355.29,-54.01 351.79,-64.01 358.79,-64.01"/>
<text xml:space="preserve" text-anchor="middle" x="364.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- vetvisitreport -->
<g id="node9" class="node">
<title>vetvisitreport</title>
<path fill="#bbdefb" stroke="black" d="M694.34,-187.2C694.34,-187.2 621.66,-187.2 621.66,-187.2 615.66,-187.2 609.66,-181.2 609.66,-175.2 609.66,-175.2 609.66,-163.2 609.66,-163.2 609.66,-157.2 615.66,-151.2 621.66,-151.2 621.66,-151.2 694.34,-151.2 694.34,-151.2 700.34,-151.2 706.34,-157.2 706.34,-163.2 706.34,-163.2 706.34,-175.2 706.34,-175.2 706.34,-181.2 700.34,-187.2 694.34,-187.2"/>
<text xml:space="preserve" text-anchor="middle" x="658" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">VetVisitReport</text>
<text xml:space="preserve" text-anchor="middle" x="658" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Informe Clinico)</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitreport -->
<g id="edge10" class="edge">
<title>vetvisit&#45;&gt;vetvisitreport</title>
<path fill="none" stroke="#1565c0" d="M688.49,-286.17C688.49,-286.17 688.49,-199.09 688.49,-199.09"/>
<polygon fill="#1565c0" stroke="#1565c0" points="691.99,-199.09 688.49,-189.09 684.99,-199.09 691.99,-199.09"/>
<text xml:space="preserve" text-anchor="middle" x="671" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- vetvisit&#45;&gt;servicerequest -->
<g id="edge11" class="edge">
<title>vetvisit&#45;&gt;servicerequest</title>
<path fill="none" stroke="#1565c0" stroke-dasharray="5,2" d="M731.8,-322.73C731.8,-347.07 731.8,-387 731.8,-387 731.8,-387 1070.36,-387 1070.36,-387"/>
<polygon fill="#1565c0" stroke="#1565c0" points="1070.36,-390.5 1080.36,-387 1070.36,-383.5 1070.36,-390.5"/>
<text xml:space="preserve" text-anchor="middle" x="749.26" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
</g>
<!-- google_cal -->
<g id="node31" class="node">
<title>google_cal</title>
<path fill="#ffe0b2" stroke="black" d="M1122.58,-52C1122.58,-52 1047.42,-52 1047.42,-52 1041.42,-52 1035.42,-46 1035.42,-40 1035.42,-40 1035.42,-28 1035.42,-28 1035.42,-22 1041.42,-16 1047.42,-16 1047.42,-16 1122.58,-16 1122.58,-16 1128.58,-16 1134.58,-22 1134.58,-28 1134.58,-28 1134.58,-40 1134.58,-40 1134.58,-46 1128.58,-52 1122.58,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1085" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Google Calendar</text>
<text xml:space="preserve" text-anchor="middle" x="1085" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Agenda)</text>
</g>
<!-- vetvisit&#45;&gt;google_cal -->
<g id="edge33" class="edge">
<title>vetvisit&#45;&gt;google_cal</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M737.59,-304C824.11,-304 1047.82,-304 1047.82,-304 1047.82,-304 1047.82,-63.65 1047.82,-63.65"/>
<polygon fill="#e65100" stroke="#e65100" points="1051.32,-63.65 1047.82,-53.65 1044.32,-63.65 1051.32,-63.65"/>
<text xml:space="preserve" text-anchor="middle" x="1054.25" y="-166.5" font-family="Helvetica,sans-Serif" font-size="9.00">sync</text>
</g>
<!-- receipt -->
<g id="node36" class="node">
<title>receipt</title>
<path fill="#fff9c4" stroke="black" d="M880.99,-187.2C880.99,-187.2 815.01,-187.2 815.01,-187.2 809.01,-187.2 803.01,-181.2 803.01,-175.2 803.01,-175.2 803.01,-163.2 803.01,-163.2 803.01,-157.2 809.01,-151.2 815.01,-151.2 815.01,-151.2 880.99,-151.2 880.99,-151.2 886.99,-151.2 892.99,-157.2 892.99,-163.2 892.99,-163.2 892.99,-175.2 892.99,-175.2 892.99,-181.2 886.99,-187.2 880.99,-187.2"/>
<text xml:space="preserve" text-anchor="middle" x="848" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">Receipt</text>
<text xml:space="preserve" text-anchor="middle" x="848" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Comprobante)</text>
</g>
<!-- vetvisit&#45;&gt;receipt -->
<g id="edge36" class="edge">
<title>vetvisit&#45;&gt;receipt</title>
<path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M722.89,-286.14C722.89,-249.18 722.89,-169 722.89,-169 722.89,-169 791.32,-169 791.32,-169"/>
<polygon fill="#f9a825" stroke="#f9a825" points="791.32,-172.5 801.32,-169 791.32,-165.5 791.32,-172.5"/>
<text xml:space="preserve" text-anchor="middle" x="727.27" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:1 optional</text>
</g>
<!-- medication -->
<g id="node25" class="node">
<title>medication</title>
<path fill="#e1bee7" stroke="black" d="M678.43,-52C678.43,-52 607.57,-52 607.57,-52 601.57,-52 595.57,-46 595.57,-40 595.57,-40 595.57,-28 595.57,-28 595.57,-22 601.57,-16 607.57,-16 607.57,-16 678.43,-16 678.43,-16 684.43,-16 690.43,-22 690.43,-28 690.43,-28 690.43,-40 690.43,-40 690.43,-46 684.43,-52 678.43,-52"/>
<text xml:space="preserve" text-anchor="middle" x="643" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Medication</text>
<text xml:space="preserve" text-anchor="middle" x="643" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Medicamentos)</text>
</g>
<!-- vetvisitreport&#45;&gt;medication -->
<g id="edge29" class="edge">
<title>vetvisitreport&#45;&gt;medication</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M650.04,-150.97C650.04,-150.97 650.04,-63.89 650.04,-63.89"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="653.54,-63.89 650.04,-53.89 646.54,-63.89 653.54,-63.89"/>
<text xml:space="preserve" text-anchor="middle" x="667.26" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">references</text>
</g>
<!-- service -->
<g id="node12" class="node">
<title>service</title>
<path fill="#c8e6c9" stroke="black" d="M972.22,-675.6C972.22,-675.6 933.78,-675.6 933.78,-675.6 927.78,-675.6 921.78,-669.6 921.78,-663.6 921.78,-663.6 921.78,-651.6 921.78,-651.6 921.78,-645.6 927.78,-639.6 933.78,-639.6 933.78,-639.6 972.22,-639.6 972.22,-639.6 978.22,-639.6 984.22,-645.6 984.22,-651.6 984.22,-651.6 984.22,-663.6 984.22,-663.6 984.22,-669.6 978.22,-675.6 972.22,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="953" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Service</text>
<text xml:space="preserve" text-anchor="middle" x="953" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">(Servicio)</text>
</g>
<!-- category -->
<g id="node13" class="node">
<title>category</title>
<path fill="#c8e6c9" stroke="black" d="M849.19,-540.4C849.19,-540.4 772.81,-540.4 772.81,-540.4 766.81,-540.4 760.81,-534.4 760.81,-528.4 760.81,-528.4 760.81,-516.4 760.81,-516.4 760.81,-510.4 766.81,-504.4 772.81,-504.4 772.81,-504.4 849.19,-504.4 849.19,-504.4 855.19,-504.4 861.19,-510.4 861.19,-516.4 861.19,-516.4 861.19,-528.4 861.19,-528.4 861.19,-534.4 855.19,-540.4 849.19,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="811" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Category / Group</text>
<text xml:space="preserve" text-anchor="middle" x="811" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Categorias)</text>
</g>
<!-- service&#45;&gt;category -->
<g id="edge12" class="edge">
<title>service&#45;&gt;category</title>
<path fill="none" stroke="#2e7d32" d="M921.49,-652C879.65,-652 811,-652 811,-652 811,-652 811,-552.21 811,-552.21"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="814.5,-552.21 811,-542.21 807.5,-552.21 814.5,-552.21"/>
<text xml:space="preserve" text-anchor="middle" x="835" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- prices -->
<g id="node14" class="node">
<title>prices</title>
<path fill="#c8e6c9" stroke="black" d="M927,-540.4C927,-540.4 891,-540.4 891,-540.4 885,-540.4 879,-534.4 879,-528.4 879,-528.4 879,-516.4 879,-516.4 879,-510.4 885,-504.4 891,-504.4 891,-504.4 927,-504.4 927,-504.4 933,-504.4 939,-510.4 939,-516.4 939,-516.4 939,-528.4 939,-528.4 939,-534.4 933,-540.4 927,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="909" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Prices</text>
<text xml:space="preserve" text-anchor="middle" x="909" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Precios)</text>
</g>
<!-- service&#45;&gt;prices -->
<g id="edge13" class="edge">
<title>service&#45;&gt;prices</title>
<path fill="none" stroke="#2e7d32" d="M930.39,-639.37C930.39,-639.37 930.39,-552.29 930.39,-552.29"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="933.89,-552.29 930.39,-542.29 926.89,-552.29 933.89,-552.29"/>
<text xml:space="preserve" text-anchor="middle" x="929" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- discounts -->
<g id="node15" class="node">
<title>discounts</title>
<path fill="#c8e6c9" stroke="black" d="M1026.71,-540.4C1026.71,-540.4 969.29,-540.4 969.29,-540.4 963.29,-540.4 957.29,-534.4 957.29,-528.4 957.29,-528.4 957.29,-516.4 957.29,-516.4 957.29,-510.4 963.29,-504.4 969.29,-504.4 969.29,-504.4 1026.71,-504.4 1026.71,-504.4 1032.71,-504.4 1038.71,-510.4 1038.71,-516.4 1038.71,-516.4 1038.71,-528.4 1038.71,-528.4 1038.71,-534.4 1032.71,-540.4 1026.71,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="998" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Discounts</text>
<text xml:space="preserve" text-anchor="middle" x="998" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Descuentos)</text>
</g>
<!-- service&#45;&gt;discounts -->
<g id="edge14" class="edge">
<title>service&#45;&gt;discounts</title>
<path fill="none" stroke="#2e7d32" d="M970.76,-639.37C970.76,-639.37 970.76,-552.29 970.76,-552.29"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="974.26,-552.29 970.76,-542.29 967.26,-552.29 974.26,-552.29"/>
<text xml:space="preserve" text-anchor="middle" x="992" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- cart -->
<g id="node16" class="node">
<title>cart</title>
<path fill="#c8e6c9" stroke="black" d="M1133.06,-540.4C1133.06,-540.4 1068.94,-540.4 1068.94,-540.4 1062.94,-540.4 1056.94,-534.4 1056.94,-528.4 1056.94,-528.4 1056.94,-516.4 1056.94,-516.4 1056.94,-510.4 1062.94,-504.4 1068.94,-504.4 1068.94,-504.4 1133.06,-504.4 1133.06,-504.4 1139.06,-504.4 1145.06,-510.4 1145.06,-516.4 1145.06,-516.4 1145.06,-528.4 1145.06,-528.4 1145.06,-534.4 1139.06,-540.4 1133.06,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="1101" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Cart / CartItem</text>
<text xml:space="preserve" text-anchor="middle" x="1101" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Carrito)</text>
</g>
<!-- service&#45;&gt;cart -->
<g id="edge17" class="edge">
<title>service&#45;&gt;cart</title>
<path fill="none" stroke="#2e7d32" d="M984.39,-658C1027.83,-658 1101,-658 1101,-658 1101,-658 1101,-552.2 1101,-552.2"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1104.5,-552.2 1101,-542.2 1097.5,-552.2 1104.5,-552.2"/>
<text xml:space="preserve" text-anchor="middle" x="1089.26" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">via CartItem</text>
</g>
<!-- cart&#45;&gt;petowner -->
<g id="edge15" class="edge">
<title>cart&#45;&gt;petowner</title>
<path fill="none" stroke="#2e7d32" d="M1064.64,-504.13C1064.64,-489.36 1064.64,-471 1064.64,-471 1064.64,-471 734.47,-471 734.47,-471 734.47,-471 734.47,-492.62 734.47,-492.62"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="730.97,-492.62 734.47,-502.62 737.97,-492.62 730.97,-492.62"/>
<text xml:space="preserve" text-anchor="middle" x="871" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- cart&#45;&gt;veterinarian -->
<g id="edge16" class="edge">
<title>cart&#45;&gt;veterinarian</title>
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M1072.35,-504.09C1072.35,-479.26 1072.35,-438 1072.35,-438 1072.35,-438 530.06,-438 530.06,-438 530.06,-438 530.06,-417.05 530.06,-417.05"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="533.56,-417.05 530.06,-407.05 526.56,-417.05 533.56,-417.05"/>
<text xml:space="preserve" text-anchor="middle" x="886.02" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:1 optional</text>
</g>
<!-- combo -->
<g id="node17" class="node">
<title>combo</title>
<path fill="#c8e6c9" stroke="black" d="M985.07,-810.8C985.07,-810.8 920.93,-810.8 920.93,-810.8 914.93,-810.8 908.93,-804.8 908.93,-798.8 908.93,-798.8 908.93,-786.8 908.93,-786.8 908.93,-780.8 914.93,-774.8 920.93,-774.8 920.93,-774.8 985.07,-774.8 985.07,-774.8 991.07,-774.8 997.07,-780.8 997.07,-786.8 997.07,-786.8 997.07,-798.8 997.07,-798.8 997.07,-804.8 991.07,-810.8 985.07,-810.8"/>
<text xml:space="preserve" text-anchor="middle" x="953" y="-796.1" font-family="Helvetica,sans-Serif" font-size="11.00">ServiceCombo</text>
<text xml:space="preserve" text-anchor="middle" x="953" y="-782.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Paquetes)</text>
</g>
<!-- combo&#45;&gt;service -->
<g id="edge18" class="edge">
<title>combo&#45;&gt;service</title>
<path fill="none" stroke="#2e7d32" d="M953,-774.57C953,-774.57 953,-687.49 953,-687.49"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="956.5,-687.49 953,-677.49 949.5,-687.49 956.5,-687.49"/>
<text xml:space="preserve" text-anchor="middle" x="969.76" y="-748.7" font-family="Helvetica,sans-Serif" font-size="9.00">contains</text>
</g>
<!-- servicerequest&#45;&gt;cart -->
<g id="edge19" class="edge">
<title>servicerequest&#45;&gt;cart</title>
<path fill="none" stroke="#c62828" d="M1113.62,-405.43C1113.62,-405.43 1113.62,-492.51 1113.62,-492.51"/>
<polygon fill="#c62828" stroke="#c62828" points="1110.12,-492.51 1113.62,-502.51 1117.12,-492.51 1110.12,-492.51"/>
<text xml:space="preserve" text-anchor="middle" x="1121.26" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
</g>
<!-- statehistory -->
<g id="node19" class="node">
<title>statehistory</title>
<path fill="#ffcdd2" stroke="black" d="M1267.95,-322.4C1267.95,-322.4 1216.05,-322.4 1216.05,-322.4 1210.05,-322.4 1204.05,-316.4 1204.05,-310.4 1204.05,-310.4 1204.05,-298.4 1204.05,-298.4 1204.05,-292.4 1210.05,-286.4 1216.05,-286.4 1216.05,-286.4 1267.95,-286.4 1267.95,-286.4 1273.95,-286.4 1279.95,-292.4 1279.95,-298.4 1279.95,-298.4 1279.95,-310.4 1279.95,-310.4 1279.95,-316.4 1273.95,-322.4 1267.95,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="1242" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">StateHistory</text>
<text xml:space="preserve" text-anchor="middle" x="1242" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Historial)</text>
</g>
<!-- servicerequest&#45;&gt;statehistory -->
<g id="edge20" class="edge">
<title>servicerequest&#45;&gt;statehistory</title>
<path fill="none" stroke="#c62828" d="M1176.2,-378C1201.95,-378 1228.64,-378 1228.64,-378 1228.64,-378 1228.64,-334.11 1228.64,-334.11"/>
<polygon fill="#c62828" stroke="#c62828" points="1232.14,-334.11 1228.64,-324.11 1225.14,-334.11 1232.14,-334.11"/>
<text xml:space="preserve" text-anchor="middle" x="1212" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- servicerequest&#45;&gt;vetasked -->
<g id="edge21" class="edge">
<title>servicerequest&#45;&gt;vetasked</title>
<path fill="none" stroke="#c62828" d="M1152.41,-368.82C1152.41,-368.82 1152.41,-334.24 1152.41,-334.24"/>
<polygon fill="#c62828" stroke="#c62828" points="1155.91,-334.24 1152.41,-324.24 1148.91,-334.24 1155.91,-334.24"/>
<text xml:space="preserve" text-anchor="middle" x="1137" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- campaign -->
<g id="node22" class="node">
<title>campaign</title>
<path fill="#e1bee7" stroke="black" d="M436.81,-52C436.81,-52 389.19,-52 389.19,-52 383.19,-52 377.19,-46 377.19,-40 377.19,-40 377.19,-28 377.19,-28 377.19,-22 383.19,-16 389.19,-16 389.19,-16 436.81,-16 436.81,-16 442.81,-16 448.81,-22 448.81,-28 448.81,-28 448.81,-40 448.81,-40 448.81,-46 442.81,-52 436.81,-52"/>
<text xml:space="preserve" text-anchor="middle" x="413" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Campaign</text>
<text xml:space="preserve" text-anchor="middle" x="413" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Marketing)</text>
</g>
<!-- servicerequest&#45;&gt;campaign -->
<g id="edge27" class="edge">
<title>servicerequest&#45;&gt;campaign</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M1105.59,-369.05C1105.59,-361.29 1105.59,-354 1105.59,-354 1105.59,-354 401.06,-354 401.06,-354 401.06,-354 401.06,-63.63 401.06,-63.63"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="404.56,-63.63 401.06,-53.63 397.56,-63.64 404.56,-63.63"/>
<text xml:space="preserve" text-anchor="middle" x="437.02" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:1 optional</text>
</g>
<!-- tag -->
<g id="node23" class="node">
<title>tag</title>
<path fill="#e1bee7" stroke="black" d="M765.59,-52C765.59,-52 720.41,-52 720.41,-52 714.41,-52 708.41,-46 708.41,-40 708.41,-40 708.41,-28 708.41,-28 708.41,-22 714.41,-16 720.41,-16 720.41,-16 765.59,-16 765.59,-16 771.59,-16 777.59,-22 777.59,-28 777.59,-28 777.59,-40 777.59,-40 777.59,-46 771.59,-52 765.59,-52"/>
<text xml:space="preserve" text-anchor="middle" x="743" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Tag</text>
<text xml:space="preserve" text-anchor="middle" x="743" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Etiquetas)</text>
</g>
<!-- servicerequest&#45;&gt;tag -->
<g id="edge28" class="edge">
<title>servicerequest&#45;&gt;tag</title>
<path fill="none" stroke="#7b1fa2" d="M1081.96,-378C984.44,-378 769.2,-378 769.2,-378 769.2,-378 769.2,-64.01 769.2,-64.01"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="772.7,-64.01 769.2,-54.01 765.7,-64.01 772.7,-64.01"/>
<text xml:space="preserve" text-anchor="middle" x="783.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- mercadopago -->
<g id="node29" class="node">
<title>mercadopago</title>
<path fill="#f8bbd9" stroke="black" d="M1241.24,-540.4C1241.24,-540.4 1180.76,-540.4 1180.76,-540.4 1174.76,-540.4 1168.76,-534.4 1168.76,-528.4 1168.76,-528.4 1168.76,-516.4 1168.76,-516.4 1168.76,-510.4 1174.76,-504.4 1180.76,-504.4 1180.76,-504.4 1241.24,-504.4 1241.24,-504.4 1247.24,-504.4 1253.24,-510.4 1253.24,-516.4 1253.24,-516.4 1253.24,-528.4 1253.24,-528.4 1253.24,-534.4 1247.24,-540.4 1241.24,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="1211" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">MercadoPago</text>
<text xml:space="preserve" text-anchor="middle" x="1211" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">Account</text>
</g>
<!-- servicerequest&#45;&gt;mercadopago -->
<g id="edge31" class="edge">
<title>servicerequest&#45;&gt;mercadopago</title>
<path fill="none" stroke="#ad1457" d="M1172.29,-405.43C1172.29,-405.43 1172.29,-492.51 1172.29,-492.51"/>
<polygon fill="#ad1457" stroke="#ad1457" points="1168.79,-492.51 1172.29,-502.51 1175.79,-492.51 1168.79,-492.51"/>
<text xml:space="preserve" text-anchor="middle" x="1205.26" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">payment</text>
</g>
<!-- mercately -->
<g id="node33" class="node">
<title>mercately</title>
<path fill="#ffe0b2" stroke="black" d="M1523.04,-52C1523.04,-52 1472.96,-52 1472.96,-52 1466.96,-52 1460.96,-46 1460.96,-40 1460.96,-40 1460.96,-28 1460.96,-28 1460.96,-22 1466.96,-16 1472.96,-16 1472.96,-16 1523.04,-16 1523.04,-16 1529.04,-16 1535.04,-22 1535.04,-28 1535.04,-28 1535.04,-40 1535.04,-40 1535.04,-46 1529.04,-52 1523.04,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1498" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Mercately</text>
<text xml:space="preserve" text-anchor="middle" x="1498" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(WhatsApp)</text>
</g>
<!-- servicerequest&#45;&gt;mercately -->
<g id="edge34" class="edge">
<title>servicerequest&#45;&gt;mercately</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1176.11,-387C1275.59,-387 1498,-387 1498,-387 1498,-387 1498,-64 1498,-64"/>
<polygon fill="#e65100" stroke="#e65100" points="1501.5,-64 1498,-54 1494.5,-64 1501.5,-64"/>
<text xml:space="preserve" text-anchor="middle" x="1350.76" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">notify</text>
</g>
<!-- reminders -->
<g id="node21" class="node">
<title>reminders</title>
<path fill="#ffcdd2" stroke="black" d="M1249.9,-187.2C1249.9,-187.2 1182.1,-187.2 1182.1,-187.2 1176.1,-187.2 1170.1,-181.2 1170.1,-175.2 1170.1,-175.2 1170.1,-163.2 1170.1,-163.2 1170.1,-157.2 1176.1,-151.2 1182.1,-151.2 1182.1,-151.2 1249.9,-151.2 1249.9,-151.2 1255.9,-151.2 1261.9,-157.2 1261.9,-163.2 1261.9,-163.2 1261.9,-175.2 1261.9,-175.2 1261.9,-181.2 1255.9,-187.2 1249.9,-187.2"/>
<text xml:space="preserve" text-anchor="middle" x="1216" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">Reminders</text>
<text xml:space="preserve" text-anchor="middle" x="1216" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Recordatorios)</text>
</g>
<!-- vetasked&#45;&gt;reminders -->
<g id="edge22" class="edge">
<title>vetasked&#45;&gt;reminders</title>
<path fill="none" stroke="#c62828" d="M1178.02,-286.17C1178.02,-286.17 1178.02,-199.09 1178.02,-199.09"/>
<polygon fill="#c62828" stroke="#c62828" points="1181.52,-199.09 1178.02,-189.09 1174.52,-199.09 1181.52,-199.09"/>
<text xml:space="preserve" text-anchor="middle" x="1181" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- celery -->
<g id="node35" class="node">
<title>celery</title>
<path fill="#ffe0b2" stroke="black" d="M1332.84,-52C1332.84,-52 1271.16,-52 1271.16,-52 1265.16,-52 1259.16,-46 1259.16,-40 1259.16,-40 1259.16,-28 1259.16,-28 1259.16,-22 1265.16,-16 1271.16,-16 1271.16,-16 1332.84,-16 1332.84,-16 1338.84,-16 1344.84,-22 1344.84,-28 1344.84,-28 1344.84,-40 1344.84,-40 1344.84,-46 1338.84,-52 1332.84,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1302" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Celery</text>
<text xml:space="preserve" text-anchor="middle" x="1302" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Async Tasks)</text>
</g>
<!-- reminders&#45;&gt;celery -->
<g id="edge35" class="edge">
<title>reminders&#45;&gt;celery</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1250.27,-150.97C1250.27,-114.06 1250.27,-34 1250.27,-34 1250.27,-34 1251.13,-34 1251.13,-34"/>
<polygon fill="#e65100" stroke="#e65100" points="1247.3,-37.5 1257.3,-34 1247.3,-30.5 1247.3,-37.5"/>
<text xml:space="preserve" text-anchor="middle" x="1270.52" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">scheduled</text>
</g>
<!-- breed -->
<g id="node26" class="node">
<title>breed</title>
<path fill="#e1bee7" stroke="black" d="M565.08,-52C565.08,-52 478.92,-52 478.92,-52 472.92,-52 466.92,-46 466.92,-40 466.92,-40 466.92,-28 466.92,-28 466.92,-22 472.92,-16 478.92,-16 478.92,-16 565.08,-16 565.08,-16 571.08,-16 577.08,-22 577.08,-28 577.08,-28 577.08,-40 577.08,-40 577.08,-46 571.08,-52 565.08,-52"/>
<text xml:space="preserve" text-anchor="middle" x="522" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">PetBreed / Vaccine</text>
<text xml:space="preserve" text-anchor="middle" x="522" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/ Study</text>
</g>
<!-- mpnotification -->
<g id="node30" class="node">
<title>mpnotification</title>
<path fill="#f8bbd9" stroke="black" d="M1348.99,-540.4C1348.99,-540.4 1283.01,-540.4 1283.01,-540.4 1277.01,-540.4 1271.01,-534.4 1271.01,-528.4 1271.01,-528.4 1271.01,-516.4 1271.01,-516.4 1271.01,-510.4 1277.01,-504.4 1283.01,-504.4 1283.01,-504.4 1348.99,-504.4 1348.99,-504.4 1354.99,-504.4 1360.99,-510.4 1360.99,-516.4 1360.99,-516.4 1360.99,-528.4 1360.99,-528.4 1360.99,-534.4 1354.99,-540.4 1348.99,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="1316" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">MP Notification</text>
<text xml:space="preserve" text-anchor="middle" x="1316" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Webhooks)</text>
</g>
<!-- mpnotification&#45;&gt;servicerequest -->
<g id="edge32" class="edge">
<title>mpnotification&#45;&gt;servicerequest</title>
<path fill="none" stroke="#ad1457" d="M1275.48,-504.02C1275.48,-468.96 1275.48,-396 1275.48,-396 1275.48,-396 1187.67,-396 1187.67,-396"/>
<polygon fill="#ad1457" stroke="#ad1457" points="1187.67,-392.5 1177.67,-396 1187.67,-399.5 1187.67,-392.5"/>
<text xml:space="preserve" text-anchor="middle" x="1305" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">confirms</text>
</g>
<!-- google_sheets -->
<g id="node32" class="node">
<title>google_sheets</title>
<path fill="#ffe0b2" stroke="black" d="M1229.39,-52C1229.39,-52 1164.61,-52 1164.61,-52 1158.61,-52 1152.61,-46 1152.61,-40 1152.61,-40 1152.61,-28 1152.61,-28 1152.61,-22 1158.61,-16 1164.61,-16 1164.61,-16 1229.39,-16 1229.39,-16 1235.39,-16 1241.39,-22 1241.39,-28 1241.39,-28 1241.39,-40 1241.39,-40 1241.39,-46 1235.39,-52 1229.39,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1197" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Google Sheets</text>
<text xml:space="preserve" text-anchor="middle" x="1197" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Exports)</text>
</g>
<!-- afip -->
<g id="node34" class="node">
<title>afip</title>
<path fill="#ffe0b2" stroke="black" d="M1431.4,-52C1431.4,-52 1374.6,-52 1374.6,-52 1368.6,-52 1362.6,-46 1362.6,-40 1362.6,-40 1362.6,-28 1362.6,-28 1362.6,-22 1368.6,-16 1374.6,-16 1374.6,-16 1431.4,-16 1431.4,-16 1437.4,-16 1443.4,-22 1443.4,-28 1443.4,-28 1443.4,-40 1443.4,-40 1443.4,-46 1437.4,-52 1431.4,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1403" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">AFIP</text>
<text xml:space="preserve" text-anchor="middle" x="1403" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Facturacion)</text>
</g>
<!-- taxpayer -->
<g id="node37" class="node">
<title>taxpayer</title>
<path fill="#fff9c4" stroke="black" d="M881.91,-52C881.91,-52 814.09,-52 814.09,-52 808.09,-52 802.09,-46 802.09,-40 802.09,-40 802.09,-28 802.09,-28 802.09,-22 808.09,-16 814.09,-16 814.09,-16 881.91,-16 881.91,-16 887.91,-16 893.91,-22 893.91,-28 893.91,-28 893.91,-40 893.91,-40 893.91,-46 887.91,-52 881.91,-52"/>
<text xml:space="preserve" text-anchor="middle" x="848" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">TaxPayer</text>
<text xml:space="preserve" text-anchor="middle" x="848" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Contribuyente)</text>
</g>
<!-- receipt&#45;&gt;taxpayer -->
<g id="edge37" class="edge">
<title>receipt&#45;&gt;taxpayer</title>
<path fill="none" stroke="#f9a825" d="M848,-150.97C848,-150.97 848,-63.89 848,-63.89"/>
<polygon fill="#f9a825" stroke="#f9a825" points="851.5,-63.89 848,-53.89 844.5,-63.89 851.5,-63.89"/>
<text xml:space="preserve" text-anchor="middle" x="855" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- pos -->
<g id="node38" class="node">
<title>pos</title>
<path fill="#fff9c4" stroke="black" d="M998.58,-52C998.58,-52 923.42,-52 923.42,-52 917.42,-52 911.42,-46 911.42,-40 911.42,-40 911.42,-28 911.42,-28 911.42,-22 917.42,-16 923.42,-16 923.42,-16 998.58,-16 998.58,-16 1004.58,-16 1010.58,-22 1010.58,-28 1010.58,-28 1010.58,-40 1010.58,-40 1010.58,-46 1004.58,-52 998.58,-52"/>
<text xml:space="preserve" text-anchor="middle" x="961" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">PointOfSales</text>
<text xml:space="preserve" text-anchor="middle" x="961" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Punto de Venta)</text>
</g>
<!-- receipt&#45;&gt;pos -->
<g id="edge38" class="edge">
<title>receipt&#45;&gt;pos</title>
<path fill="none" stroke="#f9a825" d="M893.36,-169C910.17,-169 925.21,-169 925.21,-169 925.21,-169 925.21,-64 925.21,-64"/>
<polygon fill="#f9a825" stroke="#f9a825" points="928.71,-64 925.21,-54 921.71,-64 928.71,-64"/>
<text xml:space="preserve" text-anchor="middle" x="930" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,184 @@
digraph FrontendArchitecture {
// Graph settings
rankdir=TB
compound=true
splines=ortho
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11]
edge [fontname="Helvetica", fontsize=9]
label="AMAR Mascotas - Frontend Architecture (Next.js)\n\n"
labelloc="t"
fontsize=16
fontname="Helvetica-Bold"
// Next.js Core Cluster
subgraph cluster_nextjs {
label="Next.js 13+ (App Router)"
style="rounded,filled"
fillcolor="#E3F2FD"
color="#1565C0"
app_router [label="App Router\n/app/*", fillcolor="#BBDEFB"]
layout [label="Layout Components\n(RootLayout, etc)", fillcolor="#BBDEFB"]
middleware [label="Middleware\n(Auth redirect)", fillcolor="#BBDEFB"]
}
// Public Frontend Cluster
subgraph cluster_public {
label="Public Frontend (/(frontend))"
style="rounded,filled"
fillcolor="#E8F5E9"
color="#2E7D32"
home [label="Home Page\n/", fillcolor="#C8E6C9"]
services_page [label="Services Catalog\n/servicios", fillcolor="#C8E6C9"]
cart_page [label="Cart\n/carrito", fillcolor="#C8E6C9"]
login_page [label="Login/Register\n/login", fillcolor="#C8E6C9"]
profile_page [label="User Profile\n/perfil", fillcolor="#C8E6C9"]
pets_page [label="My Pets\n/mascotas", fillcolor="#C8E6C9"]
requests_page [label="My Requests\n/solicitudes", fillcolor="#C8E6C9"]
}
// Backoffice Cluster
subgraph cluster_backoffice {
label="Backoffice (/(backoffice)/admin)"
style="rounded,filled"
fillcolor="#FFF3E0"
color="#E65100"
admin_dash [label="Dashboard\n/admin", fillcolor="#FFE0B2"]
admin_visits [label="Visits Management\n/admin/visits", fillcolor="#FFE0B2"]
admin_pets [label="Pets Overview\n/admin/pets", fillcolor="#FFE0B2"]
admin_requests [label="Service Requests\n/admin/solicitudes", fillcolor="#FFE0B2"]
admin_calendar [label="Calendar View\n/admin/calendario", fillcolor="#FFE0B2"]
}
// Components Cluster
subgraph cluster_components {
label="Shared Components (/components)"
style="rounded,filled"
fillcolor="#F3E5F5"
color="#7B1FA2"
sidebar [label="Sidebar\nNavigation", fillcolor="#E1BEE7"]
navbar [label="NavbarBackoffice\nTop Bar", fillcolor="#E1BEE7"]
visits_section [label="VisitsSection\n(List + Actions)", fillcolor="#E1BEE7"]
drawer [label="VisitsDrawer\n(Side Panel)", fillcolor="#E1BEE7"]
tables [label="DataTable\nComponents", fillcolor="#E1BEE7"]
forms [label="Form Components\n(Pet, Visit, etc)", fillcolor="#E1BEE7"]
}
// Services Layer Cluster
subgraph cluster_services {
label="Services Layer (/services)"
style="rounded,filled"
fillcolor="#FFEBEE"
color="#C62828"
http_service [label="HttpService\n(Axios wrapper)", fillcolor="#FFCDD2"]
auth_api [label="authAPI\n(login/register)", fillcolor="#FFCDD2"]
visits_api [label="visitsAPI\n(CRUD visits)", fillcolor="#FFCDD2"]
orders_api [label="OrdersAPI\n(service requests)", fillcolor="#FFCDD2"]
petowners_api [label="petOwnersAPI\n(clients)", fillcolor="#FFCDD2"]
vets_api [label="VeterinariansAPI\n(professionals)", fillcolor="#FFCDD2"]
services_api [label="servicesAPI\n(catalog)", fillcolor="#FFCDD2"]
cart_api [label="CartAPI\n(shopping)", fillcolor="#FFCDD2"]
}
// State Management Cluster
subgraph cluster_state {
label="State Management (/redux, /contexts)"
style="rounded,filled"
fillcolor="#E0F7FA"
color="#00838F"
redux_store [label="Redux Store\n(Global State)", fillcolor="#B2EBF2"]
auth_slice [label="Auth Slice\n(user, token)", fillcolor="#B2EBF2"]
visits_slice [label="Visits Slice\n(visit data)", fillcolor="#B2EBF2"]
cart_slice [label="Cart Slice\n(items)", fillcolor="#B2EBF2"]
auth_context [label="AuthContext\n(Provider)", fillcolor="#B2EBF2"]
}
// Backend API Cluster (external)
subgraph cluster_backend {
label="Django Backend API"
style="rounded,filled"
fillcolor="#ECEFF1"
color="#455A64"
api_mascotas [label="/mascotas/api/v1/\n(Pets, Vets, Visits)", fillcolor="#CFD8DC"]
api_productos [label="/productos/\n(Services, Prices)", fillcolor="#CFD8DC"]
api_solicitudes [label="/solicitudes/\n(Requests)", fillcolor="#CFD8DC"]
api_auth [label="/api/token/\n(JWT Auth)", fillcolor="#CFD8DC"]
api_payments [label="/payments/\n(MercadoPago)", fillcolor="#CFD8DC"]
}
// User Types
subgraph cluster_users {
label="User Types"
style="rounded,filled"
fillcolor="#FCE4EC"
color="#AD1457"
petowner_user [label="PetOwner\n(Cliente)", shape=ellipse, fillcolor="#F8BBD9"]
vet_user [label="Veterinarian\n(Profesional)", shape=ellipse, fillcolor="#F8BBD9"]
admin_user [label="Admin/Staff\n(Interno)", shape=ellipse, fillcolor="#F8BBD9"]
}
// Relationships - Router
app_router -> layout [color="#1565C0"]
app_router -> middleware [color="#1565C0"]
// Public pages
home -> services_page [color="#2E7D32"]
services_page -> cart_page [color="#2E7D32"]
cart_page -> login_page [label="if not auth", color="#2E7D32", style=dashed]
login_page -> profile_page [color="#2E7D32"]
profile_page -> pets_page [color="#2E7D32"]
profile_page -> requests_page [color="#2E7D32"]
// Backoffice pages
admin_dash -> admin_visits [color="#E65100"]
admin_dash -> admin_pets [color="#E65100"]
admin_dash -> admin_requests [color="#E65100"]
admin_dash -> admin_calendar [color="#E65100"]
// Components used by pages
admin_visits -> visits_section [color="#7B1FA2", style=dashed]
admin_visits -> drawer [color="#7B1FA2", style=dashed]
admin_dash -> sidebar [color="#7B1FA2", style=dashed]
admin_dash -> navbar [color="#7B1FA2", style=dashed]
// Services to APIs
http_service -> auth_api [color="#C62828"]
http_service -> visits_api [color="#C62828"]
http_service -> orders_api [color="#C62828"]
http_service -> petowners_api [color="#C62828"]
http_service -> vets_api [color="#C62828"]
http_service -> services_api [color="#C62828"]
http_service -> cart_api [color="#C62828"]
// Services to Backend
auth_api -> api_auth [label="POST /api/token/", color="#455A64"]
visits_api -> api_mascotas [label="CRUD /vet-visits/", color="#455A64"]
orders_api -> api_solicitudes [label="CRUD /service-requests/", color="#455A64"]
petowners_api -> api_mascotas [label="CRUD /pet-owners/", color="#455A64"]
vets_api -> api_mascotas [label="GET /veterinarians/", color="#455A64"]
services_api -> api_productos [label="GET /services/", color="#455A64"]
cart_api -> api_productos [label="CRUD /cart/", color="#455A64"]
// State connections
auth_slice -> redux_store [color="#00838F"]
visits_slice -> redux_store [color="#00838F"]
cart_slice -> redux_store [color="#00838F"]
auth_context -> redux_store [color="#00838F", style=dashed]
// User access paths
petowner_user -> home [label="public access", color="#AD1457"]
petowner_user -> profile_page [label="authenticated", color="#AD1457", style=dashed]
vet_user -> admin_dash [label="backoffice", color="#AD1457"]
admin_user -> admin_dash [label="full access", color="#AD1457"]
// API routing through HttpService
http_service -> api_auth [label="JWT refresh", color="#455A64", style=dashed]
}

View File

@@ -0,0 +1,595 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: FrontendArchitecture Pages: 1 -->
<svg width="2391pt" height="715pt"
viewBox="0.00 0.00 2391.00 715.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 710.85)">
<title>FrontendArchitecture</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-710.85 2387,-710.85 2387,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="1191.5" y="-687.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Frontend Architecture (Next.js)</text>
<g id="clust1" class="cluster">
<title>cluster_nextjs</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M20,-452.8C20,-452.8 230,-452.8 230,-452.8 236,-452.8 242,-458.8 242,-464.8 242,-464.8 242,-634.32 242,-634.32 242,-640.32 236,-646.32 230,-646.32 230,-646.32 20,-646.32 20,-646.32 14,-646.32 8,-640.32 8,-634.32 8,-634.32 8,-464.8 8,-464.8 8,-458.8 14,-452.8 20,-452.8"/>
<text xml:space="preserve" text-anchor="middle" x="125" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Next.js 13+ (App Router)</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_public</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M262,-8C262,-8 454,-8 454,-8 460,-8 466,-14 466,-20 466,-20 466,-520 466,-520 466,-526 460,-532 454,-532 454,-532 262,-532 262,-532 256,-532 250,-526 250,-520 250,-520 250,-20 250,-20 250,-14 256,-8 262,-8"/>
<text xml:space="preserve" text-anchor="middle" x="358" y="-512.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Public Frontend (/(frontend))</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_backoffice</title>
<path fill="#fff3e0" stroke="#e65100" d="M647,-344.8C647,-344.8 1095,-344.8 1095,-344.8 1101,-344.8 1107,-350.8 1107,-356.8 1107,-356.8 1107,-520 1107,-520 1107,-526 1101,-532 1095,-532 1095,-532 647,-532 647,-532 641,-532 635,-526 635,-520 635,-520 635,-356.8 635,-356.8 635,-350.8 641,-344.8 647,-344.8"/>
<text xml:space="preserve" text-anchor="middle" x="871" y="-512.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Backoffice (/(backoffice)/admin)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_components</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M486,-236.8C486,-236.8 1091,-236.8 1091,-236.8 1097,-236.8 1103,-242.8 1103,-248.8 1103,-248.8 1103,-304 1103,-304 1103,-310 1097,-316 1091,-316 1091,-316 486,-316 486,-316 480,-316 474,-310 474,-304 474,-304 474,-248.8 474,-248.8 474,-242.8 480,-236.8 486,-236.8"/>
<text xml:space="preserve" text-anchor="middle" x="788.5" y="-296.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Shared Components (/components)</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_services</title>
<path fill="#ffebee" stroke="#c62828" d="M1222,-452.8C1222,-452.8 1922,-452.8 1922,-452.8 1928,-452.8 1934,-458.8 1934,-464.8 1934,-464.8 1934,-634.32 1934,-634.32 1934,-640.32 1928,-646.32 1922,-646.32 1922,-646.32 1222,-646.32 1222,-646.32 1216,-646.32 1210,-640.32 1210,-634.32 1210,-634.32 1210,-464.8 1210,-464.8 1210,-458.8 1216,-452.8 1222,-452.8"/>
<text xml:space="preserve" text-anchor="middle" x="1572" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services Layer (/services)</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_state</title>
<path fill="#e0f7fa" stroke="#00838f" d="M2031,-452.8C2031,-452.8 2363,-452.8 2363,-452.8 2369,-452.8 2375,-458.8 2375,-464.8 2375,-464.8 2375,-634.32 2375,-634.32 2375,-640.32 2369,-646.32 2363,-646.32 2363,-646.32 2031,-646.32 2031,-646.32 2025,-646.32 2019,-640.32 2019,-634.32 2019,-634.32 2019,-464.8 2019,-464.8 2019,-458.8 2025,-452.8 2031,-452.8"/>
<text xml:space="preserve" text-anchor="middle" x="2197" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">State Management (/redux, /contexts)</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_backend</title>
<path fill="#eceff1" stroke="#455a64" d="M1382,-344.8C1382,-344.8 1892,-344.8 1892,-344.8 1898,-344.8 1904,-350.8 1904,-356.8 1904,-356.8 1904,-412 1904,-412 1904,-418 1898,-424 1892,-424 1892,-424 1382,-424 1382,-424 1376,-424 1370,-418 1370,-412 1370,-412 1370,-356.8 1370,-356.8 1370,-350.8 1376,-344.8 1382,-344.8"/>
<text xml:space="preserve" text-anchor="middle" x="1637" y="-404.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Django Backend API</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_users</title>
<path fill="#fce4ec" stroke="#ad1457" d="M824,-560.8C824,-560.8 1158,-560.8 1158,-560.8 1164,-560.8 1170,-566.8 1170,-572.8 1170,-572.8 1170,-640.65 1170,-640.65 1170,-646.65 1164,-652.65 1158,-652.65 1158,-652.65 824,-652.65 824,-652.65 818,-652.65 812,-646.65 812,-640.65 812,-640.65 812,-572.8 812,-572.8 812,-566.8 818,-560.8 824,-560.8"/>
<text xml:space="preserve" text-anchor="middle" x="991" y="-633.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">User Types</text>
</g>
<!-- app_router -->
<g id="node1" class="node">
<title>app_router</title>
<path fill="#bbdefb" stroke="black" d="M155.82,-611.12C155.82,-611.12 108.18,-611.12 108.18,-611.12 102.18,-611.12 96.18,-605.12 96.18,-599.12 96.18,-599.12 96.18,-587.12 96.18,-587.12 96.18,-581.12 102.18,-575.12 108.18,-575.12 108.18,-575.12 155.82,-575.12 155.82,-575.12 161.82,-575.12 167.82,-581.12 167.82,-587.12 167.82,-587.12 167.82,-599.12 167.82,-599.12 167.82,-605.12 161.82,-611.12 155.82,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="132" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">App Router</text>
<text xml:space="preserve" text-anchor="middle" x="132" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">/app/*</text>
</g>
<!-- layout -->
<g id="node2" class="node">
<title>layout</title>
<path fill="#bbdefb" stroke="black" d="M118.23,-496.8C118.23,-496.8 27.77,-496.8 27.77,-496.8 21.77,-496.8 15.77,-490.8 15.77,-484.8 15.77,-484.8 15.77,-472.8 15.77,-472.8 15.77,-466.8 21.77,-460.8 27.77,-460.8 27.77,-460.8 118.23,-460.8 118.23,-460.8 124.23,-460.8 130.23,-466.8 130.23,-472.8 130.23,-472.8 130.23,-484.8 130.23,-484.8 130.23,-490.8 124.23,-496.8 118.23,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="73" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Layout Components</text>
<text xml:space="preserve" text-anchor="middle" x="73" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(RootLayout, etc)</text>
</g>
<!-- app_router&#45;&gt;layout -->
<g id="edge1" class="edge">
<title>app_router&#45;&gt;layout</title>
<path fill="none" stroke="#1565c0" d="M113.2,-574.76C113.2,-574.76 113.2,-508.64 113.2,-508.64"/>
<polygon fill="#1565c0" stroke="#1565c0" points="116.7,-508.64 113.2,-498.64 109.7,-508.64 116.7,-508.64"/>
</g>
<!-- middleware -->
<g id="node3" class="node">
<title>middleware</title>
<path fill="#bbdefb" stroke="black" d="M221.84,-496.8C221.84,-496.8 160.16,-496.8 160.16,-496.8 154.16,-496.8 148.16,-490.8 148.16,-484.8 148.16,-484.8 148.16,-472.8 148.16,-472.8 148.16,-466.8 154.16,-460.8 160.16,-460.8 160.16,-460.8 221.84,-460.8 221.84,-460.8 227.84,-460.8 233.84,-466.8 233.84,-472.8 233.84,-472.8 233.84,-484.8 233.84,-484.8 233.84,-490.8 227.84,-496.8 221.84,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="191" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Middleware</text>
<text xml:space="preserve" text-anchor="middle" x="191" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Auth redirect)</text>
</g>
<!-- app_router&#45;&gt;middleware -->
<g id="edge2" class="edge">
<title>app_router&#45;&gt;middleware</title>
<path fill="none" stroke="#1565c0" d="M157.99,-574.76C157.99,-574.76 157.99,-508.64 157.99,-508.64"/>
<polygon fill="#1565c0" stroke="#1565c0" points="161.49,-508.64 157.99,-498.64 154.49,-508.64 161.49,-508.64"/>
</g>
<!-- home -->
<g id="node4" class="node">
<title>home</title>
<path fill="#c8e6c9" stroke="black" d="M442.04,-496.8C442.04,-496.8 391.96,-496.8 391.96,-496.8 385.96,-496.8 379.96,-490.8 379.96,-484.8 379.96,-484.8 379.96,-472.8 379.96,-472.8 379.96,-466.8 385.96,-460.8 391.96,-460.8 391.96,-460.8 442.04,-460.8 442.04,-460.8 448.04,-460.8 454.04,-466.8 454.04,-472.8 454.04,-472.8 454.04,-484.8 454.04,-484.8 454.04,-490.8 448.04,-496.8 442.04,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="417" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Home Page</text>
<text xml:space="preserve" text-anchor="middle" x="417" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">/</text>
</g>
<!-- services_page -->
<g id="node5" class="node">
<title>services_page</title>
<path fill="#c8e6c9" stroke="black" d="M445.57,-388.8C445.57,-388.8 370.43,-388.8 370.43,-388.8 364.43,-388.8 358.43,-382.8 358.43,-376.8 358.43,-376.8 358.43,-364.8 358.43,-364.8 358.43,-358.8 364.43,-352.8 370.43,-352.8 370.43,-352.8 445.57,-352.8 445.57,-352.8 451.57,-352.8 457.57,-358.8 457.57,-364.8 457.57,-364.8 457.57,-376.8 457.57,-376.8 457.57,-382.8 451.57,-388.8 445.57,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="408" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Services Catalog</text>
<text xml:space="preserve" text-anchor="middle" x="408" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/servicios</text>
</g>
<!-- home&#45;&gt;services_page -->
<g id="edge3" class="edge">
<title>home&#45;&gt;services_page</title>
<path fill="none" stroke="#2e7d32" d="M417,-460.48C417,-460.48 417,-400.72 417,-400.72"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="420.5,-400.72 417,-390.72 413.5,-400.72 420.5,-400.72"/>
</g>
<!-- cart_page -->
<g id="node6" class="node">
<title>cart_page</title>
<path fill="#c8e6c9" stroke="black" d="M425,-280.8C425,-280.8 395,-280.8 395,-280.8 389,-280.8 383,-274.8 383,-268.8 383,-268.8 383,-256.8 383,-256.8 383,-250.8 389,-244.8 395,-244.8 395,-244.8 425,-244.8 425,-244.8 431,-244.8 437,-250.8 437,-256.8 437,-256.8 437,-268.8 437,-268.8 437,-274.8 431,-280.8 425,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="410" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Cart</text>
<text xml:space="preserve" text-anchor="middle" x="410" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">/carrito</text>
</g>
<!-- services_page&#45;&gt;cart_page -->
<g id="edge4" class="edge">
<title>services_page&#45;&gt;cart_page</title>
<path fill="none" stroke="#2e7d32" d="M410,-352.48C410,-352.48 410,-292.72 410,-292.72"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="413.5,-292.72 410,-282.72 406.5,-292.72 413.5,-292.72"/>
</g>
<!-- login_page -->
<g id="node7" class="node">
<title>login_page</title>
<path fill="#c8e6c9" stroke="black" d="M444.47,-198C444.47,-198 381.53,-198 381.53,-198 375.53,-198 369.53,-192 369.53,-186 369.53,-186 369.53,-174 369.53,-174 369.53,-168 375.53,-162 381.53,-162 381.53,-162 444.47,-162 444.47,-162 450.47,-162 456.47,-168 456.47,-174 456.47,-174 456.47,-186 456.47,-186 456.47,-192 450.47,-198 444.47,-198"/>
<text xml:space="preserve" text-anchor="middle" x="413" y="-183.3" font-family="Helvetica,sans-Serif" font-size="11.00">Login/Register</text>
<text xml:space="preserve" text-anchor="middle" x="413" y="-170.1" font-family="Helvetica,sans-Serif" font-size="11.00">/login</text>
</g>
<!-- cart_page&#45;&gt;login_page -->
<g id="edge5" class="edge">
<title>cart_page&#45;&gt;login_page</title>
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M410,-244.42C410,-244.42 410,-209.84 410,-209.84"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="413.5,-209.84 410,-199.84 406.5,-209.84 413.5,-209.84"/>
<text xml:space="preserve" text-anchor="middle" x="432.76" y="-218.7" font-family="Helvetica,sans-Serif" font-size="9.00">if not auth</text>
</g>
<!-- profile_page -->
<g id="node8" class="node">
<title>profile_page</title>
<path fill="#c8e6c9" stroke="black" d="M438.73,-125C438.73,-125 389.27,-125 389.27,-125 383.27,-125 377.27,-119 377.27,-113 377.27,-113 377.27,-101 377.27,-101 377.27,-95 383.27,-89 389.27,-89 389.27,-89 438.73,-89 438.73,-89 444.73,-89 450.73,-95 450.73,-101 450.73,-101 450.73,-113 450.73,-113 450.73,-119 444.73,-125 438.73,-125"/>
<text xml:space="preserve" text-anchor="middle" x="414" y="-110.3" font-family="Helvetica,sans-Serif" font-size="11.00">User Profile</text>
<text xml:space="preserve" text-anchor="middle" x="414" y="-97.1" font-family="Helvetica,sans-Serif" font-size="11.00">/perfil</text>
</g>
<!-- login_page&#45;&gt;profile_page -->
<g id="edge6" class="edge">
<title>login_page&#45;&gt;profile_page</title>
<path fill="none" stroke="#2e7d32" d="M414,-161.58C414,-161.58 414,-136.93 414,-136.93"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="417.5,-136.93 414,-126.93 410.5,-136.93 417.5,-136.93"/>
</g>
<!-- pets_page -->
<g id="node9" class="node">
<title>pets_page</title>
<path fill="#c8e6c9" stroke="black" d="M347.06,-52C347.06,-52 304.94,-52 304.94,-52 298.94,-52 292.94,-46 292.94,-40 292.94,-40 292.94,-28 292.94,-28 292.94,-22 298.94,-16 304.94,-16 304.94,-16 347.06,-16 347.06,-16 353.06,-16 359.06,-22 359.06,-28 359.06,-28 359.06,-40 359.06,-40 359.06,-46 353.06,-52 347.06,-52"/>
<text xml:space="preserve" text-anchor="middle" x="326" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">My Pets</text>
<text xml:space="preserve" text-anchor="middle" x="326" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/mascotas</text>
</g>
<!-- profile_page&#45;&gt;pets_page -->
<g id="edge7" class="edge">
<title>profile_page&#45;&gt;pets_page</title>
<path fill="none" stroke="#2e7d32" d="M376.8,-107C352.66,-107 325.68,-107 325.68,-107 325.68,-107 325.68,-63.93 325.68,-63.93"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="329.18,-63.93 325.68,-53.93 322.18,-63.93 329.18,-63.93"/>
</g>
<!-- requests_page -->
<g id="node10" class="node">
<title>requests_page</title>
<path fill="#c8e6c9" stroke="black" d="M445.09,-52C445.09,-52 388.91,-52 388.91,-52 382.91,-52 376.91,-46 376.91,-40 376.91,-40 376.91,-28 376.91,-28 376.91,-22 382.91,-16 388.91,-16 388.91,-16 445.09,-16 445.09,-16 451.09,-16 457.09,-22 457.09,-28 457.09,-28 457.09,-40 457.09,-40 457.09,-46 451.09,-52 445.09,-52"/>
<text xml:space="preserve" text-anchor="middle" x="417" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">My Requests</text>
<text xml:space="preserve" text-anchor="middle" x="417" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/solicitudes</text>
</g>
<!-- profile_page&#45;&gt;requests_page -->
<g id="edge8" class="edge">
<title>profile_page&#45;&gt;requests_page</title>
<path fill="none" stroke="#2e7d32" d="M414,-88.58C414,-88.58 414,-63.93 414,-63.93"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="417.5,-63.93 414,-53.93 410.5,-63.93 417.5,-63.93"/>
</g>
<!-- admin_dash -->
<g id="node11" class="node">
<title>admin_dash</title>
<path fill="#ffe0b2" stroke="black" d="M892.91,-496.8C892.91,-496.8 847.09,-496.8 847.09,-496.8 841.09,-496.8 835.09,-490.8 835.09,-484.8 835.09,-484.8 835.09,-472.8 835.09,-472.8 835.09,-466.8 841.09,-460.8 847.09,-460.8 847.09,-460.8 892.91,-460.8 892.91,-460.8 898.91,-460.8 904.91,-466.8 904.91,-472.8 904.91,-472.8 904.91,-484.8 904.91,-484.8 904.91,-490.8 898.91,-496.8 892.91,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="870" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Dashboard</text>
<text xml:space="preserve" text-anchor="middle" x="870" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin</text>
</g>
<!-- admin_visits -->
<g id="node12" class="node">
<title>admin_visits</title>
<path fill="#ffe0b2" stroke="black" d="M740.77,-388.8C740.77,-388.8 655.23,-388.8 655.23,-388.8 649.23,-388.8 643.23,-382.8 643.23,-376.8 643.23,-376.8 643.23,-364.8 643.23,-364.8 643.23,-358.8 649.23,-352.8 655.23,-352.8 655.23,-352.8 740.77,-352.8 740.77,-352.8 746.77,-352.8 752.77,-358.8 752.77,-364.8 752.77,-364.8 752.77,-376.8 752.77,-376.8 752.77,-382.8 746.77,-388.8 740.77,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="698" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Visits Management</text>
<text xml:space="preserve" text-anchor="middle" x="698" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/visits</text>
</g>
<!-- admin_dash&#45;&gt;admin_visits -->
<g id="edge9" class="edge">
<title>admin_dash&#45;&gt;admin_visits</title>
<path fill="none" stroke="#e65100" d="M834.78,-468C784.46,-468 698,-468 698,-468 698,-468 698,-400.6 698,-400.6"/>
<polygon fill="#e65100" stroke="#e65100" points="701.5,-400.6 698,-390.6 694.5,-400.6 701.5,-400.6"/>
</g>
<!-- admin_pets -->
<g id="node13" class="node">
<title>admin_pets</title>
<path fill="#ffe0b2" stroke="black" d="M845.45,-388.8C845.45,-388.8 782.55,-388.8 782.55,-388.8 776.55,-388.8 770.55,-382.8 770.55,-376.8 770.55,-376.8 770.55,-364.8 770.55,-364.8 770.55,-358.8 776.55,-352.8 782.55,-352.8 782.55,-352.8 845.45,-352.8 845.45,-352.8 851.45,-352.8 857.45,-358.8 857.45,-364.8 857.45,-364.8 857.45,-376.8 857.45,-376.8 857.45,-382.8 851.45,-388.8 845.45,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="814" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Pets Overview</text>
<text xml:space="preserve" text-anchor="middle" x="814" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/pets</text>
</g>
<!-- admin_dash&#45;&gt;admin_pets -->
<g id="edge10" class="edge">
<title>admin_dash&#45;&gt;admin_pets</title>
<path fill="none" stroke="#e65100" d="M846.27,-460.48C846.27,-460.48 846.27,-400.72 846.27,-400.72"/>
<polygon fill="#e65100" stroke="#e65100" points="849.77,-400.72 846.27,-390.72 842.77,-400.72 849.77,-400.72"/>
</g>
<!-- admin_requests -->
<g id="node14" class="node">
<title>admin_requests</title>
<path fill="#ffe0b2" stroke="black" d="M966.72,-388.8C966.72,-388.8 887.28,-388.8 887.28,-388.8 881.28,-388.8 875.28,-382.8 875.28,-376.8 875.28,-376.8 875.28,-364.8 875.28,-364.8 875.28,-358.8 881.28,-352.8 887.28,-352.8 887.28,-352.8 966.72,-352.8 966.72,-352.8 972.72,-352.8 978.72,-358.8 978.72,-364.8 978.72,-364.8 978.72,-376.8 978.72,-376.8 978.72,-382.8 972.72,-388.8 966.72,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="927" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Service Requests</text>
<text xml:space="preserve" text-anchor="middle" x="927" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/solicitudes</text>
</g>
<!-- admin_dash&#45;&gt;admin_requests -->
<g id="edge11" class="edge">
<title>admin_dash&#45;&gt;admin_requests</title>
<path fill="none" stroke="#e65100" d="M890.1,-460.48C890.1,-460.48 890.1,-400.72 890.1,-400.72"/>
<polygon fill="#e65100" stroke="#e65100" points="893.6,-400.72 890.1,-390.72 886.6,-400.72 893.6,-400.72"/>
</g>
<!-- admin_calendar -->
<g id="node15" class="node">
<title>admin_calendar</title>
<path fill="#ffe0b2" stroke="black" d="M1087.41,-388.8C1087.41,-388.8 1008.59,-388.8 1008.59,-388.8 1002.59,-388.8 996.59,-382.8 996.59,-376.8 996.59,-376.8 996.59,-364.8 996.59,-364.8 996.59,-358.8 1002.59,-352.8 1008.59,-352.8 1008.59,-352.8 1087.41,-352.8 1087.41,-352.8 1093.41,-352.8 1099.41,-358.8 1099.41,-364.8 1099.41,-364.8 1099.41,-376.8 1099.41,-376.8 1099.41,-382.8 1093.41,-388.8 1087.41,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1048" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Calendar View</text>
<text xml:space="preserve" text-anchor="middle" x="1048" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/calendario</text>
</g>
<!-- admin_dash&#45;&gt;admin_calendar -->
<g id="edge12" class="edge">
<title>admin_dash&#45;&gt;admin_calendar</title>
<path fill="none" stroke="#e65100" d="M905.19,-470C949.1,-470 1018.5,-470 1018.5,-470 1018.5,-470 1018.5,-400.51 1018.5,-400.51"/>
<polygon fill="#e65100" stroke="#e65100" points="1022,-400.51 1018.5,-390.51 1015,-400.51 1022,-400.51"/>
</g>
<!-- sidebar -->
<g id="node16" class="node">
<title>sidebar</title>
<path fill="#e1bee7" stroke="black" d="M537.99,-280.8C537.99,-280.8 494.01,-280.8 494.01,-280.8 488.01,-280.8 482.01,-274.8 482.01,-268.8 482.01,-268.8 482.01,-256.8 482.01,-256.8 482.01,-250.8 488.01,-244.8 494.01,-244.8 494.01,-244.8 537.99,-244.8 537.99,-244.8 543.99,-244.8 549.99,-250.8 549.99,-256.8 549.99,-256.8 549.99,-268.8 549.99,-268.8 549.99,-274.8 543.99,-280.8 537.99,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="516" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Sidebar</text>
<text xml:space="preserve" text-anchor="middle" x="516" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Navigation</text>
</g>
<!-- admin_dash&#45;&gt;sidebar -->
<g id="edge15" class="edge">
<title>admin_dash&#45;&gt;sidebar</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M834.76,-482C745.22,-482 516,-482 516,-482 516,-482 516,-292.63 516,-292.63"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="519.5,-292.63 516,-282.63 512.5,-292.63 519.5,-292.63"/>
</g>
<!-- navbar -->
<g id="node17" class="node">
<title>navbar</title>
<path fill="#e1bee7" stroke="black" d="M658.1,-280.8C658.1,-280.8 579.9,-280.8 579.9,-280.8 573.9,-280.8 567.9,-274.8 567.9,-268.8 567.9,-268.8 567.9,-256.8 567.9,-256.8 567.9,-250.8 573.9,-244.8 579.9,-244.8 579.9,-244.8 658.1,-244.8 658.1,-244.8 664.1,-244.8 670.1,-250.8 670.1,-256.8 670.1,-256.8 670.1,-268.8 670.1,-268.8 670.1,-274.8 664.1,-280.8 658.1,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="619" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">NavbarBackoffice</text>
<text xml:space="preserve" text-anchor="middle" x="619" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Top Bar</text>
</g>
<!-- admin_dash&#45;&gt;navbar -->
<g id="edge16" class="edge">
<title>admin_dash&#45;&gt;navbar</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M834.61,-475C762.55,-475 605.56,-475 605.56,-475 605.56,-475 605.56,-292.77 605.56,-292.77"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="609.06,-292.77 605.56,-282.77 602.06,-292.77 609.06,-292.77"/>
</g>
<!-- visits_section -->
<g id="node18" class="node">
<title>visits_section</title>
<path fill="#e1bee7" stroke="black" d="M765.53,-280.8C765.53,-280.8 700.47,-280.8 700.47,-280.8 694.47,-280.8 688.47,-274.8 688.47,-268.8 688.47,-268.8 688.47,-256.8 688.47,-256.8 688.47,-250.8 694.47,-244.8 700.47,-244.8 700.47,-244.8 765.53,-244.8 765.53,-244.8 771.53,-244.8 777.53,-250.8 777.53,-256.8 777.53,-256.8 777.53,-268.8 777.53,-268.8 777.53,-274.8 771.53,-280.8 765.53,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="733" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">VisitsSection</text>
<text xml:space="preserve" text-anchor="middle" x="733" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(List + Actions)</text>
</g>
<!-- admin_visits&#45;&gt;visits_section -->
<g id="edge13" class="edge">
<title>admin_visits&#45;&gt;visits_section</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M709.91,-352.48C709.91,-352.48 709.91,-292.72 709.91,-292.72"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="713.41,-292.72 709.91,-282.72 706.41,-292.72 713.41,-292.72"/>
</g>
<!-- drawer -->
<g id="node19" class="node">
<title>drawer</title>
<path fill="#e1bee7" stroke="black" d="M860.86,-280.8C860.86,-280.8 807.14,-280.8 807.14,-280.8 801.14,-280.8 795.14,-274.8 795.14,-268.8 795.14,-268.8 795.14,-256.8 795.14,-256.8 795.14,-250.8 801.14,-244.8 807.14,-244.8 807.14,-244.8 860.86,-244.8 860.86,-244.8 866.86,-244.8 872.86,-250.8 872.86,-256.8 872.86,-256.8 872.86,-268.8 872.86,-268.8 872.86,-274.8 866.86,-280.8 860.86,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="834" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">VisitsDrawer</text>
<text xml:space="preserve" text-anchor="middle" x="834" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Side Panel)</text>
</g>
<!-- admin_visits&#45;&gt;drawer -->
<g id="edge14" class="edge">
<title>admin_visits&#45;&gt;drawer</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M731.34,-352.59C731.34,-336.98 731.34,-317 731.34,-317 731.34,-317 826.29,-317 826.29,-317 826.29,-317 826.29,-292.66 826.29,-292.66"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="829.79,-292.66 826.29,-282.66 822.79,-292.66 829.79,-292.66"/>
</g>
<!-- tables -->
<g id="node20" class="node">
<title>tables</title>
<path fill="#e1bee7" stroke="black" d="M957.18,-280.8C957.18,-280.8 902.82,-280.8 902.82,-280.8 896.82,-280.8 890.82,-274.8 890.82,-268.8 890.82,-268.8 890.82,-256.8 890.82,-256.8 890.82,-250.8 896.82,-244.8 902.82,-244.8 902.82,-244.8 957.18,-244.8 957.18,-244.8 963.18,-244.8 969.18,-250.8 969.18,-256.8 969.18,-256.8 969.18,-268.8 969.18,-268.8 969.18,-274.8 963.18,-280.8 957.18,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="930" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">DataTable</text>
<text xml:space="preserve" text-anchor="middle" x="930" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Components</text>
</g>
<!-- forms -->
<g id="node21" class="node">
<title>forms</title>
<path fill="#e1bee7" stroke="black" d="M1082.54,-280.8C1082.54,-280.8 999.46,-280.8 999.46,-280.8 993.46,-280.8 987.46,-274.8 987.46,-268.8 987.46,-268.8 987.46,-256.8 987.46,-256.8 987.46,-250.8 993.46,-244.8 999.46,-244.8 999.46,-244.8 1082.54,-244.8 1082.54,-244.8 1088.54,-244.8 1094.54,-250.8 1094.54,-256.8 1094.54,-256.8 1094.54,-268.8 1094.54,-268.8 1094.54,-274.8 1088.54,-280.8 1082.54,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="1041" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Form Components</text>
<text xml:space="preserve" text-anchor="middle" x="1041" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Pet, Visit, etc)</text>
</g>
<!-- http_service -->
<g id="node22" class="node">
<title>http_service</title>
<path fill="#ffcdd2" stroke="black" d="M1644.51,-611.12C1644.51,-611.12 1575.49,-611.12 1575.49,-611.12 1569.49,-611.12 1563.49,-605.12 1563.49,-599.12 1563.49,-599.12 1563.49,-587.12 1563.49,-587.12 1563.49,-581.12 1569.49,-575.12 1575.49,-575.12 1575.49,-575.12 1644.51,-575.12 1644.51,-575.12 1650.51,-575.12 1656.51,-581.12 1656.51,-587.12 1656.51,-587.12 1656.51,-599.12 1656.51,-599.12 1656.51,-605.12 1650.51,-611.12 1644.51,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="1610" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">HttpService</text>
<text xml:space="preserve" text-anchor="middle" x="1610" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Axios wrapper)</text>
</g>
<!-- auth_api -->
<g id="node23" class="node">
<title>auth_api</title>
<path fill="#ffcdd2" stroke="black" d="M1914.15,-496.8C1914.15,-496.8 1851.85,-496.8 1851.85,-496.8 1845.85,-496.8 1839.85,-490.8 1839.85,-484.8 1839.85,-484.8 1839.85,-472.8 1839.85,-472.8 1839.85,-466.8 1845.85,-460.8 1851.85,-460.8 1851.85,-460.8 1914.15,-460.8 1914.15,-460.8 1920.15,-460.8 1926.15,-466.8 1926.15,-472.8 1926.15,-472.8 1926.15,-484.8 1926.15,-484.8 1926.15,-490.8 1920.15,-496.8 1914.15,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1883" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">authAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1883" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(login/register)</text>
</g>
<!-- http_service&#45;&gt;auth_api -->
<g id="edge17" class="edge">
<title>http_service&#45;&gt;auth_api</title>
<path fill="none" stroke="#c62828" d="M1656.93,-602C1734.99,-602 1883,-602 1883,-602 1883,-602 1883,-508.76 1883,-508.76"/>
<polygon fill="#c62828" stroke="#c62828" points="1886.5,-508.76 1883,-498.76 1879.5,-508.76 1886.5,-508.76"/>
</g>
<!-- visits_api -->
<g id="node24" class="node">
<title>visits_api</title>
<path fill="#ffcdd2" stroke="black" d="M1810.3,-496.8C1810.3,-496.8 1751.7,-496.8 1751.7,-496.8 1745.7,-496.8 1739.7,-490.8 1739.7,-484.8 1739.7,-484.8 1739.7,-472.8 1739.7,-472.8 1739.7,-466.8 1745.7,-460.8 1751.7,-460.8 1751.7,-460.8 1810.3,-460.8 1810.3,-460.8 1816.3,-460.8 1822.3,-466.8 1822.3,-472.8 1822.3,-472.8 1822.3,-484.8 1822.3,-484.8 1822.3,-490.8 1816.3,-496.8 1810.3,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1781" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">visitsAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1781" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(CRUD visits)</text>
</g>
<!-- http_service&#45;&gt;visits_api -->
<g id="edge18" class="edge">
<title>http_service&#45;&gt;visits_api</title>
<path fill="none" stroke="#c62828" d="M1656.73,-593C1707.36,-593 1781,-593 1781,-593 1781,-593 1781,-508.66 1781,-508.66"/>
<polygon fill="#c62828" stroke="#c62828" points="1784.5,-508.66 1781,-498.66 1777.5,-508.66 1784.5,-508.66"/>
</g>
<!-- orders_api -->
<g id="node25" class="node">
<title>orders_api</title>
<path fill="#ffcdd2" stroke="black" d="M1309.71,-496.8C1309.71,-496.8 1230.29,-496.8 1230.29,-496.8 1224.29,-496.8 1218.29,-490.8 1218.29,-484.8 1218.29,-484.8 1218.29,-472.8 1218.29,-472.8 1218.29,-466.8 1224.29,-460.8 1230.29,-460.8 1230.29,-460.8 1309.71,-460.8 1309.71,-460.8 1315.71,-460.8 1321.71,-466.8 1321.71,-472.8 1321.71,-472.8 1321.71,-484.8 1321.71,-484.8 1321.71,-490.8 1315.71,-496.8 1309.71,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1270" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">OrdersAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1270" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(service requests)</text>
</g>
<!-- http_service&#45;&gt;orders_api -->
<g id="edge19" class="edge">
<title>http_service&#45;&gt;orders_api</title>
<path fill="none" stroke="#c62828" d="M1563.08,-602C1469.81,-602 1270,-602 1270,-602 1270,-602 1270,-508.76 1270,-508.76"/>
<polygon fill="#c62828" stroke="#c62828" points="1273.5,-508.76 1270,-498.76 1266.5,-508.76 1273.5,-508.76"/>
</g>
<!-- petowners_api -->
<g id="node26" class="node">
<title>petowners_api</title>
<path fill="#ffcdd2" stroke="black" d="M1709.46,-496.8C1709.46,-496.8 1646.54,-496.8 1646.54,-496.8 1640.54,-496.8 1634.54,-490.8 1634.54,-484.8 1634.54,-484.8 1634.54,-472.8 1634.54,-472.8 1634.54,-466.8 1640.54,-460.8 1646.54,-460.8 1646.54,-460.8 1709.46,-460.8 1709.46,-460.8 1715.46,-460.8 1721.46,-466.8 1721.46,-472.8 1721.46,-472.8 1721.46,-484.8 1721.46,-484.8 1721.46,-490.8 1715.46,-496.8 1709.46,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">petOwnersAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(clients)</text>
</g>
<!-- http_service&#45;&gt;petowners_api -->
<g id="edge20" class="edge">
<title>http_service&#45;&gt;petowners_api</title>
<path fill="none" stroke="#c62828" d="M1645.53,-574.76C1645.53,-574.76 1645.53,-508.64 1645.53,-508.64"/>
<polygon fill="#c62828" stroke="#c62828" points="1649.03,-508.64 1645.53,-498.64 1642.03,-508.64 1649.03,-508.64"/>
</g>
<!-- vets_api -->
<g id="node27" class="node">
<title>vets_api</title>
<path fill="#ffcdd2" stroke="black" d="M1604.27,-496.8C1604.27,-496.8 1529.73,-496.8 1529.73,-496.8 1523.73,-496.8 1517.73,-490.8 1517.73,-484.8 1517.73,-484.8 1517.73,-472.8 1517.73,-472.8 1517.73,-466.8 1523.73,-460.8 1529.73,-460.8 1529.73,-460.8 1604.27,-460.8 1604.27,-460.8 1610.27,-460.8 1616.27,-466.8 1616.27,-472.8 1616.27,-472.8 1616.27,-484.8 1616.27,-484.8 1616.27,-490.8 1610.27,-496.8 1604.27,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1567" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">VeterinariansAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1567" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(professionals)</text>
</g>
<!-- http_service&#45;&gt;vets_api -->
<g id="edge21" class="edge">
<title>http_service&#45;&gt;vets_api</title>
<path fill="none" stroke="#c62828" d="M1589.88,-574.76C1589.88,-574.76 1589.88,-508.64 1589.88,-508.64"/>
<polygon fill="#c62828" stroke="#c62828" points="1593.38,-508.64 1589.88,-498.64 1586.38,-508.64 1593.38,-508.64"/>
</g>
<!-- services_api -->
<g id="node28" class="node">
<title>services_api</title>
<path fill="#ffcdd2" stroke="black" d="M1488.04,-496.8C1488.04,-496.8 1437.96,-496.8 1437.96,-496.8 1431.96,-496.8 1425.96,-490.8 1425.96,-484.8 1425.96,-484.8 1425.96,-472.8 1425.96,-472.8 1425.96,-466.8 1431.96,-460.8 1437.96,-460.8 1437.96,-460.8 1488.04,-460.8 1488.04,-460.8 1494.04,-460.8 1500.04,-466.8 1500.04,-472.8 1500.04,-472.8 1500.04,-484.8 1500.04,-484.8 1500.04,-490.8 1494.04,-496.8 1488.04,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1463" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">servicesAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1463" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(catalog)</text>
</g>
<!-- http_service&#45;&gt;services_api -->
<g id="edge22" class="edge">
<title>http_service&#45;&gt;services_api</title>
<path fill="none" stroke="#c62828" d="M1563.08,-584C1520.06,-584 1463,-584 1463,-584 1463,-584 1463,-508.64 1463,-508.64"/>
<polygon fill="#c62828" stroke="#c62828" points="1466.5,-508.64 1463,-498.64 1459.5,-508.64 1466.5,-508.64"/>
</g>
<!-- cart_api -->
<g id="node29" class="node">
<title>cart_api</title>
<path fill="#ffcdd2" stroke="black" d="M1395.99,-496.8C1395.99,-496.8 1352.01,-496.8 1352.01,-496.8 1346.01,-496.8 1340.01,-490.8 1340.01,-484.8 1340.01,-484.8 1340.01,-472.8 1340.01,-472.8 1340.01,-466.8 1346.01,-460.8 1352.01,-460.8 1352.01,-460.8 1395.99,-460.8 1395.99,-460.8 1401.99,-460.8 1407.99,-466.8 1407.99,-472.8 1407.99,-472.8 1407.99,-484.8 1407.99,-484.8 1407.99,-490.8 1401.99,-496.8 1395.99,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1374" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">CartAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1374" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(shopping)</text>
</g>
<!-- http_service&#45;&gt;cart_api -->
<g id="edge23" class="edge">
<title>http_service&#45;&gt;cart_api</title>
<path fill="none" stroke="#c62828" d="M1563.09,-593C1494.27,-593 1374,-593 1374,-593 1374,-593 1374,-508.66 1374,-508.66"/>
<polygon fill="#c62828" stroke="#c62828" points="1377.5,-508.66 1374,-498.66 1370.5,-508.66 1377.5,-508.66"/>
</g>
<!-- api_auth -->
<g id="node38" class="node">
<title>api_auth</title>
<path fill="#cfd8dc" stroke="black" d="M1773.81,-388.8C1773.81,-388.8 1726.19,-388.8 1726.19,-388.8 1720.19,-388.8 1714.19,-382.8 1714.19,-376.8 1714.19,-376.8 1714.19,-364.8 1714.19,-364.8 1714.19,-358.8 1720.19,-352.8 1726.19,-352.8 1726.19,-352.8 1773.81,-352.8 1773.81,-352.8 1779.81,-352.8 1785.81,-358.8 1785.81,-364.8 1785.81,-364.8 1785.81,-376.8 1785.81,-376.8 1785.81,-382.8 1779.81,-388.8 1773.81,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1750" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/api/token/</text>
<text xml:space="preserve" text-anchor="middle" x="1750" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(JWT Auth)</text>
</g>
<!-- http_service&#45;&gt;api_auth -->
<g id="edge39" class="edge">
<title>http_service&#45;&gt;api_auth</title>
<path fill="none" stroke="#455a64" stroke-dasharray="5,2" d="M1656.96,-584C1690.82,-584 1730.58,-584 1730.58,-584 1730.58,-584 1730.58,-400.49 1730.58,-400.49"/>
<polygon fill="#455a64" stroke="#455a64" points="1734.08,-400.49 1730.58,-390.49 1727.08,-400.49 1734.08,-400.49"/>
<text xml:space="preserve" text-anchor="middle" x="1986.5" y="-476.1" font-family="Helvetica,sans-Serif" font-size="9.00">JWT refresh</text>
</g>
<!-- auth_api&#45;&gt;api_auth -->
<g id="edge24" class="edge">
<title>auth_api&#45;&gt;api_auth</title>
<path fill="none" stroke="#455a64" d="M1867.88,-460.31C1867.88,-439.04 1867.88,-407 1867.88,-407 1867.88,-407 1770.44,-407 1770.44,-407 1770.44,-407 1770.44,-400.41 1770.44,-400.41"/>
<polygon fill="#455a64" stroke="#455a64" points="1773.94,-400.41 1770.44,-390.41 1766.94,-400.41 1773.94,-400.41"/>
<text xml:space="preserve" text-anchor="middle" x="1909.27" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">POST /api/token/</text>
</g>
<!-- api_mascotas -->
<g id="node35" class="node">
<title>api_mascotas</title>
<path fill="#cfd8dc" stroke="black" d="M1683.93,-388.8C1683.93,-388.8 1602.07,-388.8 1602.07,-388.8 1596.07,-388.8 1590.07,-382.8 1590.07,-376.8 1590.07,-376.8 1590.07,-364.8 1590.07,-364.8 1590.07,-358.8 1596.07,-352.8 1602.07,-352.8 1602.07,-352.8 1683.93,-352.8 1683.93,-352.8 1689.93,-352.8 1695.93,-358.8 1695.93,-364.8 1695.93,-364.8 1695.93,-376.8 1695.93,-376.8 1695.93,-382.8 1689.93,-388.8 1683.93,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1643" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/mascotas/api/v1/</text>
<text xml:space="preserve" text-anchor="middle" x="1643" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Pets, Vets, Visits)</text>
</g>
<!-- visits_api&#45;&gt;api_mascotas -->
<g id="edge25" class="edge">
<title>visits_api&#45;&gt;api_mascotas</title>
<path fill="none" stroke="#455a64" d="M1755.07,-460.59C1755.07,-444.98 1755.07,-425 1755.07,-425 1755.07,-425 1675.46,-425 1675.46,-425 1675.46,-425 1675.46,-400.66 1675.46,-400.66"/>
<polygon fill="#455a64" stroke="#455a64" points="1678.96,-400.66 1675.46,-390.66 1671.96,-400.66 1678.96,-400.66"/>
<text xml:space="preserve" text-anchor="middle" x="1814.25" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /vet&#45;visits/</text>
</g>
<!-- api_solicitudes -->
<g id="node37" class="node">
<title>api_solicitudes</title>
<path fill="#cfd8dc" stroke="black" d="M1439.74,-388.8C1439.74,-388.8 1390.26,-388.8 1390.26,-388.8 1384.26,-388.8 1378.26,-382.8 1378.26,-376.8 1378.26,-376.8 1378.26,-364.8 1378.26,-364.8 1378.26,-358.8 1384.26,-352.8 1390.26,-352.8 1390.26,-352.8 1439.74,-352.8 1439.74,-352.8 1445.74,-352.8 1451.74,-358.8 1451.74,-364.8 1451.74,-364.8 1451.74,-376.8 1451.74,-376.8 1451.74,-382.8 1445.74,-388.8 1439.74,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1415" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/solicitudes/</text>
<text xml:space="preserve" text-anchor="middle" x="1415" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Requests)</text>
</g>
<!-- orders_api&#45;&gt;api_solicitudes -->
<g id="edge26" class="edge">
<title>orders_api&#45;&gt;api_solicitudes</title>
<path fill="none" stroke="#455a64" d="M1270,-460.51C1270,-429.76 1270,-371 1270,-371 1270,-371 1366.52,-371 1366.52,-371"/>
<polygon fill="#455a64" stroke="#455a64" points="1366.52,-374.5 1376.52,-371 1366.52,-367.5 1366.52,-374.5"/>
<text xml:space="preserve" text-anchor="middle" x="1320.76" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /service&#45;requests/</text>
</g>
<!-- petowners_api&#45;&gt;api_mascotas -->
<g id="edge27" class="edge">
<title>petowners_api&#45;&gt;api_mascotas</title>
<path fill="none" stroke="#455a64" d="M1655,-460.48C1655,-460.48 1655,-400.72 1655,-400.72"/>
<polygon fill="#455a64" stroke="#455a64" points="1658.5,-400.72 1655,-390.72 1651.5,-400.72 1658.5,-400.72"/>
<text xml:space="preserve" text-anchor="middle" x="1715.01" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /pet&#45;owners/</text>
</g>
<!-- vets_api&#45;&gt;api_mascotas -->
<g id="edge28" class="edge">
<title>vets_api&#45;&gt;api_mascotas</title>
<path fill="none" stroke="#455a64" d="M1603.17,-460.48C1603.17,-460.48 1603.17,-400.72 1603.17,-400.72"/>
<polygon fill="#455a64" stroke="#455a64" points="1606.67,-400.72 1603.17,-390.72 1599.67,-400.72 1606.67,-400.72"/>
<text xml:space="preserve" text-anchor="middle" x="1609.76" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">GET /veterinarians/</text>
</g>
<!-- api_productos -->
<g id="node36" class="node">
<title>api_productos</title>
<path fill="#cfd8dc" stroke="black" d="M1560.09,-388.8C1560.09,-388.8 1481.91,-388.8 1481.91,-388.8 1475.91,-388.8 1469.91,-382.8 1469.91,-376.8 1469.91,-376.8 1469.91,-364.8 1469.91,-364.8 1469.91,-358.8 1475.91,-352.8 1481.91,-352.8 1481.91,-352.8 1560.09,-352.8 1560.09,-352.8 1566.09,-352.8 1572.09,-358.8 1572.09,-364.8 1572.09,-364.8 1572.09,-376.8 1572.09,-376.8 1572.09,-382.8 1566.09,-388.8 1560.09,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1521" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/productos/</text>
<text xml:space="preserve" text-anchor="middle" x="1521" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Services, Prices)</text>
</g>
<!-- services_api&#45;&gt;api_productos -->
<g id="edge29" class="edge">
<title>services_api&#45;&gt;api_productos</title>
<path fill="none" stroke="#455a64" d="M1489.99,-460.48C1489.99,-460.48 1489.99,-400.72 1489.99,-400.72"/>
<polygon fill="#455a64" stroke="#455a64" points="1493.49,-400.72 1489.99,-390.72 1486.49,-400.72 1493.49,-400.72"/>
<text xml:space="preserve" text-anchor="middle" x="1517.5" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">GET /services/</text>
</g>
<!-- cart_api&#45;&gt;api_productos -->
<g id="edge30" class="edge">
<title>cart_api&#45;&gt;api_productos</title>
<path fill="none" stroke="#455a64" d="M1393.13,-460.48C1393.13,-451.67 1393.13,-443 1393.13,-443 1393.13,-443 1479.95,-443 1479.95,-443 1479.95,-443 1479.95,-400.53 1479.95,-400.53"/>
<polygon fill="#455a64" stroke="#455a64" points="1483.45,-400.53 1479.95,-390.53 1476.45,-400.53 1483.45,-400.53"/>
<text xml:space="preserve" text-anchor="middle" x="1433.25" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /cart/</text>
</g>
<!-- redux_store -->
<g id="node30" class="node">
<title>redux_store</title>
<path fill="#b2ebf2" stroke="black" d="M2228.93,-496.8C2228.93,-496.8 2169.07,-496.8 2169.07,-496.8 2163.07,-496.8 2157.07,-490.8 2157.07,-484.8 2157.07,-484.8 2157.07,-472.8 2157.07,-472.8 2157.07,-466.8 2163.07,-460.8 2169.07,-460.8 2169.07,-460.8 2228.93,-460.8 2228.93,-460.8 2234.93,-460.8 2240.93,-466.8 2240.93,-472.8 2240.93,-472.8 2240.93,-484.8 2240.93,-484.8 2240.93,-490.8 2234.93,-496.8 2228.93,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="2199" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Redux Store</text>
<text xml:space="preserve" text-anchor="middle" x="2199" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Global State)</text>
</g>
<!-- auth_slice -->
<g id="node31" class="node">
<title>auth_slice</title>
<path fill="#b2ebf2" stroke="black" d="M2092.87,-611.12C2092.87,-611.12 2039.13,-611.12 2039.13,-611.12 2033.13,-611.12 2027.13,-605.12 2027.13,-599.12 2027.13,-599.12 2027.13,-587.12 2027.13,-587.12 2027.13,-581.12 2033.13,-575.12 2039.13,-575.12 2039.13,-575.12 2092.87,-575.12 2092.87,-575.12 2098.87,-575.12 2104.87,-581.12 2104.87,-587.12 2104.87,-587.12 2104.87,-599.12 2104.87,-599.12 2104.87,-605.12 2098.87,-611.12 2092.87,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2066" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Auth Slice</text>
<text xml:space="preserve" text-anchor="middle" x="2066" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(user, token)</text>
</g>
<!-- auth_slice&#45;&gt;redux_store -->
<g id="edge31" class="edge">
<title>auth_slice&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" d="M2066,-574.79C2066,-542.53 2066,-479 2066,-479 2066,-479 2145.4,-479 2145.4,-479"/>
<polygon fill="#00838f" stroke="#00838f" points="2145.4,-482.5 2155.4,-479 2145.4,-475.5 2145.4,-482.5"/>
</g>
<!-- visits_slice -->
<g id="node32" class="node">
<title>visits_slice</title>
<path fill="#b2ebf2" stroke="black" d="M2179.59,-611.12C2179.59,-611.12 2134.41,-611.12 2134.41,-611.12 2128.41,-611.12 2122.41,-605.12 2122.41,-599.12 2122.41,-599.12 2122.41,-587.12 2122.41,-587.12 2122.41,-581.12 2128.41,-575.12 2134.41,-575.12 2134.41,-575.12 2179.59,-575.12 2179.59,-575.12 2185.59,-575.12 2191.59,-581.12 2191.59,-587.12 2191.59,-587.12 2191.59,-599.12 2191.59,-599.12 2191.59,-605.12 2185.59,-611.12 2179.59,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2157" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Visits Slice</text>
<text xml:space="preserve" text-anchor="middle" x="2157" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(visit data)</text>
</g>
<!-- visits_slice&#45;&gt;redux_store -->
<g id="edge32" class="edge">
<title>visits_slice&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" d="M2174.33,-574.76C2174.33,-574.76 2174.33,-508.64 2174.33,-508.64"/>
<polygon fill="#00838f" stroke="#00838f" points="2177.83,-508.64 2174.33,-498.64 2170.83,-508.64 2177.83,-508.64"/>
</g>
<!-- cart_slice -->
<g id="node33" class="node">
<title>cart_slice</title>
<path fill="#b2ebf2" stroke="black" d="M2260.84,-611.12C2260.84,-611.12 2221.16,-611.12 2221.16,-611.12 2215.16,-611.12 2209.16,-605.12 2209.16,-599.12 2209.16,-599.12 2209.16,-587.12 2209.16,-587.12 2209.16,-581.12 2215.16,-575.12 2221.16,-575.12 2221.16,-575.12 2260.84,-575.12 2260.84,-575.12 2266.84,-575.12 2272.84,-581.12 2272.84,-587.12 2272.84,-587.12 2272.84,-599.12 2272.84,-599.12 2272.84,-605.12 2266.84,-611.12 2260.84,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2241" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Cart Slice</text>
<text xml:space="preserve" text-anchor="middle" x="2241" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(items)</text>
</g>
<!-- cart_slice&#45;&gt;redux_store -->
<g id="edge33" class="edge">
<title>cart_slice&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" d="M2225.05,-574.76C2225.05,-574.76 2225.05,-508.64 2225.05,-508.64"/>
<polygon fill="#00838f" stroke="#00838f" points="2228.55,-508.64 2225.05,-498.64 2221.55,-508.64 2228.55,-508.64"/>
</g>
<!-- auth_context -->
<g id="node34" class="node">
<title>auth_context</title>
<path fill="#b2ebf2" stroke="black" d="M2355.27,-611.12C2355.27,-611.12 2302.73,-611.12 2302.73,-611.12 2296.73,-611.12 2290.73,-605.12 2290.73,-599.12 2290.73,-599.12 2290.73,-587.12 2290.73,-587.12 2290.73,-581.12 2296.73,-575.12 2302.73,-575.12 2302.73,-575.12 2355.27,-575.12 2355.27,-575.12 2361.27,-575.12 2367.27,-581.12 2367.27,-587.12 2367.27,-587.12 2367.27,-599.12 2367.27,-599.12 2367.27,-605.12 2361.27,-611.12 2355.27,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2329" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">AuthContext</text>
<text xml:space="preserve" text-anchor="middle" x="2329" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Provider)</text>
</g>
<!-- auth_context&#45;&gt;redux_store -->
<g id="edge34" class="edge">
<title>auth_context&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M2329,-574.79C2329,-542.53 2329,-479 2329,-479 2329,-479 2252.72,-479 2252.72,-479"/>
<polygon fill="#00838f" stroke="#00838f" points="2252.72,-475.5 2242.72,-479 2252.72,-482.5 2252.72,-475.5"/>
</g>
<!-- api_payments -->
<g id="node39" class="node">
<title>api_payments</title>
<path fill="#cfd8dc" stroke="black" d="M1883.91,-388.8C1883.91,-388.8 1816.09,-388.8 1816.09,-388.8 1810.09,-388.8 1804.09,-382.8 1804.09,-376.8 1804.09,-376.8 1804.09,-364.8 1804.09,-364.8 1804.09,-358.8 1810.09,-352.8 1816.09,-352.8 1816.09,-352.8 1883.91,-352.8 1883.91,-352.8 1889.91,-352.8 1895.91,-358.8 1895.91,-364.8 1895.91,-364.8 1895.91,-376.8 1895.91,-376.8 1895.91,-382.8 1889.91,-388.8 1883.91,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1850" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/payments/</text>
<text xml:space="preserve" text-anchor="middle" x="1850" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(MercadoPago)</text>
</g>
<!-- petowner_user -->
<g id="node40" class="node">
<title>petowner_user</title>
<ellipse fill="#f8bbd9" stroke="black" cx="866" cy="-593.12" rx="45.9" ry="24.32"/>
<text xml:space="preserve" text-anchor="middle" x="866" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">PetOwner</text>
<text xml:space="preserve" text-anchor="middle" x="866" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Cliente)</text>
</g>
<!-- petowner_user&#45;&gt;home -->
<g id="edge35" class="edge">
<title>petowner_user&#45;&gt;home</title>
<path fill="none" stroke="#ad1457" d="M827.6,-579.22C827.6,-551.07 827.6,-490 827.6,-490 827.6,-490 466.04,-490 466.04,-490"/>
<polygon fill="#ad1457" stroke="#ad1457" points="466.04,-486.5 456.04,-490 466.04,-493.5 466.04,-486.5"/>
<text xml:space="preserve" text-anchor="middle" x="830.01" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">public access</text>
</g>
<!-- petowner_user&#45;&gt;profile_page -->
<g id="edge36" class="edge">
<title>petowner_user&#45;&gt;profile_page</title>
<path fill="none" stroke="#ad1457" stroke-dasharray="5,2" d="M819.7,-593C733.92,-593 558.94,-593 558.94,-593 558.94,-593 558.94,-107 558.94,-107 558.94,-107 462.46,-107 462.46,-107"/>
<polygon fill="#ad1457" stroke="#ad1457" points="462.46,-103.5 452.46,-107 462.46,-110.5 462.46,-103.5"/>
<text xml:space="preserve" text-anchor="middle" x="1154.02" y="-326.7" font-family="Helvetica,sans-Serif" font-size="9.00">authenticated</text>
</g>
<!-- vet_user -->
<g id="node41" class="node">
<title>vet_user</title>
<ellipse fill="#f8bbd9" stroke="black" cx="985" cy="-593.12" rx="55.41" ry="24.32"/>
<text xml:space="preserve" text-anchor="middle" x="985" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Veterinarian</text>
<text xml:space="preserve" text-anchor="middle" x="985" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Profesional)</text>
</g>
<!-- vet_user&#45;&gt;admin_dash -->
<g id="edge37" class="edge">
<title>vet_user&#45;&gt;admin_dash</title>
<path fill="none" stroke="#ad1457" d="M954.15,-572.61C954.15,-542.01 954.15,-488 954.15,-488 954.15,-488 916.71,-488 916.71,-488"/>
<polygon fill="#ad1457" stroke="#ad1457" points="916.71,-484.5 906.71,-488 916.71,-491.5 916.71,-484.5"/>
<text xml:space="preserve" text-anchor="middle" x="921.26" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">backoffice</text>
</g>
<!-- admin_user -->
<g id="node42" class="node">
<title>admin_user</title>
<ellipse fill="#f8bbd9" stroke="black" cx="1110" cy="-593.12" rx="51.52" ry="24.32"/>
<text xml:space="preserve" text-anchor="middle" x="1110" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Admin/Staff</text>
<text xml:space="preserve" text-anchor="middle" x="1110" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Interno)</text>
</g>
<!-- admin_user&#45;&gt;admin_dash -->
<g id="edge38" class="edge">
<title>admin_user&#45;&gt;admin_dash</title>
<path fill="none" stroke="#ad1457" d="M1078.95,-573.25C1078.95,-540.55 1078.95,-479 1078.95,-479 1078.95,-479 916.49,-479 916.49,-479"/>
<polygon fill="#ad1457" stroke="#ad1457" points="916.49,-475.5 906.49,-479 916.49,-482.5 916.49,-475.5"/>
<text xml:space="preserve" text-anchor="middle" x="1181.01" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">full access</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,234 @@
digraph DataModel {
// Graph settings
rankdir=TB
compound=true
splines=ortho
node [shape=record, style="filled", fontname="Helvetica", fontsize=10]
edge [fontname="Helvetica", fontsize=8]
label="AMAR Mascotas - Data Model (Entity Relationships)\n\n"
labelloc="t"
fontsize=16
fontname="Helvetica-Bold"
// === USERS & AUTHENTICATION ===
subgraph cluster_auth {
label="Users & Authentication"
style="rounded,filled"
fillcolor="#E8F5E9"
color="#2E7D32"
auth_user [label="{auth.User|id: PK\luser name: str\lemail: str\lis_staff: bool\lis_superuser: bool\l}", fillcolor="#C8E6C9"]
}
// === PET OWNERS & PETS ===
subgraph cluster_mascotas_owners {
label="Pet Owners & Pets"
style="rounded,filled"
fillcolor="#E3F2FD"
color="#1565C0"
petowner [label="{PetOwner|id: PK\lemail: str (unique)\lphone: str\lfirst_name: str\llast_name: str\lneighborhood_id: FK\lcampaign_id: FK (opt)\lgeo_latitude: float\lgeo_longitude: float\laddress: str\l}", fillcolor="#BBDEFB"]
pet [label="{Pet|id: PK\lowner_id: FK\lname: str\lpet_type: DOG/CAT\lbreed_id: FK (opt)\lgender: M/F\lage: int\lweight: decimal\lheight: decimal\lbirth_date: date\lallergies: text\lneutered: bool\lis_deceased: bool\lstate: puppy/adult/...\lprofile_picture: file\l}", fillcolor="#BBDEFB"]
petvaccine [label="{PetVaccine|id: PK\lpet_id: FK\lvaccine_id: FK\lapplication_date: date\lnext_application: date\l}", fillcolor="#90CAF9"]
petstudy [label="{PetStudy|id: PK\lpet_id: FK\lstudy_id: FK\ldate: date\lresult: text\limages: files\l}", fillcolor="#90CAF9"]
}
// === VETERINARIANS ===
subgraph cluster_veterinarians {
label="Veterinarians"
style="rounded,filled"
fillcolor="#FFF3E0"
color="#E65100"
veterinarian [label="{Veterinarian|id: PK\luser_id: FK\llicense: str\lemail: str\lphone: str\l}", fillcolor="#FFE0B2"]
availability [label="{Availability|id: PK\lveterinarian_id: FK\lstart_day: 0-6\lend_day: 0-6\lstart_time: time\lend_time: time\l}", fillcolor="#FFCC80"]
unavailability [label="{Unavailability|id: PK\lveterinarian_id: FK\lstart_date: date\lend_date: date\lstart_time: time\lend_time: time\lreason: str\l}", fillcolor="#FFCC80"]
vet_specialty [label="{M2M: Vet-Specialty|veterinarian_id: FK\lspecialty_id: FK\l}", shape=diamond, fillcolor="#FFB74D"]
vet_neighborhood [label="{M2M: Vet-Neighborhood|veterinarian_id: FK\lneighborhood_id: FK\l}", shape=diamond, fillcolor="#FFB74D"]
}
// === SERVICES & PRICING ===
subgraph cluster_productos {
label="Services & Pricing"
style="rounded,filled"
fillcolor="#F3E5F5"
color="#7B1FA2"
grupo [label="{Group|id: PK\lname: str\ldescription: text\l}", fillcolor="#E1BEE7"]
category [label="{Category|id: PK\lgroup_id: FK\lname: str\ldescription: text\lvalue: int (order)\l}", fillcolor="#E1BEE7"]
service [label="{Service|id: PK\lname: str\ldescription: text\lspecialty_id: FK\lcategory_id: FK\lduration: int (min)\lmodality: onsite/online\lpayment_sign_req: bool\lpet_type_filter: str\lage_filter: str\lweight_range: str\l}", fillcolor="#CE93D8"]
prices [label="{Prices|id: PK\lservice_id: FK\lveterinarian_id: FK (opt)\lprice: decimal\lprofessional_fee: decimal\lpayment_sign: decimal\lfrom_date: date\lto_date: date\lactive: bool\l}", fillcolor="#BA68C8"]
discounts [label="{Discounts|id: PK\lservice_id: FK\ldiscount: decimal (%)\lfrom_date: date\lto_date: date\lactive: bool\l}", fillcolor="#BA68C8"]
servicecombo [label="{ServiceCombo|id: PK\lname: str\ldescription: text\ldiscount_percent: decimal\ldiscount_fixed: decimal\l}", fillcolor="#E1BEE7"]
}
// === CART & CHECKOUT ===
subgraph cluster_cart {
label="Cart & Checkout"
style="rounded,filled"
fillcolor="#E0F7FA"
color="#00838F"
cart [label="{Cart|id: PK\lpetowner_id: FK\lveterinarian_id: FK (opt)\luse_vet_prices: bool\lapply_turn_fee: bool\l}", fillcolor="#B2EBF2"]
cartitem [label="{CartItem|id: PK\lcart_id: FK\lpet_id: FK (opt)\lservice_id: FK\lprice: decimal\lquantity: int\ltotal: decimal (calc)\l}", fillcolor="#80DEEA"]
cartresumeitem [label="{CartResumeItem|id: PK\lcart_id: FK\lconcept: SUBTOTAL/\l DESCUENTO/\l ADELANTO/\l TOTAL/\l COSTO_SERVICIO\lamount: decimal\lorder: int\l}", fillcolor="#80DEEA"]
cartpetreason [label="{CartPetReason|id: PK\lcart_id: FK\lpet_id: FK\lreason: text\l}", fillcolor="#80DEEA"]
}
// === SERVICE REQUESTS ===
subgraph cluster_solicitudes {
label="Service Requests (Workflow)"
style="rounded,filled"
fillcolor="#FFEBEE"
color="#C62828"
servicerequest [label="{ServiceRequest|id: PK\lpetowner_id: FK\lcart_id: FK\lveterinarian_id: FK (opt)\lstate: pending/vet_asked/\l vet_accepted/coordinated/\l payed/Confirmado/...\lreason: text\ldays_requested: JSON\ldate_coordinated: datetime\lhour_coordinated: time\lpay_number: str\lcampaign_id: FK (opt)\lattended_by_id: FK (opt)\l}", fillcolor="#FFCDD2"]
statehistory [label="{StateHistory|id: PK\lservice_request_id: FK\lstate: str\ladditional_data: JSON\lcreated_at: datetime\luser_id: FK\l}", fillcolor="#EF9A9A"]
vetasked [label="{VeterinarianAsked|id: PK\lservice_request_id: FK\lveterinarian_id: FK\ldate_asked: datetime\ldate_answered: datetime\laccepted: bool\l}", fillcolor="#EF9A9A"]
vetreminder [label="{ScheduledVetReminder|id: PK\lvet_asked_id: FK\lscheduled_for: datetime\lprocessed_at: datetime\lstatus: pending/sent/...\lcelery_task_id: str\l}", fillcolor="#E57373"]
payreminder [label="{PaymentReminder|id: PK\lservice_request_id: FK\lscheduled_for: datetime\lprocessed_at: datetime\lstatus: pending/sent/...\l}", fillcolor="#E57373"]
}
// === VET VISITS ===
subgraph cluster_vetvisits {
label="Veterinary Visits"
style="rounded,filled"
fillcolor="#FFFDE7"
color="#F9A825"
vetvisit [label="{VetVisit|id: PK\lservice_request_id: FK (opt)\lowner_id: FK\lveterinarian_id: FK\ldate: date\lhour: time\lvisit_type: clinical/\l vaccination/\l telemedicina\lvisit_state: PENDING/\l IN_PROGRESS/\l COMPLETED/\l NO_REPORT/\l CANCELLED\lreason: text\lobservations: text\lprice: decimal\ldeposit: decimal\lvet_fee: decimal\lpay_transaction: str\lgoogle_event_id: str\lafip_receipt_id: FK (opt)\l}", fillcolor="#FFF9C4"]
vetvisitreport [label="{VetVisitReport|id: PK\lvisit_id: FK\lpet_id: FK\lreason: text\lphysical_exam: text\ldiagnosis: text\ltreatment: text\lpdf_file: file\l}", fillcolor="#FFF59D"]
vetvisitfollowup [label="{VetVisitFollowUp|id: PK\lreport_id: FK\ldate: date\ldescription: text\l}", fillcolor="#FFF176"]
vetvisitpetreason [label="{VetVisitPetReason|id: PK\lvisit_id: FK\lpet_id: FK\lreason: text\l}", fillcolor="#FFF176"]
visit_pets [label="{M2M: Visit-Pets|vetvisit_id: FK\lpet_id: FK\l}", shape=diamond, fillcolor="#FFEE58"]
}
// === REFERENCE DATA ===
subgraph cluster_reference {
label="Reference Data"
style="rounded,filled"
fillcolor="#ECEFF1"
color="#455A64"
specialty [label="{Specialty|id: PK\lname: str\l}", fillcolor="#CFD8DC"]
neighborhood [label="{Neighborhood|id: PK\lname: str\ldistance_coefficient: decimal\lcoverage_area: GIS Polygon\l}", fillcolor="#CFD8DC"]
province [label="{Province|id: PK\lname: str\l}", fillcolor="#CFD8DC"]
locality [label="{Locality|id: PK\lprovince_id: FK\lname: str\l}", fillcolor="#CFD8DC"]
petbreed [label="{PetBreed|id: PK\lname: str\lpet_type: DOG/CAT\l}", fillcolor="#CFD8DC"]
vaccine [label="{Vaccine|id: PK\lname: str\lpet_type: DOG/CAT\lperiodicity: int (months)\l}", fillcolor="#CFD8DC"]
study [label="{Study|id: PK\lname: str\lpet_type: DOG/CAT\lgroup_id: FK (opt)\l}", fillcolor="#CFD8DC"]
campaign [label="{Campaign|id: PK\lname: str\lutm_source: str\lutm_medium: str\lis_active: bool\l}", fillcolor="#CFD8DC"]
tag [label="{Tag|id: PK\lname: str (unique)\l}", fillcolor="#CFD8DC"]
medication [label="{Medication|id: PK\lname: str\lgeneral_name_id: FK\ltype_id: FK\lpresentation_id: FK\lpet_type: str\l}", fillcolor="#CFD8DC"]
turnfeegroup [label="{IndividualTurnFeeGroup|id: PK\lname: str\lfee_percentage: decimal\l}", fillcolor="#CFD8DC"]
}
// === AFIP/INVOICING ===
subgraph cluster_afip {
label="AFIP Invoicing"
style="rounded,filled"
fillcolor="#FCE4EC"
color="#AD1457"
receipt [label="{Receipt|id: PK\ldocument_number: bigint\lreceipt_number: int\lissued_date: date\ltotal_amount: decimal\lnet_taxed: decimal\lcae: str\lcae_expiration: date\l}", fillcolor="#F8BBD9"]
}
// === RELATIONSHIPS ===
// Auth -> PetOwner/Vet
auth_user -> petowner [label="1:1 opt", style=dashed, color="#2E7D32"]
auth_user -> veterinarian [label="1:1", color="#2E7D32"]
// PetOwner relationships
petowner -> pet [label="1:N owns", color="#1565C0"]
petowner -> neighborhood [label="N:1 lives in", color="#1565C0"]
petowner -> campaign [label="N:1 opt", style=dashed, color="#666"]
petowner -> cart [label="1:N", color="#00838F"]
petowner -> servicerequest [label="1:N requests", color="#C62828"]
petowner -> vetvisit [label="1:N as owner", color="#F9A825"]
// Pet relationships
pet -> petbreed [label="N:1 opt", style=dashed, color="#666"]
pet -> petvaccine [label="1:N", color="#1565C0"]
pet -> petstudy [label="1:N", color="#1565C0"]
petvaccine -> vaccine [label="N:1", color="#666"]
petstudy -> study [label="N:1", color="#666"]
// Veterinarian relationships
veterinarian -> vet_specialty [label="1:N", color="#E65100"]
vet_specialty -> specialty [label="N:1", color="#E65100"]
veterinarian -> vet_neighborhood [label="1:N coverage", color="#E65100"]
vet_neighborhood -> neighborhood [label="N:1", color="#E65100"]
veterinarian -> availability [label="1:N", color="#E65100"]
veterinarian -> unavailability [label="1:N", color="#E65100"]
veterinarian -> turnfeegroup [label="N:M", color="#E65100", style=dashed]
// Service/Pricing relationships
grupo -> category [label="1:N", color="#7B1FA2"]
category -> service [label="1:N", color="#7B1FA2"]
service -> specialty [label="N:1 opt", style=dashed, color="#7B1FA2"]
service -> prices [label="1:N", color="#7B1FA2"]
service -> discounts [label="1:N", color="#7B1FA2"]
prices -> veterinarian [label="N:1 opt\n(vet-specific)", style=dashed, color="#7B1FA2"]
// Cart relationships
cart -> veterinarian [label="N:1 opt\n(assigned vet)", style=dashed, color="#00838F"]
cart -> cartitem [label="1:N", color="#00838F"]
cart -> cartresumeitem [label="1:N", color="#00838F"]
cart -> cartpetreason [label="1:N", color="#00838F"]
cartitem -> service [label="N:1", color="#00838F"]
cartitem -> pet [label="N:1 opt", style=dashed, color="#00838F"]
cartpetreason -> pet [label="N:1", color="#00838F"]
// ServiceRequest relationships
servicerequest -> cart [label="1:1", color="#C62828"]
servicerequest -> veterinarian [label="N:1 opt\n(assigned)", style=dashed, color="#C62828"]
servicerequest -> statehistory [label="1:N audit", color="#C62828"]
servicerequest -> vetasked [label="1:N", color="#C62828"]
servicerequest -> payreminder [label="1:N", color="#C62828"]
servicerequest -> campaign [label="N:1 opt", style=dashed, color="#666"]
servicerequest -> tag [label="N:M", color="#666"]
vetasked -> veterinarian [label="N:1", color="#C62828"]
vetasked -> vetreminder [label="1:N", color="#C62828"]
// VetVisit relationships
vetvisit -> servicerequest [label="1:1 opt\n(from request)", style=dashed, color="#F9A825"]
vetvisit -> veterinarian [label="N:1", color="#F9A825"]
vetvisit -> visit_pets [label="1:N", color="#F9A825"]
visit_pets -> pet [label="N:1", color="#F9A825"]
vetvisit -> vetvisitreport [label="1:N", color="#F9A825"]
vetvisit -> vetvisitpetreason [label="1:N", color="#F9A825"]
vetvisitreport -> pet [label="N:1", color="#F9A825"]
vetvisitreport -> vetvisitfollowup [label="1:N", color="#F9A825"]
vetvisitpetreason -> pet [label="N:1", color="#F9A825"]
// AFIP
vetvisit -> receipt [label="1:1 opt\n(invoice)", style=dashed, color="#AD1457"]
// Geography
province -> locality [label="1:N", color="#666"]
}

View File

@@ -0,0 +1,976 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: DataModel Pages: 1 -->
<svg width="2547pt" height="1691pt"
viewBox="0.00 0.00 2547.00 1691.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1687.2)">
<title>DataModel</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1687.2 2543,-1687.2 2543,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="1269.5" y="-1664" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Data Model (Entity Relationships)</text>
<g id="clust1" class="cluster">
<title>cluster_auth</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M550,-1367.6C550,-1367.6 703,-1367.6 703,-1367.6 709,-1367.6 715,-1373.6 715,-1379.6 715,-1379.6 715,-1515 715,-1515 715,-1521 709,-1527 703,-1527 703,-1527 550,-1527 550,-1527 544,-1527 538,-1521 538,-1515 538,-1515 538,-1379.6 538,-1379.6 538,-1373.6 544,-1367.6 550,-1367.6"/>
<text xml:space="preserve" text-anchor="middle" x="626.5" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Users &amp; Authentication</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_mascotas_owners</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M482,-494.2C482,-494.2 678,-494.2 678,-494.2 684,-494.2 690,-500.2 690,-506.2 690,-506.2 690,-1192.4 690,-1192.4 690,-1198.4 684,-1204.4 678,-1204.4 678,-1204.4 482,-1204.4 482,-1204.4 476,-1204.4 470,-1198.4 470,-1192.4 470,-1192.4 470,-506.2 470,-506.2 470,-500.2 476,-494.2 482,-494.2"/>
<text xml:space="preserve" text-anchor="middle" x="580" y="-1185.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Pet Owners &amp; Pets</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_veterinarians</title>
<path fill="#fff3e0" stroke="#e65100" d="M806,-317.6C806,-317.6 1867,-317.6 1867,-317.6 1873,-317.6 1879,-323.6 1879,-329.6 1879,-329.6 1879,-647.6 1879,-647.6 1879,-653.6 1873,-659.6 1867,-659.6 1867,-659.6 806,-659.6 806,-659.6 800,-659.6 794,-653.6 794,-647.6 794,-647.6 794,-329.6 794,-329.6 794,-323.6 800,-317.6 806,-317.6"/>
<text xml:space="preserve" text-anchor="middle" x="1336.5" y="-640.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinarians</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_productos</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M67,-476.2C67,-476.2 318,-476.2 318,-476.2 324,-476.2 330,-482.2 330,-488.2 330,-488.2 330,-1515 330,-1515 330,-1521 324,-1527 318,-1527 318,-1527 67,-1527 67,-1527 61,-1527 55,-1521 55,-1515 55,-1515 55,-488.2 55,-488.2 55,-482.2 61,-476.2 67,-476.2"/>
<text xml:space="preserve" text-anchor="middle" x="192.5" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services &amp; Pricing</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_cart</title>
<path fill="#e0f7fa" stroke="#00838f" d="M740,-991C740,-991 1066,-991 1066,-991 1072,-991 1078,-997 1078,-1003 1078,-1003 1078,-1515 1078,-1515 1078,-1521 1072,-1527 1066,-1527 1066,-1527 740,-1527 740,-1527 734,-1527 728,-1521 728,-1515 728,-1515 728,-1003 728,-1003 728,-997 734,-991 740,-991"/>
<text xml:space="preserve" text-anchor="middle" x="903" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Cart &amp; Checkout</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_solicitudes</title>
<path fill="#ffebee" stroke="#c62828" d="M1899,-494.2C1899,-494.2 2297,-494.2 2297,-494.2 2303,-494.2 2309,-500.2 2309,-506.2 2309,-506.2 2309,-1216.4 2309,-1216.4 2309,-1222.4 2303,-1228.4 2297,-1228.4 2297,-1228.4 1899,-1228.4 1899,-1228.4 1893,-1228.4 1887,-1222.4 1887,-1216.4 1887,-1216.4 1887,-506.2 1887,-506.2 1887,-500.2 1893,-494.2 1899,-494.2"/>
<text xml:space="preserve" text-anchor="middle" x="2098" y="-1209.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Service Requests (Workflow)</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_vetvisits</title>
<path fill="#fffde7" stroke="#f9a825" d="M1186,-762.8C1186,-762.8 1726,-762.8 1726,-762.8 1732,-762.8 1738,-768.8 1738,-774.8 1738,-774.8 1738,-1617 1738,-1617 1738,-1623 1732,-1629 1726,-1629 1726,-1629 1186,-1629 1186,-1629 1180,-1629 1174,-1623 1174,-1617 1174,-1617 1174,-774.8 1174,-774.8 1174,-768.8 1180,-762.8 1186,-762.8"/>
<text xml:space="preserve" text-anchor="middle" x="1456" y="-1609.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinary Visits</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_reference</title>
<path fill="#eceff1" stroke="#455a64" d="M20,-8C20,-8 1189,-8 1189,-8 1195,-8 1201,-14 1201,-20 1201,-20 1201,-278 1201,-278 1201,-284 1195,-290 1189,-290 1189,-290 20,-290 20,-290 14,-290 8,-284 8,-278 8,-278 8,-20 8,-20 8,-14 14,-8 20,-8"/>
<text xml:space="preserve" text-anchor="middle" x="604.5" y="-270.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reference Data</text>
</g>
<g id="clust9" class="cluster">
<title>cluster_afip</title>
<path fill="#fce4ec" stroke="#ad1457" d="M2399,-997C2399,-997 2519,-997 2519,-997 2525,-997 2531,-1003 2531,-1009 2531,-1009 2531,-1180.4 2531,-1180.4 2531,-1186.4 2525,-1192.4 2519,-1192.4 2519,-1192.4 2399,-1192.4 2399,-1192.4 2393,-1192.4 2387,-1186.4 2387,-1180.4 2387,-1180.4 2387,-1009 2387,-1009 2387,-1003 2393,-997 2399,-997"/>
<text xml:space="preserve" text-anchor="middle" x="2459" y="-1173.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AFIP Invoicing</text>
</g>
<!-- auth_user -->
<g id="node1" class="node">
<title>auth_user</title>
<polygon fill="#c8e6c9" stroke="black" points="604.14,-1376.1 604.14,-1464.1 701.86,-1464.1 701.86,-1376.1 604.14,-1376.1"/>
<text xml:space="preserve" text-anchor="middle" x="653" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">auth.User</text>
<polyline fill="none" stroke="black" points="604.14,-1444.1 701.86,-1444.1"/>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">user name: str</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">email: str</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_staff: bool</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_superuser: bool</text>
</g>
<!-- petowner -->
<g id="node2" class="node">
<title>petowner</title>
<polygon fill="#bbdefb" stroke="black" points="565.81,-993.5 565.81,-1141.5 680.19,-1141.5 680.19,-993.5 565.81,-993.5"/>
<text xml:space="preserve" text-anchor="middle" x="623" y="-1128.5" font-family="Helvetica,sans-Serif" font-size="10.00">PetOwner</text>
<polyline fill="none" stroke="black" points="565.81,-1121.5 680.19,-1121.5"/>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1108.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">email: str (unique)</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">phone: str</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">first_name: str</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">last_name: str</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">neighborhood_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">campaign_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">geo_latitude: float</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">geo_longitude: float</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1000.5" font-family="Helvetica,sans-Serif" font-size="10.00">address: str</text>
</g>
<!-- auth_user&#45;&gt;petowner -->
<g id="edge1" class="edge">
<title>auth_user&#45;&gt;petowner</title>
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M642.17,-1375.8C642.17,-1375.8 642.17,-1153.26 642.17,-1153.26"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="645.67,-1153.26 642.17,-1143.26 638.67,-1153.26 645.67,-1153.26"/>
<text xml:space="preserve" text-anchor="middle" x="645.23" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
</g>
<!-- veterinarian -->
<g id="node6" class="node">
<title>veterinarian</title>
<polygon fill="#ffe0b2" stroke="black" points="1538.04,-508.7 1538.04,-596.7 1607.96,-596.7 1607.96,-508.7 1538.04,-508.7"/>
<text xml:space="preserve" text-anchor="middle" x="1573" y="-583.7" font-family="Helvetica,sans-Serif" font-size="10.00">Veterinarian</text>
<polyline fill="none" stroke="black" points="1538.04,-576.7 1607.96,-576.7"/>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">user_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">license: str</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">email: str</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">phone: str</text>
</g>
<!-- auth_user&#45;&gt;veterinarian -->
<g id="edge2" class="edge">
<title>auth_user&#45;&gt;veterinarian</title>
<path fill="none" stroke="#2e7d32" d="M695.21,-1375.77C695.21,-1200.06 695.21,-562 695.21,-562 695.21,-562 1526.26,-562 1526.26,-562"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1526.26,-565.5 1536.26,-562 1526.26,-558.5 1526.26,-565.5"/>
<text xml:space="preserve" text-anchor="middle" x="787.56" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- pet -->
<g id="node3" class="node">
<title>pet</title>
<polygon fill="#bbdefb" stroke="black" points="574.97,-705.3 574.97,-913.3 681.03,-913.3 681.03,-705.3 574.97,-705.3"/>
<text xml:space="preserve" text-anchor="middle" x="628" y="-900.3" font-family="Helvetica,sans-Serif" font-size="10.00">Pet</text>
<polyline fill="none" stroke="black" points="574.97,-893.3 681.03,-893.3"/>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-880.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-868.3" font-family="Helvetica,sans-Serif" font-size="10.00">owner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-856.3" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-844.3" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-832.3" font-family="Helvetica,sans-Serif" font-size="10.00">breed_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">gender: M/F</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">age: int</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">weight: decimal</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">height: decimal</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">birth_date: date</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-760.3" font-family="Helvetica,sans-Serif" font-size="10.00">allergies: text</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-748.3" font-family="Helvetica,sans-Serif" font-size="10.00">neutered: bool</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-736.3" font-family="Helvetica,sans-Serif" font-size="10.00">is_deceased: bool</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-724.3" font-family="Helvetica,sans-Serif" font-size="10.00">state: puppy/adult/...</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-712.3" font-family="Helvetica,sans-Serif" font-size="10.00">profile_picture: file</text>
</g>
<!-- petowner&#45;&gt;pet -->
<g id="edge3" class="edge">
<title>petowner&#45;&gt;pet</title>
<path fill="none" stroke="#1565c0" d="M627.58,-993.16C627.58,-993.16 627.58,-924.87 627.58,-924.87"/>
<polygon fill="#1565c0" stroke="#1565c0" points="631.08,-924.87 627.58,-914.87 624.08,-924.87 631.08,-924.87"/>
<text xml:space="preserve" text-anchor="middle" x="588.67" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N owns</text>
</g>
<!-- cart -->
<g id="node17" class="node">
<title>cart</title>
<polygon fill="#b2ebf2" stroke="black" points="906.36,-1376.1 906.36,-1464.1 1029.64,-1464.1 1029.64,-1376.1 906.36,-1376.1"/>
<text xml:space="preserve" text-anchor="middle" x="968" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">Cart</text>
<polyline fill="none" stroke="black" points="906.36,-1444.1 1029.64,-1444.1"/>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">petowner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">use_vet_prices: bool</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">apply_turn_fee: bool</text>
</g>
<!-- petowner&#45;&gt;cart -->
<g id="edge6" class="edge">
<title>petowner&#45;&gt;cart</title>
<path fill="none" stroke="#00838f" d="M680.65,-1130C765.7,-1130 914.82,-1130 914.82,-1130 914.82,-1130 914.82,-1364.21 914.82,-1364.21"/>
<polygon fill="#00838f" stroke="#00838f" points="911.32,-1364.21 914.82,-1374.21 918.32,-1364.21 911.32,-1364.21"/>
<text xml:space="preserve" text-anchor="middle" x="790.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- servicerequest -->
<g id="node21" class="node">
<title>servicerequest</title>
<polygon fill="#ffcdd2" stroke="black" points="1895.4,-969.5 1895.4,-1165.5 2032.6,-1165.5 2032.6,-969.5 1895.4,-969.5"/>
<text xml:space="preserve" text-anchor="middle" x="1964" y="-1152.5" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceRequest</text>
<polyline fill="none" stroke="black" points="1895.4,-1145.5 2032.6,-1145.5"/>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1132.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1120.5" font-family="Helvetica,sans-Serif" font-size="10.00">petowner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1108.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">state: pending/vet_asked/</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00"> vet_accepted/coordinated/</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00"> payed/Confirmado/...</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">days_requested: JSON</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">date_coordinated: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">hour_coordinated: time</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1000.5" font-family="Helvetica,sans-Serif" font-size="10.00">pay_number: str</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-988.5" font-family="Helvetica,sans-Serif" font-size="10.00">campaign_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-976.5" font-family="Helvetica,sans-Serif" font-size="10.00">attended_by_id: FK (opt)</text>
</g>
<!-- petowner&#45;&gt;servicerequest -->
<g id="edge7" class="edge">
<title>petowner&#45;&gt;servicerequest</title>
<path fill="none" stroke="#c62828" d="M623.15,-1141.95C623.15,-1145.73 623.15,-1148 623.15,-1148 623.15,-1148 1883.69,-1148 1883.69,-1148"/>
<polygon fill="#c62828" stroke="#c62828" points="1883.69,-1151.5 1893.69,-1148 1883.69,-1144.5 1883.69,-1151.5"/>
<text xml:space="preserve" text-anchor="middle" x="1109" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N requests</text>
</g>
<!-- vetvisit -->
<g id="node26" class="node">
<title>vetvisit</title>
<polygon fill="#fff9c4" stroke="black" points="1588.19,-1274.1 1588.19,-1566.1 1729.81,-1566.1 1729.81,-1274.1 1588.19,-1274.1"/>
<text xml:space="preserve" text-anchor="middle" x="1659" y="-1553.1" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisit</text>
<polyline fill="none" stroke="black" points="1588.19,-1546.1 1729.81,-1546.1"/>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1533.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1521.1" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1509.1" font-family="Helvetica,sans-Serif" font-size="10.00">owner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1497.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1485.1" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1473.1" font-family="Helvetica,sans-Serif" font-size="10.00">hour: time</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1461.1" font-family="Helvetica,sans-Serif" font-size="10.00">visit_type: clinical/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1449.1" font-family="Helvetica,sans-Serif" font-size="10.00"> vaccination/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1437.1" font-family="Helvetica,sans-Serif" font-size="10.00"> telemedicina</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1425.1" font-family="Helvetica,sans-Serif" font-size="10.00">visit_state: PENDING/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1413.1" font-family="Helvetica,sans-Serif" font-size="10.00"> IN_PROGRESS/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1401.1" font-family="Helvetica,sans-Serif" font-size="10.00"> COMPLETED/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1389.1" font-family="Helvetica,sans-Serif" font-size="10.00"> NO_REPORT/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1377.1" font-family="Helvetica,sans-Serif" font-size="10.00"> CANCELLED</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1365.1" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1353.1" font-family="Helvetica,sans-Serif" font-size="10.00">observations: text</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1341.1" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1329.1" font-family="Helvetica,sans-Serif" font-size="10.00">deposit: decimal</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1317.1" font-family="Helvetica,sans-Serif" font-size="10.00">vet_fee: decimal</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1305.1" font-family="Helvetica,sans-Serif" font-size="10.00">pay_transaction: str</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1293.1" font-family="Helvetica,sans-Serif" font-size="10.00">google_event_id: str</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1281.1" font-family="Helvetica,sans-Serif" font-size="10.00">afip_receipt_id: FK (opt)</text>
</g>
<!-- petowner&#45;&gt;vetvisit -->
<g id="edge8" class="edge">
<title>petowner&#45;&gt;vetvisit</title>
<path fill="none" stroke="#f9a825" d="M661.18,-1141.78C661.18,-1223.08 661.18,-1342 661.18,-1342 661.18,-1342 1576.53,-1342 1576.53,-1342"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1576.53,-1345.5 1586.53,-1342 1576.53,-1338.5 1576.53,-1345.5"/>
<text xml:space="preserve" text-anchor="middle" x="1249.57" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N as owner</text>
</g>
<!-- neighborhood -->
<g id="node32" class="node">
<title>neighborhood</title>
<polygon fill="#cfd8dc" stroke="black" points="139.79,-139.1 139.79,-215.1 284.21,-215.1 284.21,-139.1 139.79,-139.1"/>
<text xml:space="preserve" text-anchor="middle" x="212" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Neighborhood</text>
<polyline fill="none" stroke="black" points="139.79,-195.1 284.21,-195.1"/>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">distance_coefficient: decimal</text>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">coverage_area: GIS Polygon</text>
</g>
<!-- petowner&#45;&gt;neighborhood -->
<g id="edge4" class="edge">
<title>petowner&#45;&gt;neighborhood</title>
<path fill="none" stroke="#1565c0" d="M565.5,-1008C489.96,-1008 366.79,-1008 366.79,-1008 366.79,-1008 366.79,-210 366.79,-210 366.79,-210 295.92,-210 295.92,-210"/>
<polygon fill="#1565c0" stroke="#1565c0" points="295.92,-206.5 285.92,-210 295.92,-213.5 295.92,-206.5"/>
<text xml:space="preserve" text-anchor="middle" x="25.56" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 lives in</text>
</g>
<!-- campaign -->
<g id="node38" class="node">
<title>campaign</title>
<polygon fill="#cfd8dc" stroke="black" points="828.77,-133.1 828.77,-221.1 919.23,-221.1 919.23,-133.1 828.77,-133.1"/>
<text xml:space="preserve" text-anchor="middle" x="874" y="-208.1" font-family="Helvetica,sans-Serif" font-size="10.00">Campaign</text>
<polyline fill="none" stroke="black" points="828.77,-201.1 919.23,-201.1"/>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-188.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">utm_source: str</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">utm_medium: str</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-140.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_active: bool</text>
</g>
<!-- petowner&#45;&gt;campaign -->
<g id="edge5" class="edge">
<title>petowner&#45;&gt;campaign</title>
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M680.53,-1005C685.43,-1005 688.57,-1005 688.57,-1005 688.57,-1005 688.57,-210 688.57,-210 688.57,-210 817.13,-210 817.13,-210"/>
<polygon fill="#666666" stroke="#666666" points="817.13,-213.5 827.13,-210 817.13,-206.5 817.13,-213.5"/>
<text xml:space="preserve" text-anchor="middle" x="408.9" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- petvaccine -->
<g id="node4" class="node">
<title>petvaccine</title>
<polygon fill="#90caf9" stroke="black" points="568.07,-508.7 568.07,-596.7 681.93,-596.7 681.93,-508.7 568.07,-508.7"/>
<text xml:space="preserve" text-anchor="middle" x="625" y="-583.7" font-family="Helvetica,sans-Serif" font-size="10.00">PetVaccine</text>
<polyline fill="none" stroke="black" points="568.07,-576.7 681.93,-576.7"/>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">vaccine_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">application_date: date</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">next_application: date</text>
</g>
<!-- pet&#45;&gt;petvaccine -->
<g id="edge10" class="edge">
<title>pet&#45;&gt;petvaccine</title>
<path fill="none" stroke="#1565c0" d="M628,-704.93C628,-704.93 628,-608.65 628,-608.65"/>
<polygon fill="#1565c0" stroke="#1565c0" points="631.5,-608.65 628,-598.65 624.5,-608.65 631.5,-608.65"/>
<text xml:space="preserve" text-anchor="middle" x="633.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petstudy -->
<g id="node5" class="node">
<title>petstudy</title>
<polygon fill="#90caf9" stroke="black" points="478.21,-502.7 478.21,-602.7 549.79,-602.7 549.79,-502.7 478.21,-502.7"/>
<text xml:space="preserve" text-anchor="middle" x="514" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">PetStudy</text>
<polyline fill="none" stroke="black" points="478.21,-582.7 549.79,-582.7"/>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">study_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">result: text</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">images: files</text>
</g>
<!-- pet&#45;&gt;petstudy -->
<g id="edge11" class="edge">
<title>pet&#45;&gt;petstudy</title>
<path fill="none" stroke="#1565c0" d="M574.67,-713C545,-713 514,-713 514,-713 514,-713 514,-614.49 514,-614.49"/>
<polygon fill="#1565c0" stroke="#1565c0" points="517.5,-614.49 514,-604.49 510.5,-614.49 517.5,-614.49"/>
<text xml:space="preserve" text-anchor="middle" x="558.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petbreed -->
<g id="node35" class="node">
<title>petbreed</title>
<polygon fill="#cfd8dc" stroke="black" points="16.26,-145.1 16.26,-209.1 121.74,-209.1 121.74,-145.1 16.26,-145.1"/>
<text xml:space="preserve" text-anchor="middle" x="69" y="-196.1" font-family="Helvetica,sans-Serif" font-size="10.00">PetBreed</text>
<polyline fill="none" stroke="black" points="16.26,-189.1 121.74,-189.1"/>
<text xml:space="preserve" text-anchor="start" x="24.26" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="24.26" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="24.26" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
</g>
<!-- pet&#45;&gt;petbreed -->
<g id="edge9" class="edge">
<title>pet&#45;&gt;petbreed</title>
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M574.5,-721C429.5,-721 39.78,-721 39.78,-721 39.78,-721 39.78,-221.01 39.78,-221.01"/>
<polygon fill="#666666" stroke="#666666" points="43.28,-221.01 39.78,-211.01 36.28,-221.01 43.28,-221.01"/>
<text xml:space="preserve" text-anchor="middle" x="457.9" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- vaccine -->
<g id="node36" class="node">
<title>vaccine</title>
<polygon fill="#cfd8dc" stroke="black" points="501.32,-139.1 501.32,-215.1 620.68,-215.1 620.68,-139.1 501.32,-139.1"/>
<text xml:space="preserve" text-anchor="middle" x="561" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Vaccine</text>
<polyline fill="none" stroke="black" points="501.32,-195.1 620.68,-195.1"/>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">periodicity: int (months)</text>
</g>
<!-- petvaccine&#45;&gt;vaccine -->
<g id="edge12" class="edge">
<title>petvaccine&#45;&gt;vaccine</title>
<path fill="none" stroke="#666666" d="M594.38,-508.51C594.38,-508.51 594.38,-226.98 594.38,-226.98"/>
<polygon fill="#666666" stroke="#666666" points="597.88,-226.98 594.38,-216.98 590.88,-226.98 597.88,-226.98"/>
<text xml:space="preserve" text-anchor="middle" x="581.22" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- study -->
<g id="node37" class="node">
<title>study</title>
<polygon fill="#cfd8dc" stroke="black" points="378.26,-139.1 378.26,-215.1 483.74,-215.1 483.74,-139.1 378.26,-139.1"/>
<text xml:space="preserve" text-anchor="middle" x="431" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Study</text>
<polyline fill="none" stroke="black" points="378.26,-195.1 483.74,-195.1"/>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">group_id: FK (opt)</text>
</g>
<!-- petstudy&#45;&gt;study -->
<g id="edge13" class="edge">
<title>petstudy&#45;&gt;study</title>
<path fill="none" stroke="#666666" d="M492.53,-502.42C492.53,-400.05 492.53,-177 492.53,-177 492.53,-177 491.7,-177 491.7,-177"/>
<polygon fill="#666666" stroke="#666666" points="495.74,-173.5 485.74,-177 495.74,-180.5 495.74,-173.5"/>
<text xml:space="preserve" text-anchor="middle" x="495.22" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- availability -->
<g id="node7" class="node">
<title>availability</title>
<polygon fill="#ffcc80" stroke="black" points="1653.03,-332.1 1653.03,-432.1 1752.97,-432.1 1752.97,-332.1 1653.03,-332.1"/>
<text xml:space="preserve" text-anchor="middle" x="1703" y="-419.1" font-family="Helvetica,sans-Serif" font-size="10.00">Availability</text>
<polyline fill="none" stroke="black" points="1653.03,-412.1 1752.97,-412.1"/>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-399.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-387.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-375.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_day: 0&#45;6</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-363.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_day: 0&#45;6</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-351.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_time: time</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-339.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_time: time</text>
</g>
<!-- veterinarian&#45;&gt;availability -->
<g id="edge18" class="edge">
<title>veterinarian&#45;&gt;availability</title>
<path fill="none" stroke="#e65100" d="M1608.25,-530C1642.2,-530 1688.41,-530 1688.41,-530 1688.41,-530 1688.41,-444.1 1688.41,-444.1"/>
<polygon fill="#e65100" stroke="#e65100" points="1691.91,-444.1 1688.41,-434.1 1684.91,-444.1 1691.91,-444.1"/>
<text xml:space="preserve" text-anchor="middle" x="1653.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- unavailability -->
<g id="node8" class="node">
<title>unavailability</title>
<polygon fill="#ffcc80" stroke="black" points="1771.03,-326.1 1771.03,-438.1 1870.97,-438.1 1870.97,-326.1 1771.03,-326.1"/>
<text xml:space="preserve" text-anchor="middle" x="1821" y="-425.1" font-family="Helvetica,sans-Serif" font-size="10.00">Unavailability</text>
<polyline fill="none" stroke="black" points="1771.03,-418.1 1870.97,-418.1"/>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-405.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-393.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-381.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_date: date</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-369.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_date: date</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-357.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_time: time</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-345.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_time: time</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-333.1" font-family="Helvetica,sans-Serif" font-size="10.00">reason: str</text>
</g>
<!-- veterinarian&#45;&gt;unavailability -->
<g id="edge19" class="edge">
<title>veterinarian&#45;&gt;unavailability</title>
<path fill="none" stroke="#e65100" d="M1608.27,-553C1676.74,-553 1821,-553 1821,-553 1821,-553 1821,-450.05 1821,-450.05"/>
<polygon fill="#e65100" stroke="#e65100" points="1824.5,-450.05 1821,-440.05 1817.5,-450.05 1824.5,-450.05"/>
<text xml:space="preserve" text-anchor="middle" x="1729.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vet_specialty -->
<g id="node9" class="node">
<title>vet_specialty</title>
<polygon fill="#ffb74d" stroke="black" points="1442,-426.1 1249.44,-382.1 1442,-338.1 1634.56,-382.1 1442,-426.1"/>
<text xml:space="preserve" text-anchor="start" x="1283.24" y="-391.1" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Vet&#45;Specialty|veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1283.24" y="-379.1" font-family="Helvetica,sans-Serif" font-size="10.00">specialty_id: FK</text>
<text xml:space="preserve" text-anchor="middle" x="1442" y="-367.1" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
</g>
<!-- veterinarian&#45;&gt;vet_specialty -->
<g id="edge14" class="edge">
<title>veterinarian&#45;&gt;vet_specialty</title>
<path fill="none" stroke="#e65100" d="M1584.65,-508.32C1584.65,-508.32 1584.65,-405.5 1584.65,-405.5"/>
<polygon fill="#e65100" stroke="#e65100" points="1588.15,-405.5 1584.65,-395.5 1581.15,-405.5 1588.15,-405.5"/>
<text xml:space="preserve" text-anchor="middle" x="1505.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vet_neighborhood -->
<g id="node10" class="node">
<title>vet_neighborhood</title>
<polygon fill="#ffb74d" stroke="black" points="1017,-426.1 802.19,-382.1 1017,-338.1 1231.81,-382.1 1017,-426.1"/>
<text xml:space="preserve" text-anchor="start" x="838.97" y="-391.1" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Vet&#45;Neighborhood|veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="838.97" y="-379.1" font-family="Helvetica,sans-Serif" font-size="10.00">neighborhood_id: FK</text>
<text xml:space="preserve" text-anchor="middle" x="1017" y="-367.1" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
</g>
<!-- veterinarian&#45;&gt;vet_neighborhood -->
<g id="edge16" class="edge">
<title>veterinarian&#45;&gt;vet_neighborhood</title>
<path fill="none" stroke="#e65100" d="M1537.71,-544C1447.43,-544 1215.15,-544 1215.15,-544 1215.15,-544 1215.15,-397.3 1215.15,-397.3"/>
<polygon fill="#e65100" stroke="#e65100" points="1218.65,-397.3 1215.15,-387.3 1211.65,-397.3 1218.65,-397.3"/>
<text xml:space="preserve" text-anchor="middle" x="1273.79" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N coverage</text>
</g>
<!-- turnfeegroup -->
<g id="node41" class="node">
<title>turnfeegroup</title>
<polygon fill="#cfd8dc" stroke="black" points="1067.24,-145.1 1067.24,-209.1 1192.76,-209.1 1192.76,-145.1 1067.24,-145.1"/>
<text xml:space="preserve" text-anchor="middle" x="1130" y="-196.1" font-family="Helvetica,sans-Serif" font-size="10.00">IndividualTurnFeeGroup</text>
<polyline fill="none" stroke="black" points="1067.24,-189.1 1192.76,-189.1"/>
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">fee_percentage: decimal</text>
</g>
<!-- veterinarian&#45;&gt;turnfeegroup -->
<g id="edge20" class="edge">
<title>veterinarian&#45;&gt;turnfeegroup</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1537.74,-526C1452.2,-526 1240.63,-526 1240.63,-526 1240.63,-526 1240.63,-177 1240.63,-177 1240.63,-177 1204.58,-177 1204.58,-177"/>
<polygon fill="#e65100" stroke="#e65100" points="1204.58,-173.5 1194.58,-177 1204.58,-180.5 1204.58,-173.5"/>
<text xml:space="preserve" text-anchor="middle" x="777.33" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:M</text>
</g>
<!-- specialty -->
<g id="node31" class="node">
<title>specialty</title>
<polygon fill="#cfd8dc" stroke="black" points="753.16,-151.1 753.16,-203.1 810.84,-203.1 810.84,-151.1 753.16,-151.1"/>
<text xml:space="preserve" text-anchor="middle" x="782" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Specialty</text>
<polyline fill="none" stroke="black" points="753.16,-183.1 810.84,-183.1"/>
<text xml:space="preserve" text-anchor="start" x="761.16" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="761.16" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
</g>
<!-- vet_specialty&#45;&gt;specialty -->
<g id="edge15" class="edge">
<title>vet_specialty&#45;&gt;specialty</title>
<path fill="none" stroke="#e65100" d="M1442,-337.81C1442,-315.45 1442,-293 1442,-293 1442,-293 806.51,-293 806.51,-293 806.51,-293 806.51,-214.91 806.51,-214.91"/>
<polygon fill="#e65100" stroke="#e65100" points="810.01,-214.91 806.51,-204.91 803.01,-214.91 810.01,-214.91"/>
<text xml:space="preserve" text-anchor="middle" x="751.22" y="-300.4" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vet_neighborhood&#45;&gt;neighborhood -->
<g id="edge17" class="edge">
<title>vet_neighborhood&#45;&gt;neighborhood</title>
<path fill="none" stroke="#e65100" d="M800.26,-382C572.59,-382 247.21,-382 247.21,-382 247.21,-382 247.21,-226.67 247.21,-226.67"/>
<polygon fill="#e65100" stroke="#e65100" points="250.71,-226.67 247.21,-216.67 243.71,-226.67 250.71,-226.67"/>
<text xml:space="preserve" text-anchor="middle" x="657.22" y="-300.4" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- grupo -->
<g id="node11" class="node">
<title>grupo</title>
<polygon fill="#e1bee7" stroke="black" points="85.98,-1388.1 85.98,-1452.1 172.02,-1452.1 172.02,-1388.1 85.98,-1388.1"/>
<text xml:space="preserve" text-anchor="middle" x="129" y="-1439.1" font-family="Helvetica,sans-Serif" font-size="10.00">Group</text>
<polyline fill="none" stroke="black" points="85.98,-1432.1 172.02,-1432.1"/>
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
</g>
<!-- category -->
<g id="node12" class="node">
<title>category</title>
<polygon fill="#e1bee7" stroke="black" points="166.6,-1023.5 166.6,-1111.5 255.4,-1111.5 255.4,-1023.5 166.6,-1023.5"/>
<text xml:space="preserve" text-anchor="middle" x="211" y="-1098.5" font-family="Helvetica,sans-Serif" font-size="10.00">Category</text>
<polyline fill="none" stroke="black" points="166.6,-1091.5 255.4,-1091.5"/>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00">group_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00">value: int (order)</text>
</g>
<!-- grupo&#45;&gt;category -->
<g id="edge21" class="edge">
<title>grupo&#45;&gt;category</title>
<path fill="none" stroke="#7b1fa2" d="M172.38,-1420C177.51,-1420 180.96,-1420 180.96,-1420 180.96,-1420 180.96,-1123.3 180.96,-1123.3"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="184.46,-1123.3 180.96,-1113.3 177.46,-1123.3 184.46,-1123.3"/>
<text xml:space="preserve" text-anchor="middle" x="151.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- service -->
<g id="node13" class="node">
<title>service</title>
<polygon fill="#ce93d8" stroke="black" points="199.63,-729.3 199.63,-889.3 322.37,-889.3 322.37,-729.3 199.63,-729.3"/>
<text xml:space="preserve" text-anchor="middle" x="261" y="-876.3" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
<polyline fill="none" stroke="black" points="199.63,-869.3 322.37,-869.3"/>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-856.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-844.3" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-832.3" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">specialty_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">category_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">duration: int (min)</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">modality: onsite/online</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">payment_sign_req: bool</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-760.3" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type_filter: str</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-748.3" font-family="Helvetica,sans-Serif" font-size="10.00">age_filter: str</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-736.3" font-family="Helvetica,sans-Serif" font-size="10.00">weight_range: str</text>
</g>
<!-- category&#45;&gt;service -->
<g id="edge22" class="edge">
<title>category&#45;&gt;service</title>
<path fill="none" stroke="#7b1fa2" d="M227.52,-1023.11C227.52,-1023.11 227.52,-901.07 227.52,-901.07"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="231.02,-901.07 227.52,-891.07 224.02,-901.08 231.02,-901.07"/>
<text xml:space="preserve" text-anchor="middle" x="250.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- prices -->
<g id="node14" class="node">
<title>prices</title>
<polygon fill="#ba68c8" stroke="black" points="63.3,-484.7 63.3,-620.7 192.7,-620.7 192.7,-484.7 63.3,-484.7"/>
<text xml:space="preserve" text-anchor="middle" x="128" y="-607.7" font-family="Helvetica,sans-Serif" font-size="10.00">Prices</text>
<polyline fill="none" stroke="black" points="63.3,-600.7 192.7,-600.7"/>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-587.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-575.7" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">professional_fee: decimal</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">payment_sign: decimal</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">from_date: date</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-503.7" font-family="Helvetica,sans-Serif" font-size="10.00">to_date: date</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-491.7" font-family="Helvetica,sans-Serif" font-size="10.00">active: bool</text>
</g>
<!-- service&#45;&gt;prices -->
<g id="edge24" class="edge">
<title>service&#45;&gt;prices</title>
<path fill="none" stroke="#7b1fa2" d="M204.92,-728.98C204.92,-673.26 204.92,-609 204.92,-609 204.92,-609 203.72,-609 203.72,-609"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="204.41,-605.5 194.41,-609 204.41,-612.5 204.41,-605.5"/>
<text xml:space="preserve" text-anchor="middle" x="184.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- discounts -->
<g id="node15" class="node">
<title>discounts</title>
<polygon fill="#ba68c8" stroke="black" points="210.2,-502.7 210.2,-602.7 321.8,-602.7 321.8,-502.7 210.2,-502.7"/>
<text xml:space="preserve" text-anchor="middle" x="266" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">Discounts</text>
<polyline fill="none" stroke="black" points="210.2,-582.7 321.8,-582.7"/>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">discount: decimal (%)</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">from_date: date</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">to_date: date</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">active: bool</text>
</g>
<!-- service&#45;&gt;discounts -->
<g id="edge25" class="edge">
<title>service&#45;&gt;discounts</title>
<path fill="none" stroke="#7b1fa2" d="M247.4,-728.81C247.4,-728.81 247.4,-614.57 247.4,-614.57"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="250.9,-614.57 247.4,-604.57 243.9,-614.57 250.9,-614.57"/>
<text xml:space="preserve" text-anchor="middle" x="270.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- service&#45;&gt;specialty -->
<g id="edge23" class="edge">
<title>service&#45;&gt;specialty</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M284.6,-728.87C284.6,-675.42 284.6,-615 284.6,-615 284.6,-615 777.67,-615 777.67,-615 777.67,-615 777.67,-215.02 777.67,-215.02"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="781.17,-215.02 777.67,-205.02 774.17,-215.02 781.17,-215.02"/>
<text xml:space="preserve" text-anchor="middle" x="538.9" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- prices&#45;&gt;veterinarian -->
<g id="edge26" class="edge">
<title>prices&#45;&gt;veterinarian</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M192.84,-493C472.5,-493 1561.35,-493 1561.35,-493 1561.35,-493 1561.35,-496.86 1561.35,-496.86"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="1557.85,-496.86 1561.35,-506.86 1564.85,-496.86 1557.85,-496.86"/>
<text xml:space="preserve" text-anchor="middle" x="434" y="-679.6" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="434" y="-670" font-family="Helvetica,sans-Serif" font-size="8.00">(vet&#45;specific)</text>
</g>
<!-- servicecombo -->
<g id="node16" class="node">
<title>servicecombo</title>
<polygon fill="#e1bee7" stroke="black" points="189.91,-1376.1 189.91,-1464.1 322.09,-1464.1 322.09,-1376.1 189.91,-1376.1"/>
<text xml:space="preserve" text-anchor="middle" x="256" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceCombo</text>
<polyline fill="none" stroke="black" points="189.91,-1444.1 322.09,-1444.1"/>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">discount_percent: decimal</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">discount_fixed: decimal</text>
</g>
<!-- cart&#45;&gt;veterinarian -->
<g id="edge27" class="edge">
<title>cart&#45;&gt;veterinarian</title>
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M949.06,-1375.77C949.06,-1202.29 949.06,-579 949.06,-579 949.06,-579 1526.15,-579 1526.15,-579"/>
<polygon fill="#00838f" stroke="#00838f" points="1526.15,-582.5 1536.15,-579 1526.15,-575.5 1526.15,-582.5"/>
<text xml:space="preserve" text-anchor="middle" x="1789.12" y="-943.8" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="1789.12" y="-934.2" font-family="Helvetica,sans-Serif" font-size="8.00">(assigned vet)</text>
</g>
<!-- cartitem -->
<g id="node18" class="node">
<title>cartitem</title>
<polygon fill="#80deea" stroke="black" points="735.93,-1011.5 735.93,-1123.5 838.07,-1123.5 838.07,-1011.5 735.93,-1011.5"/>
<text xml:space="preserve" text-anchor="middle" x="787" y="-1110.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartItem</text>
<polyline fill="none" stroke="black" points="735.93,-1103.5 838.07,-1103.5"/>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1090.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00">quantity: int</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1018.5" font-family="Helvetica,sans-Serif" font-size="10.00">total: decimal (calc)</text>
</g>
<!-- cart&#45;&gt;cartitem -->
<g id="edge28" class="edge">
<title>cart&#45;&gt;cartitem</title>
<path fill="none" stroke="#00838f" d="M923.27,-1375.74C923.27,-1291.12 923.27,-1115 923.27,-1115 923.27,-1115 850.03,-1115 850.03,-1115"/>
<polygon fill="#00838f" stroke="#00838f" points="850.03,-1111.5 840.03,-1115 850.03,-1118.5 850.03,-1111.5"/>
<text xml:space="preserve" text-anchor="middle" x="862.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartresumeitem -->
<g id="node19" class="node">
<title>cartresumeitem</title>
<polygon fill="#80deea" stroke="black" points="957.93,-999.5 957.93,-1135.5 1070.07,-1135.5 1070.07,-999.5 957.93,-999.5"/>
<text xml:space="preserve" text-anchor="middle" x="1014" y="-1122.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartResumeItem</text>
<polyline fill="none" stroke="black" points="957.93,-1115.5 1070.07,-1115.5"/>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1102.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1090.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">concept: SUBTOTAL/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00"> DESCUENTO/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00"> ADELANTO/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00"> TOTAL/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00"> COSTO_SERVICIO</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1018.5" font-family="Helvetica,sans-Serif" font-size="10.00">amount: decimal</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1006.5" font-family="Helvetica,sans-Serif" font-size="10.00">order: int</text>
</g>
<!-- cart&#45;&gt;cartresumeitem -->
<g id="edge29" class="edge">
<title>cart&#45;&gt;cartresumeitem</title>
<path fill="none" stroke="#00838f" d="M981.83,-1375.8C981.83,-1375.8 981.83,-1147.31 981.83,-1147.31"/>
<polygon fill="#00838f" stroke="#00838f" points="985.33,-1147.31 981.83,-1137.31 978.33,-1147.31 985.33,-1147.31"/>
<text xml:space="preserve" text-anchor="middle" x="990.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartpetreason -->
<g id="node20" class="node">
<title>cartpetreason</title>
<polygon fill="#80deea" stroke="black" points="855.82,-1029.5 855.82,-1105.5 940.18,-1105.5 940.18,-1029.5 855.82,-1029.5"/>
<text xml:space="preserve" text-anchor="middle" x="898" y="-1092.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartPetReason</text>
<polyline fill="none" stroke="black" points="855.82,-1085.5 940.18,-1085.5"/>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
</g>
<!-- cart&#45;&gt;cartpetreason -->
<g id="edge30" class="edge">
<title>cart&#45;&gt;cartpetreason</title>
<path fill="none" stroke="#00838f" d="M931.73,-1375.8C931.73,-1375.8 931.73,-1117.47 931.73,-1117.47"/>
<polygon fill="#00838f" stroke="#00838f" points="935.23,-1117.47 931.73,-1107.47 928.23,-1117.47 935.23,-1117.47"/>
<text xml:space="preserve" text-anchor="middle" x="919.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartitem&#45;&gt;pet -->
<g id="edge32" class="edge">
<title>cartitem&#45;&gt;pet</title>
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M747.42,-1011.12C747.42,-964.19 747.42,-905 747.42,-905 747.42,-905 692.94,-905 692.94,-905"/>
<polygon fill="#00838f" stroke="#00838f" points="692.94,-901.5 682.94,-905 692.94,-908.5 692.94,-901.5"/>
<text xml:space="preserve" text-anchor="middle" x="640.9" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- cartitem&#45;&gt;service -->
<g id="edge31" class="edge">
<title>cartitem&#45;&gt;service</title>
<path fill="none" stroke="#00838f" d="M741.67,-1011.15C741.67,-997.62 741.67,-987 741.67,-987 741.67,-987 288.75,-987 288.75,-987 288.75,-987 288.75,-901.09 288.75,-901.09"/>
<polygon fill="#00838f" stroke="#00838f" points="292.25,-901.09 288.75,-891.09 285.25,-901.09 292.25,-901.09"/>
<text xml:space="preserve" text-anchor="middle" x="446.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- cartpetreason&#45;&gt;pet -->
<g id="edge33" class="edge">
<title>cartpetreason&#45;&gt;pet</title>
<path fill="none" stroke="#00838f" d="M898,-1029.01C898,-978.45 898,-896 898,-896 898,-896 692.86,-896 692.86,-896"/>
<polygon fill="#00838f" stroke="#00838f" points="692.86,-892.5 682.86,-896 692.86,-899.5 692.86,-892.5"/>
<text xml:space="preserve" text-anchor="middle" x="683.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- servicerequest&#45;&gt;veterinarian -->
<g id="edge35" class="edge">
<title>servicerequest&#45;&gt;veterinarian</title>
<path fill="none" stroke="#c62828" stroke-dasharray="5,2" d="M1895.22,-975C1780.2,-975 1561.26,-975 1561.26,-975 1561.26,-975 1561.26,-608.61 1561.26,-608.61"/>
<polygon fill="#c62828" stroke="#c62828" points="1564.76,-608.61 1561.26,-598.61 1557.76,-608.61 1564.76,-608.61"/>
<text xml:space="preserve" text-anchor="middle" x="1858.68" y="-811.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="1858.68" y="-802.1" font-family="Helvetica,sans-Serif" font-size="8.00">(assigned)</text>
</g>
<!-- servicerequest&#45;&gt;cart -->
<g id="edge34" class="edge">
<title>servicerequest&#45;&gt;cart</title>
<path fill="none" stroke="#c62828" d="M1895.08,-1154C1675.36,-1154 1005.73,-1154 1005.73,-1154 1005.73,-1154 1005.73,-1364.35 1005.73,-1364.35"/>
<polygon fill="#c62828" stroke="#c62828" points="1002.23,-1364.35 1005.73,-1374.35 1009.23,-1364.35 1002.23,-1364.35"/>
<text xml:space="preserve" text-anchor="middle" x="1160.56" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- statehistory -->
<g id="node22" class="node">
<title>statehistory</title>
<polygon fill="#ef9a9a" stroke="black" points="2182.86,-759.3 2182.86,-859.3 2301.14,-859.3 2301.14,-759.3 2182.86,-759.3"/>
<text xml:space="preserve" text-anchor="middle" x="2242" y="-846.3" font-family="Helvetica,sans-Serif" font-size="10.00">StateHistory</text>
<polyline fill="none" stroke="black" points="2182.86,-839.3 2301.14,-839.3"/>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-826.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">state: str</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">additional_data: JSON</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">created_at: datetime</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-766.3" font-family="Helvetica,sans-Serif" font-size="10.00">user_id: FK</text>
</g>
<!-- servicerequest&#45;&gt;statehistory -->
<g id="edge36" class="edge">
<title>servicerequest&#45;&gt;statehistory</title>
<path fill="none" stroke="#c62828" d="M2033.08,-987C2115.41,-987 2242,-987 2242,-987 2242,-987 2242,-871.26 2242,-871.26"/>
<polygon fill="#c62828" stroke="#c62828" points="2245.5,-871.26 2242,-861.26 2238.5,-871.26 2245.5,-871.26"/>
<text xml:space="preserve" text-anchor="middle" x="2179.01" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N audit</text>
</g>
<!-- vetasked -->
<g id="node23" class="node">
<title>vetasked</title>
<polygon fill="#ef9a9a" stroke="black" points="1894.85,-759.3 1894.85,-859.3 2023.15,-859.3 2023.15,-759.3 1894.85,-759.3"/>
<text xml:space="preserve" text-anchor="middle" x="1959" y="-846.3" font-family="Helvetica,sans-Serif" font-size="10.00">VeterinarianAsked</text>
<polyline fill="none" stroke="black" points="1894.85,-839.3 2023.15,-839.3"/>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-826.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">date_asked: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">date_answered: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-766.3" font-family="Helvetica,sans-Serif" font-size="10.00">accepted: bool</text>
</g>
<!-- servicerequest&#45;&gt;vetasked -->
<g id="edge37" class="edge">
<title>servicerequest&#45;&gt;vetasked</title>
<path fill="none" stroke="#c62828" d="M1959.27,-969.15C1959.27,-969.15 1959.27,-871.22 1959.27,-871.22"/>
<polygon fill="#c62828" stroke="#c62828" points="1962.77,-871.22 1959.27,-861.22 1955.77,-871.22 1962.77,-871.22"/>
<text xml:space="preserve" text-anchor="middle" x="1967.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- payreminder -->
<g id="node25" class="node">
<title>payreminder</title>
<polygon fill="#e57373" stroke="black" points="2041.63,-765.3 2041.63,-853.3 2164.37,-853.3 2164.37,-765.3 2041.63,-765.3"/>
<text xml:space="preserve" text-anchor="middle" x="2103" y="-840.3" font-family="Helvetica,sans-Serif" font-size="10.00">PaymentReminder</text>
<polyline fill="none" stroke="black" points="2041.63,-833.3 2164.37,-833.3"/>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">scheduled_for: datetime</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">processed_at: datetime</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">status: pending/sent/...</text>
</g>
<!-- servicerequest&#45;&gt;payreminder -->
<g id="edge38" class="edge">
<title>servicerequest&#45;&gt;payreminder</title>
<path fill="none" stroke="#c62828" d="M2029.45,-969.03C2029.45,-895.98 2029.45,-809 2029.45,-809 2029.45,-809 2030.63,-809 2030.63,-809"/>
<polygon fill="#c62828" stroke="#c62828" points="2029.75,-812.5 2039.75,-809 2029.75,-805.5 2029.75,-812.5"/>
<text xml:space="preserve" text-anchor="middle" x="2055.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- servicerequest&#45;&gt;campaign -->
<g id="edge39" class="edge">
<title>servicerequest&#45;&gt;campaign</title>
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M2026.3,-969.29C2026.3,-754.09 2026.3,-260 2026.3,-260 2026.3,-260 874,-260 874,-260 874,-260 874,-232.98 874,-232.98"/>
<polygon fill="#666666" stroke="#666666" points="877.5,-232.98 874,-222.98 870.5,-232.98 877.5,-232.98"/>
<text xml:space="preserve" text-anchor="middle" x="2341.9" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- tag -->
<g id="node39" class="node">
<title>tag</title>
<polygon fill="#cfd8dc" stroke="black" points="638.43,-151.1 638.43,-203.1 735.57,-203.1 735.57,-151.1 638.43,-151.1"/>
<text xml:space="preserve" text-anchor="middle" x="687" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Tag</text>
<polyline fill="none" stroke="black" points="638.43,-183.1 735.57,-183.1"/>
<text xml:space="preserve" text-anchor="start" x="646.43" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="646.43" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str (unique)</text>
</g>
<!-- servicerequest&#45;&gt;tag -->
<g id="edge40" class="edge">
<title>servicerequest&#45;&gt;tag</title>
<path fill="none" stroke="#666666" d="M1895.27,-981C1634.21,-981 718.72,-981 718.72,-981 718.72,-981 718.72,-214.9 718.72,-214.9"/>
<polygon fill="#666666" stroke="#666666" points="722.22,-214.9 718.72,-204.9 715.22,-214.9 722.22,-214.9"/>
<text xml:space="preserve" text-anchor="middle" x="756.33" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:M</text>
</g>
<!-- vetasked&#45;&gt;veterinarian -->
<g id="edge41" class="edge">
<title>vetasked&#45;&gt;veterinarian</title>
<path fill="none" stroke="#c62828" d="M1894.62,-765C1787.53,-765 1584.47,-765 1584.47,-765 1584.47,-765 1584.47,-608.46 1584.47,-608.46"/>
<polygon fill="#c62828" stroke="#c62828" points="1587.97,-608.46 1584.47,-598.46 1580.97,-608.46 1587.97,-608.46"/>
<text xml:space="preserve" text-anchor="middle" x="1848.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vetreminder -->
<g id="node24" class="node">
<title>vetreminder</title>
<polygon fill="#e57373" stroke="black" points="1897.63,-502.7 1897.63,-602.7 2020.37,-602.7 2020.37,-502.7 1897.63,-502.7"/>
<text xml:space="preserve" text-anchor="middle" x="1959" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">ScheduledVetReminder</text>
<polyline fill="none" stroke="black" points="1897.63,-582.7 2020.37,-582.7"/>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">vet_asked_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">scheduled_for: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">processed_at: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">status: pending/sent/...</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">celery_task_id: str</text>
</g>
<!-- vetasked&#45;&gt;vetreminder -->
<g id="edge42" class="edge">
<title>vetasked&#45;&gt;vetreminder</title>
<path fill="none" stroke="#c62828" d="M1959,-758.91C1959,-758.91 1959,-614.49 1959,-614.49"/>
<polygon fill="#c62828" stroke="#c62828" points="1962.5,-614.49 1959,-604.49 1955.5,-614.49 1962.5,-614.49"/>
<text xml:space="preserve" text-anchor="middle" x="1965.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisit&#45;&gt;veterinarian -->
<g id="edge44" class="edge">
<title>vetvisit&#45;&gt;veterinarian</title>
<path fill="none" stroke="#f9a825" d="M1617.03,-1273.86C1617.03,-1031.98 1617.03,-575 1617.03,-575 1617.03,-575 1616.13,-575 1616.13,-575"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1619.63,-571.5 1609.63,-575 1619.63,-578.5 1619.63,-571.5"/>
<text xml:space="preserve" text-anchor="middle" x="2373.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vetvisit&#45;&gt;servicerequest -->
<g id="edge43" class="edge">
<title>vetvisit&#45;&gt;servicerequest</title>
<path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M1695.23,-1273.71C1695.23,-1214.56 1695.23,-1160 1695.23,-1160 1695.23,-1160 1883.62,-1160 1883.62,-1160"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1883.62,-1163.5 1893.62,-1160 1883.62,-1156.5 1883.62,-1163.5"/>
<text xml:space="preserve" text-anchor="middle" x="1864.12" y="-1248.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="1864.12" y="-1238.8" font-family="Helvetica,sans-Serif" font-size="8.00">(from request)</text>
</g>
<!-- vetvisitreport -->
<g id="node27" class="node">
<title>vetvisitreport</title>
<polygon fill="#fff59d" stroke="black" points="1626.09,-1005.5 1626.09,-1129.5 1729.91,-1129.5 1729.91,-1005.5 1626.09,-1005.5"/>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-1116.5" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitReport</text>
<polyline fill="none" stroke="black" points="1626.09,-1109.5 1729.91,-1109.5"/>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">visit_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">physical_exam: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">diagnosis: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">treatment: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">pdf_file: file</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitreport -->
<g id="edge47" class="edge">
<title>vetvisit&#45;&gt;vetvisitreport</title>
<path fill="none" stroke="#f9a825" d="M1660.66,-1273.63C1660.66,-1273.63 1660.66,-1141.19 1660.66,-1141.19"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1664.16,-1141.19 1660.66,-1131.19 1657.16,-1141.19 1664.16,-1141.19"/>
<text xml:space="preserve" text-anchor="middle" x="1675.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisitpetreason -->
<g id="node29" class="node">
<title>vetvisitpetreason</title>
<polygon fill="#fff176" stroke="black" points="1508.31,-1029.5 1508.31,-1105.5 1607.69,-1105.5 1607.69,-1029.5 1508.31,-1029.5"/>
<text xml:space="preserve" text-anchor="middle" x="1558" y="-1092.5" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitPetReason</text>
<polyline fill="none" stroke="black" points="1508.31,-1085.5 1607.69,-1085.5"/>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">visit_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitpetreason -->
<g id="edge48" class="edge">
<title>vetvisit&#45;&gt;vetvisitpetreason</title>
<path fill="none" stroke="#f9a825" d="M1597.94,-1273.63C1597.94,-1273.63 1597.94,-1117.18 1597.94,-1117.18"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1601.44,-1117.18 1597.94,-1107.18 1594.44,-1117.18 1601.44,-1117.18"/>
<text xml:space="preserve" text-anchor="middle" x="1598.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- visit_pets -->
<g id="node30" class="node">
<title>visit_pets</title>
<polygon fill="#ffee58" stroke="black" points="1336,-1111.5 1181.81,-1067.5 1336,-1023.5 1490.19,-1067.5 1336,-1111.5"/>
<text xml:space="preserve" text-anchor="start" x="1210.47" y="-1076.5" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Visit&#45;Pets|vetvisit_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1210.47" y="-1064.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="middle" x="1336" y="-1052.5" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
</g>
<!-- vetvisit&#45;&gt;visit_pets -->
<g id="edge45" class="edge">
<title>vetvisit&#45;&gt;visit_pets</title>
<path fill="none" stroke="#f9a825" d="M1587.72,-1308C1492.6,-1308 1336,-1308 1336,-1308 1336,-1308 1336,-1123.41 1336,-1123.41"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1339.5,-1123.41 1336,-1113.41 1332.5,-1123.41 1339.5,-1123.41"/>
<text xml:space="preserve" text-anchor="middle" x="1476.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- receipt -->
<g id="node42" class="node">
<title>receipt</title>
<polygon fill="#f8bbd9" stroke="black" points="2394.58,-1005.5 2394.58,-1129.5 2523.42,-1129.5 2523.42,-1005.5 2394.58,-1005.5"/>
<text xml:space="preserve" text-anchor="middle" x="2459" y="-1116.5" font-family="Helvetica,sans-Serif" font-size="10.00">Receipt</text>
<polyline fill="none" stroke="black" points="2394.58,-1109.5 2523.42,-1109.5"/>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">document_number: bigint</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">receipt_number: int</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">issued_date: date</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">total_amount: decimal</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">net_taxed: decimal</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">cae: str</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">cae_expiration: date</text>
</g>
<!-- vetvisit&#45;&gt;receipt -->
<g id="edge52" class="edge">
<title>vetvisit&#45;&gt;receipt</title>
<path fill="none" stroke="#ad1457" stroke-dasharray="5,2" d="M1729.99,-1420C1925.78,-1420 2459,-1420 2459,-1420 2459,-1420 2459,-1141.22 2459,-1141.22"/>
<polygon fill="#ad1457" stroke="#ad1457" points="2462.5,-1141.22 2459,-1131.22 2455.5,-1141.22 2462.5,-1141.22"/>
<text xml:space="preserve" text-anchor="middle" x="2444.12" y="-1248.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="2444.12" y="-1238.8" font-family="Helvetica,sans-Serif" font-size="8.00">(invoice)</text>
</g>
<!-- vetvisitreport&#45;&gt;pet -->
<g id="edge49" class="edge">
<title>vetvisitreport&#45;&gt;pet</title>
<path fill="none" stroke="#f9a825" d="M1662.74,-1005.24C1662.74,-947.02 1662.74,-869 1662.74,-869 1662.74,-869 692.97,-869 692.97,-869"/>
<polygon fill="#f9a825" stroke="#f9a825" points="692.97,-865.5 682.97,-869 692.97,-872.5 692.97,-865.5"/>
<text xml:space="preserve" text-anchor="middle" x="1237.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vetvisitfollowup -->
<g id="node28" class="node">
<title>vetvisitfollowup</title>
<polygon fill="#fff176" stroke="black" points="1632.21,-771.3 1632.21,-847.3 1723.79,-847.3 1723.79,-771.3 1632.21,-771.3"/>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-834.3" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitFollowUp</text>
<polyline fill="none" stroke="black" points="1632.21,-827.3 1723.79,-827.3"/>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">report_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
</g>
<!-- vetvisitreport&#45;&gt;vetvisitfollowup -->
<g id="edge50" class="edge">
<title>vetvisitreport&#45;&gt;vetvisitfollowup</title>
<path fill="none" stroke="#f9a825" d="M1693.26,-1005.33C1693.26,-1005.33 1693.26,-859.22 1693.26,-859.22"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1696.76,-859.22 1693.26,-849.22 1689.76,-859.22 1696.76,-859.22"/>
<text xml:space="preserve" text-anchor="middle" x="1684.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisitpetreason&#45;&gt;pet -->
<g id="edge51" class="edge">
<title>vetvisitpetreason&#45;&gt;pet</title>
<path fill="none" stroke="#f9a825" d="M1523.18,-1029.15C1523.18,-973.78 1523.18,-878 1523.18,-878 1523.18,-878 692.83,-878 692.83,-878"/>
<polygon fill="#f9a825" stroke="#f9a825" points="692.83,-874.5 682.83,-878 692.83,-881.5 692.83,-874.5"/>
<text xml:space="preserve" text-anchor="middle" x="753.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- visit_pets&#45;&gt;pet -->
<g id="edge46" class="edge">
<title>visit_pets&#45;&gt;pet</title>
<path fill="none" stroke="#f9a825" d="M1198.48,-1062.29C1198.48,-1032.7 1198.48,-887 1198.48,-887 1198.48,-887 692.93,-887 692.93,-887"/>
<polygon fill="#f9a825" stroke="#f9a825" points="692.93,-883.5 682.93,-887 692.93,-890.5 692.93,-883.5"/>
<text xml:space="preserve" text-anchor="middle" x="718.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- province -->
<g id="node33" class="node">
<title>province</title>
<polygon fill="#cfd8dc" stroke="black" points="302.16,-151.1 302.16,-203.1 359.84,-203.1 359.84,-151.1 302.16,-151.1"/>
<text xml:space="preserve" text-anchor="middle" x="331" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Province</text>
<polyline fill="none" stroke="black" points="302.16,-183.1 359.84,-183.1"/>
<text xml:space="preserve" text-anchor="start" x="310.16" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="310.16" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
</g>
<!-- locality -->
<g id="node34" class="node">
<title>locality</title>
<polygon fill="#cfd8dc" stroke="black" points="288.26,-16.5 288.26,-80.5 373.74,-80.5 373.74,-16.5 288.26,-16.5"/>
<text xml:space="preserve" text-anchor="middle" x="331" y="-67.5" font-family="Helvetica,sans-Serif" font-size="10.00">Locality</text>
<polyline fill="none" stroke="black" points="288.26,-60.5 373.74,-60.5"/>
<text xml:space="preserve" text-anchor="start" x="296.26" y="-47.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="296.26" y="-35.5" font-family="Helvetica,sans-Serif" font-size="10.00">province_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="296.26" y="-23.5" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
</g>
<!-- province&#45;&gt;locality -->
<g id="edge53" class="edge">
<title>province&#45;&gt;locality</title>
<path fill="none" stroke="#666666" d="M331,-150.77C331,-150.77 331,-92.24 331,-92.24"/>
<polygon fill="#666666" stroke="#666666" points="334.5,-92.24 331,-82.24 327.5,-92.24 334.5,-92.24"/>
<text xml:space="preserve" text-anchor="middle" x="337.22" y="-101.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- medication -->
<g id="node40" class="node">
<title>medication</title>
<polygon fill="#cfd8dc" stroke="black" points="937.19,-127.1 937.19,-227.1 1048.81,-227.1 1048.81,-127.1 937.19,-127.1"/>
<text xml:space="preserve" text-anchor="middle" x="993" y="-214.1" font-family="Helvetica,sans-Serif" font-size="10.00">Medication</text>
<polyline fill="none" stroke="black" points="937.19,-207.1 1048.81,-207.1"/>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-194.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">general_name_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">type_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">presentation_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-134.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: str</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,195 @@
digraph DataModelSimple {
rankdir=TB
compound=true
splines=ortho
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10]
edge [fontname="Helvetica", fontsize=8]
nodesep=0.3
ranksep=1.2
newrank=true
label="AMAR Mascotas - Data Model Overview"
labelloc="t"
fontsize=16
fontname="Helvetica-Bold"
// === ROW 1: AUTH (center) ===
subgraph cluster_auth {
label="Users & Auth"
style="rounded,filled"
fillcolor="#E8F5E9"
color="#2E7D32"
auth_user [label="auth.User", fillcolor="#C8E6C9"]
}
// === ROW 2: LEFT COLUMN - Pet Owners ===
subgraph cluster_mascotas {
label="Pet Owners & Pets"
style="rounded,filled"
fillcolor="#E3F2FD"
color="#1565C0"
petowner [label="PetOwner", fillcolor="#BBDEFB"]
pet [label="Pet", fillcolor="#BBDEFB"]
petvaccine [label="PetVaccine", fillcolor="#90CAF9"]
petstudy [label="PetStudy", fillcolor="#90CAF9"]
}
// === ROW 2: CENTER COLUMN - Veterinarians ===
subgraph cluster_vets {
label="Veterinarians"
style="rounded,filled"
fillcolor="#FFF3E0"
color="#E65100"
veterinarian [label="Veterinarian", fillcolor="#FFE0B2"]
availability [label="Availability", fillcolor="#FFCC80"]
unavailability [label="Unavailability", fillcolor="#FFCC80"]
vet_specialty [label="Vet-Specialty", shape=diamond, fillcolor="#FFB74D"]
vet_neighborhood [label="Vet-Neighborhood", shape=diamond, fillcolor="#FFB74D"]
}
// === ROW 2: RIGHT COLUMN - Services ===
subgraph cluster_productos {
label="Services & Pricing"
style="rounded,filled"
fillcolor="#F3E5F5"
color="#7B1FA2"
grupo [label="Group", fillcolor="#E1BEE7"]
category [label="Category", fillcolor="#E1BEE7"]
service [label="Service", fillcolor="#CE93D8"]
prices [label="Prices", fillcolor="#BA68C8"]
discounts [label="Discounts", fillcolor="#BA68C8"]
}
// === ROW 3: LEFT - Cart ===
subgraph cluster_cart {
label="Cart & Checkout"
style="rounded,filled"
fillcolor="#E0F7FA"
color="#00838F"
cart [label="Cart", fillcolor="#B2EBF2"]
cartitem [label="CartItem", fillcolor="#80DEEA"]
cartresumeitem [label="CartResumeItem", fillcolor="#80DEEA"]
cartpetreason [label="CartPetReason", fillcolor="#80DEEA"]
}
// === ROW 3: CENTER - Requests ===
subgraph cluster_solicitudes {
label="Service Requests"
style="rounded,filled"
fillcolor="#FFEBEE"
color="#C62828"
servicerequest [label="ServiceRequest", fillcolor="#FFCDD2"]
statehistory [label="StateHistory", fillcolor="#EF9A9A"]
vetasked [label="VeterinarianAsked", fillcolor="#EF9A9A"]
vetreminder [label="VetReminder", fillcolor="#E57373"]
payreminder [label="PayReminder", fillcolor="#E57373"]
}
// === ROW 3: RIGHT - Visits ===
subgraph cluster_visits {
label="Veterinary Visits"
style="rounded,filled"
fillcolor="#FFFDE7"
color="#F9A825"
vetvisit [label="VetVisit", fillcolor="#FFF9C4"]
vetvisitreport [label="VetVisitReport", fillcolor="#FFF59D"]
vetvisitfollowup [label="FollowUp", fillcolor="#FFF176"]
vetvisitpetreason [label="VisitPetReason", fillcolor="#FFF176"]
visit_pets [label="Visit-Pets", shape=diamond, fillcolor="#FFEE58"]
receipt [label="Receipt\n(AFIP)", fillcolor="#F8BBD9"]
}
// === ROW 4: REFERENCE DATA (bottom, full width) ===
subgraph cluster_reference {
label="Reference Data"
style="rounded,filled"
fillcolor="#ECEFF1"
color="#455A64"
subgraph {
rank=same
specialty [label="Specialty", fillcolor="#CFD8DC"]
neighborhood [label="Neighborhood", fillcolor="#CFD8DC"]
province [label="Province", fillcolor="#CFD8DC"]
locality [label="Locality", fillcolor="#CFD8DC"]
petbreed [label="PetBreed", fillcolor="#CFD8DC"]
vaccine [label="Vaccine", fillcolor="#CFD8DC"]
}
subgraph {
rank=same
study [label="Study", fillcolor="#CFD8DC"]
campaign [label="Campaign", fillcolor="#CFD8DC"]
tag [label="Tag", fillcolor="#CFD8DC"]
medication [label="Medication", fillcolor="#CFD8DC"]
turnfeegroup [label="TurnFeeGroup", fillcolor="#CFD8DC"]
}
specialty -> study [style=invis]
province -> locality
}
// === FORCE COLUMN ALIGNMENT WITH INVISIBLE EDGES ===
// Column 1: Pets -> Cart
petowner -> cart [style=invis, weight=10]
// Column 2: Vets -> Requests
veterinarian -> servicerequest [style=invis, weight=10]
// Column 3: Services -> Visits
service -> vetvisit [style=invis, weight=10]
// Force Reference Data to bottom
cart -> specialty [style=invis, weight=10]
servicerequest -> campaign [style=invis, weight=10]
vetvisit -> turnfeegroup [style=invis, weight=10]
// === INTERNAL CLUSTER EDGES ===
petowner -> pet [label="1:N"]
pet -> petvaccine [label="1:N"]
pet -> petstudy [label="1:N"]
veterinarian -> availability
veterinarian -> unavailability
veterinarian -> vet_specialty
veterinarian -> vet_neighborhood
grupo -> category [label="1:N"]
category -> service [label="1:N"]
service -> prices [label="1:N"]
service -> discounts [label="1:N"]
cart -> cartitem [label="1:N"]
cart -> cartresumeitem
cart -> cartpetreason
servicerequest -> statehistory [label="1:N"]
servicerequest -> vetasked [label="1:N"]
servicerequest -> payreminder
vetasked -> vetreminder
vetvisit -> visit_pets
vetvisit -> vetvisitreport [label="1:N"]
vetvisit -> vetvisitpetreason
vetvisitreport -> vetvisitfollowup
vetvisit -> receipt [style=dashed]
// === CROSS-CLUSTER RELATIONSHIPS ===
auth_user -> petowner [label="1:1 opt", style=dashed]
auth_user -> veterinarian [label="1:1"]
petowner -> cart [label="1:N"]
petowner -> servicerequest [label="1:N"]
servicerequest -> cart [label="1:1", constraint=false]
vetasked -> veterinarian [constraint=false]
vetvisit -> servicerequest [style=dashed, constraint=false]
// Reference links
petvaccine -> vaccine [constraint=false]
petstudy -> study [constraint=false]
vet_specialty -> specialty [constraint=false]
vet_neighborhood -> neighborhood [constraint=false]
cartitem -> service [constraint=false]
}

View File

@@ -0,0 +1,540 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: DataModelSimple Pages: 1 -->
<svg width="2433pt" height="780pt"
viewBox="0.00 0.00 2433.00 780.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 776.4)">
<title>DataModelSimple</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-776.4 2429,-776.4 2429,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="1212.5" y="-753.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Data Model Overview</text>
<g id="clust1" class="cluster">
<title>cluster_auth</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M890,-658C890,-658 975,-658 975,-658 981,-658 987,-664 987,-670 987,-670 987,-725.2 987,-725.2 987,-731.2 981,-737.2 975,-737.2 975,-737.2 890,-737.2 890,-737.2 884,-737.2 878,-731.2 878,-725.2 878,-725.2 878,-670 878,-670 878,-664 884,-658 890,-658"/>
<text xml:space="preserve" text-anchor="middle" x="932.5" y="-718" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Users &amp; Auth</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_mascotas</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M1662,-262.6C1662,-262.6 1799,-262.6 1799,-262.6 1805,-262.6 1811,-268.6 1811,-274.6 1811,-274.6 1811,-593.4 1811,-593.4 1811,-599.4 1805,-605.4 1799,-605.4 1799,-605.4 1662,-605.4 1662,-605.4 1656,-605.4 1650,-599.4 1650,-593.4 1650,-593.4 1650,-274.6 1650,-274.6 1650,-268.6 1656,-262.6 1662,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="1730.5" y="-586.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Pet Owners &amp; Pets</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_vets</title>
<path fill="#fff3e0" stroke="#e65100" d="M20,-262.6C20,-262.6 509,-262.6 509,-262.6 515,-262.6 521,-268.6 521,-274.6 521,-274.6 521,-461.6 521,-461.6 521,-467.6 515,-473.6 509,-473.6 509,-473.6 20,-473.6 20,-473.6 14,-473.6 8,-467.6 8,-461.6 8,-461.6 8,-274.6 8,-274.6 8,-268.6 14,-262.6 20,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="264.5" y="-454.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinarians</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_productos</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M2277,-262.6C2277,-262.6 2405,-262.6 2405,-262.6 2411,-262.6 2417,-268.6 2417,-274.6 2417,-274.6 2417,-725.2 2417,-725.2 2417,-731.2 2411,-737.2 2405,-737.2 2405,-737.2 2277,-737.2 2277,-737.2 2271,-737.2 2265,-731.2 2265,-725.2 2265,-725.2 2265,-274.6 2265,-274.6 2265,-268.6 2271,-262.6 2277,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="2341" y="-718" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services &amp; Pricing</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_cart</title>
<path fill="#e0f7fa" stroke="#00838f" d="M853,-262.6C853,-262.6 1119,-262.6 1119,-262.6 1125,-262.6 1131,-268.6 1131,-274.6 1131,-274.6 1131,-461.6 1131,-461.6 1131,-467.6 1125,-473.6 1119,-473.6 1119,-473.6 853,-473.6 853,-473.6 847,-473.6 841,-467.6 841,-461.6 841,-461.6 841,-274.6 841,-274.6 841,-268.6 847,-262.6 853,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="986" y="-454.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Cart &amp; Checkout</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_solicitudes</title>
<path fill="#ffebee" stroke="#c62828" d="M541,-8C541,-8 821,-8 821,-8 827,-8 833,-14 833,-20 833,-20 833,-329.8 833,-329.8 833,-335.8 827,-341.8 821,-341.8 821,-341.8 541,-341.8 541,-341.8 535,-341.8 529,-335.8 529,-329.8 529,-329.8 529,-20 529,-20 529,-14 535,-8 541,-8"/>
<text xml:space="preserve" text-anchor="middle" x="681" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Service Requests</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_visits</title>
<path fill="#fffde7" stroke="#f9a825" d="M1869,-8C1869,-8 2245,-8 2245,-8 2251,-8 2257,-14 2257,-20 2257,-20 2257,-329.8 2257,-329.8 2257,-335.8 2251,-341.8 2245,-341.8 2245,-341.8 1869,-341.8 1869,-341.8 1863,-341.8 1857,-335.8 1857,-329.8 1857,-329.8 1857,-20 1857,-20 1857,-14 1863,-8 1869,-8"/>
<text xml:space="preserve" text-anchor="middle" x="2057" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinary Visits</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_reference</title>
<path fill="#eceff1" stroke="#455a64" d="M1151,-131C1151,-131 1630,-131 1630,-131 1636,-131 1642,-137 1642,-143 1642,-143 1642,-329.8 1642,-329.8 1642,-335.8 1636,-341.8 1630,-341.8 1630,-341.8 1151,-341.8 1151,-341.8 1145,-341.8 1139,-335.8 1139,-329.8 1139,-329.8 1139,-143 1139,-143 1139,-137 1145,-131 1151,-131"/>
<text xml:space="preserve" text-anchor="middle" x="1390.5" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reference Data</text>
</g>
<!-- auth_user -->
<g id="node1" class="node">
<title>auth_user</title>
<path fill="#c8e6c9" stroke="black" d="M949.68,-702C949.68,-702 914.32,-702 914.32,-702 908.32,-702 902.32,-696 902.32,-690 902.32,-690 902.32,-678 902.32,-678 902.32,-672 908.32,-666 914.32,-666 914.32,-666 949.68,-666 949.68,-666 955.68,-666 961.68,-672 961.68,-678 961.68,-678 961.68,-690 961.68,-690 961.68,-696 955.68,-702 949.68,-702"/>
<text xml:space="preserve" text-anchor="middle" x="932" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00">auth.User</text>
</g>
<!-- petowner -->
<g id="node2" class="node">
<title>petowner</title>
<path fill="#bbdefb" stroke="black" d="M1706.23,-570.2C1706.23,-570.2 1669.77,-570.2 1669.77,-570.2 1663.77,-570.2 1657.77,-564.2 1657.77,-558.2 1657.77,-558.2 1657.77,-546.2 1657.77,-546.2 1657.77,-540.2 1663.77,-534.2 1669.77,-534.2 1669.77,-534.2 1706.23,-534.2 1706.23,-534.2 1712.23,-534.2 1718.23,-540.2 1718.23,-546.2 1718.23,-546.2 1718.23,-558.2 1718.23,-558.2 1718.23,-564.2 1712.23,-570.2 1706.23,-570.2"/>
<text xml:space="preserve" text-anchor="middle" x="1688" y="-549.2" font-family="Helvetica,sans-Serif" font-size="10.00">PetOwner</text>
</g>
<!-- auth_user&#45;&gt;petowner -->
<g id="edge32" class="edge">
<title>auth_user&#45;&gt;petowner</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M943.44,-665.62C943.44,-631.77 943.44,-563 943.44,-563 943.44,-563 1645.91,-563 1645.91,-563"/>
<polygon fill="black" stroke="black" points="1645.91,-566.5 1655.91,-563 1645.91,-559.5 1645.91,-566.5"/>
<text xml:space="preserve" text-anchor="middle" x="1213.23" y="-615.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
</g>
<!-- veterinarian -->
<g id="node6" class="node">
<title>veterinarian</title>
<path fill="#ffe0b2" stroke="black" d="M500.96,-438.4C500.96,-438.4 455.04,-438.4 455.04,-438.4 449.04,-438.4 443.04,-432.4 443.04,-426.4 443.04,-426.4 443.04,-414.4 443.04,-414.4 443.04,-408.4 449.04,-402.4 455.04,-402.4 455.04,-402.4 500.96,-402.4 500.96,-402.4 506.96,-402.4 512.96,-408.4 512.96,-414.4 512.96,-414.4 512.96,-426.4 512.96,-426.4 512.96,-432.4 506.96,-438.4 500.96,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="478" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Veterinarian</text>
</g>
<!-- auth_user&#45;&gt;veterinarian -->
<g id="edge33" class="edge">
<title>auth_user&#45;&gt;veterinarian</title>
<path fill="none" stroke="black" d="M914.19,-665.54C914.19,-607.58 914.19,-433 914.19,-433 914.19,-433 524.84,-433 524.84,-433"/>
<polygon fill="black" stroke="black" points="524.84,-429.5 514.84,-433 524.84,-436.5 524.84,-429.5"/>
<text xml:space="preserve" text-anchor="middle" x="667.56" y="-549.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- pet -->
<g id="node3" class="node">
<title>pet</title>
<path fill="#bbdefb" stroke="black" d="M1706,-438.4C1706,-438.4 1676,-438.4 1676,-438.4 1670,-438.4 1664,-432.4 1664,-426.4 1664,-426.4 1664,-414.4 1664,-414.4 1664,-408.4 1670,-402.4 1676,-402.4 1676,-402.4 1706,-402.4 1706,-402.4 1712,-402.4 1718,-408.4 1718,-414.4 1718,-414.4 1718,-426.4 1718,-426.4 1718,-432.4 1712,-438.4 1706,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="1691" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Pet</text>
</g>
<!-- petowner&#45;&gt;pet -->
<g id="edge9" class="edge">
<title>petowner&#45;&gt;pet</title>
<path fill="none" stroke="black" d="M1691,-533.87C1691,-533.87 1691,-450.08 1691,-450.08"/>
<polygon fill="black" stroke="black" points="1694.5,-450.08 1691,-440.08 1687.5,-450.08 1694.5,-450.08"/>
<text xml:space="preserve" text-anchor="middle" x="1696.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cart -->
<g id="node16" class="node">
<title>cart</title>
<path fill="#b2ebf2" stroke="black" d="M1111,-438.4C1111,-438.4 1081,-438.4 1081,-438.4 1075,-438.4 1069,-432.4 1069,-426.4 1069,-426.4 1069,-414.4 1069,-414.4 1069,-408.4 1075,-402.4 1081,-402.4 1081,-402.4 1111,-402.4 1111,-402.4 1117,-402.4 1123,-408.4 1123,-414.4 1123,-414.4 1123,-426.4 1123,-426.4 1123,-432.4 1117,-438.4 1111,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="1096" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Cart</text>
</g>
<!-- petowner&#45;&gt;cart -->
<!-- petowner&#45;&gt;cart -->
<g id="edge34" class="edge">
<title>petowner&#45;&gt;cart</title>
<path fill="none" stroke="black" d="M1657.48,-541C1537.52,-541 1105,-541 1105,-541 1105,-541 1105,-450.23 1105,-450.23"/>
<polygon fill="black" stroke="black" points="1108.5,-450.23 1105,-440.23 1101.5,-450.23 1108.5,-450.23"/>
<text xml:space="preserve" text-anchor="middle" x="1233.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- servicerequest -->
<g id="node20" class="node">
<title>servicerequest</title>
<path fill="#ffcdd2" stroke="black" d="M763.3,-306.6C763.3,-306.6 700.7,-306.6 700.7,-306.6 694.7,-306.6 688.7,-300.6 688.7,-294.6 688.7,-294.6 688.7,-282.6 688.7,-282.6 688.7,-276.6 694.7,-270.6 700.7,-270.6 700.7,-270.6 763.3,-270.6 763.3,-270.6 769.3,-270.6 775.3,-276.6 775.3,-282.6 775.3,-282.6 775.3,-294.6 775.3,-294.6 775.3,-300.6 769.3,-306.6 763.3,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="732" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceRequest</text>
</g>
<!-- petowner&#45;&gt;servicerequest -->
<g id="edge35" class="edge">
<title>petowner&#45;&gt;servicerequest</title>
<path fill="none" stroke="black" d="M1657.27,-556C1493.64,-556 732,-556 732,-556 732,-556 732,-318.49 732,-318.49"/>
<polygon fill="black" stroke="black" points="735.5,-318.49 732,-308.49 728.5,-318.49 735.5,-318.49"/>
<text xml:space="preserve" text-anchor="middle" x="782.22" y="-418" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petvaccine -->
<g id="node4" class="node">
<title>petvaccine</title>
<path fill="#90caf9" stroke="black" d="M1712.29,-306.6C1712.29,-306.6 1669.71,-306.6 1669.71,-306.6 1663.71,-306.6 1657.71,-300.6 1657.71,-294.6 1657.71,-294.6 1657.71,-282.6 1657.71,-282.6 1657.71,-276.6 1663.71,-270.6 1669.71,-270.6 1669.71,-270.6 1712.29,-270.6 1712.29,-270.6 1718.29,-270.6 1724.29,-276.6 1724.29,-282.6 1724.29,-282.6 1724.29,-294.6 1724.29,-294.6 1724.29,-300.6 1718.29,-306.6 1712.29,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1691" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetVaccine</text>
</g>
<!-- pet&#45;&gt;petvaccine -->
<g id="edge10" class="edge">
<title>pet&#45;&gt;petvaccine</title>
<path fill="none" stroke="black" d="M1691,-402.07C1691,-402.07 1691,-318.28 1691,-318.28"/>
<polygon fill="black" stroke="black" points="1694.5,-318.28 1691,-308.28 1687.5,-318.28 1694.5,-318.28"/>
<text xml:space="preserve" text-anchor="middle" x="1697.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petstudy -->
<g id="node5" class="node">
<title>petstudy</title>
<path fill="#90caf9" stroke="black" d="M1791.29,-306.6C1791.29,-306.6 1758.71,-306.6 1758.71,-306.6 1752.71,-306.6 1746.71,-300.6 1746.71,-294.6 1746.71,-294.6 1746.71,-282.6 1746.71,-282.6 1746.71,-276.6 1752.71,-270.6 1758.71,-270.6 1758.71,-270.6 1791.29,-270.6 1791.29,-270.6 1797.29,-270.6 1803.29,-276.6 1803.29,-282.6 1803.29,-282.6 1803.29,-294.6 1803.29,-294.6 1803.29,-300.6 1797.29,-306.6 1791.29,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1775" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetStudy</text>
</g>
<!-- pet&#45;&gt;petstudy -->
<g id="edge11" class="edge">
<title>pet&#45;&gt;petstudy</title>
<path fill="none" stroke="black" d="M1718.04,-414C1742.6,-414 1775,-414 1775,-414 1775,-414 1775,-318.35 1775,-318.35"/>
<polygon fill="black" stroke="black" points="1778.5,-318.35 1775,-308.35 1771.5,-318.35 1778.5,-318.35"/>
<text xml:space="preserve" text-anchor="middle" x="1758.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vaccine -->
<g id="node36" class="node">
<title>vaccine</title>
<path fill="#cfd8dc" stroke="black" d="M1189,-306.6C1189,-306.6 1159,-306.6 1159,-306.6 1153,-306.6 1147,-300.6 1147,-294.6 1147,-294.6 1147,-282.6 1147,-282.6 1147,-276.6 1153,-270.6 1159,-270.6 1159,-270.6 1189,-270.6 1189,-270.6 1195,-270.6 1201,-276.6 1201,-282.6 1201,-282.6 1201,-294.6 1201,-294.6 1201,-300.6 1195,-306.6 1189,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1174" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vaccine</text>
</g>
<!-- petvaccine&#45;&gt;vaccine -->
<g id="edge39" class="edge">
<title>petvaccine&#45;&gt;vaccine</title>
<path fill="none" stroke="black" d="M1691,-270.32C1691,-257.63 1691,-243 1691,-243 1691,-243 1174.1,-243 1174.1,-243 1174.1,-243 1174.1,-258.8 1174.1,-258.8"/>
<polygon fill="black" stroke="black" points="1170.6,-258.8 1174.1,-268.8 1177.6,-258.8 1170.6,-258.8"/>
</g>
<!-- study -->
<g id="node37" class="node">
<title>study</title>
<path fill="#cfd8dc" stroke="black" d="M1356,-175C1356,-175 1326,-175 1326,-175 1320,-175 1314,-169 1314,-163 1314,-163 1314,-151 1314,-151 1314,-145 1320,-139 1326,-139 1326,-139 1356,-139 1356,-139 1362,-139 1368,-145 1368,-151 1368,-151 1368,-163 1368,-163 1368,-169 1362,-175 1356,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1341" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Study</text>
</g>
<!-- petstudy&#45;&gt;study -->
<g id="edge40" class="edge">
<title>petstudy&#45;&gt;study</title>
<path fill="none" stroke="black" d="M1775,-270.23C1775,-244.81 1775,-202 1775,-202 1775,-202 1353.18,-202 1353.18,-202 1353.18,-202 1353.18,-186.82 1353.18,-186.82"/>
<polygon fill="black" stroke="black" points="1356.68,-186.82 1353.18,-176.82 1349.68,-186.82 1356.68,-186.82"/>
</g>
<!-- availability -->
<g id="node7" class="node">
<title>availability</title>
<path fill="#ffcc80" stroke="black" d="M67.62,-306.6C67.62,-306.6 28.38,-306.6 28.38,-306.6 22.38,-306.6 16.38,-300.6 16.38,-294.6 16.38,-294.6 16.38,-282.6 16.38,-282.6 16.38,-276.6 22.38,-270.6 28.38,-270.6 28.38,-270.6 67.62,-270.6 67.62,-270.6 73.62,-270.6 79.62,-276.6 79.62,-282.6 79.62,-282.6 79.62,-294.6 79.62,-294.6 79.62,-300.6 73.62,-306.6 67.62,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="48" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Availability</text>
</g>
<!-- veterinarian&#45;&gt;availability -->
<g id="edge12" class="edge">
<title>veterinarian&#45;&gt;availability</title>
<path fill="none" stroke="black" d="M442.81,-429C340.11,-429 48,-429 48,-429 48,-429 48,-318.47 48,-318.47"/>
<polygon fill="black" stroke="black" points="51.5,-318.47 48,-308.47 44.5,-318.47 51.5,-318.47"/>
</g>
<!-- unavailability -->
<g id="node8" class="node">
<title>unavailability</title>
<path fill="#ffcc80" stroke="black" d="M164.46,-306.6C164.46,-306.6 113.54,-306.6 113.54,-306.6 107.54,-306.6 101.54,-300.6 101.54,-294.6 101.54,-294.6 101.54,-282.6 101.54,-282.6 101.54,-276.6 107.54,-270.6 113.54,-270.6 113.54,-270.6 164.46,-270.6 164.46,-270.6 170.46,-270.6 176.46,-276.6 176.46,-282.6 176.46,-282.6 176.46,-294.6 176.46,-294.6 176.46,-300.6 170.46,-306.6 164.46,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="139" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Unavailability</text>
</g>
<!-- veterinarian&#45;&gt;unavailability -->
<g id="edge13" class="edge">
<title>veterinarian&#45;&gt;unavailability</title>
<path fill="none" stroke="black" d="M442.68,-420C355.85,-420 139,-420 139,-420 139,-420 139,-318.38 139,-318.38"/>
<polygon fill="black" stroke="black" points="142.5,-318.38 139,-308.38 135.5,-318.38 142.5,-318.38"/>
</g>
<!-- vet_specialty -->
<g id="node9" class="node">
<title>vet_specialty</title>
<path fill="#ffb74d" stroke="black" d="M250.45,-303.34C250.45,-303.34 209.84,-291.86 209.84,-291.86 204.06,-290.23 204.06,-286.97 209.84,-285.34 209.84,-285.34 250.45,-273.86 250.45,-273.86 256.23,-272.23 267.77,-272.23 273.55,-273.86 273.55,-273.86 314.16,-285.34 314.16,-285.34 319.94,-286.97 319.94,-290.23 314.16,-291.86 314.16,-291.86 273.55,-303.34 273.55,-303.34 267.77,-304.97 256.23,-304.97 250.45,-303.34"/>
<text xml:space="preserve" text-anchor="middle" x="262" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vet&#45;Specialty</text>
</g>
<!-- veterinarian&#45;&gt;vet_specialty -->
<g id="edge14" class="edge">
<title>veterinarian&#45;&gt;vet_specialty</title>
<path fill="none" stroke="black" d="M442.82,-411C381.61,-411 262,-411 262,-411 262,-411 262,-318.44 262,-318.44"/>
<polygon fill="black" stroke="black" points="265.5,-318.44 262,-308.44 258.5,-318.44 265.5,-318.44"/>
</g>
<!-- vet_neighborhood -->
<g id="node10" class="node">
<title>vet_neighborhood</title>
<path fill="#ffb74d" stroke="black" d="M418.27,-304.05C418.27,-304.05 359.09,-291.15 359.09,-291.15 353.23,-289.88 353.23,-287.32 359.09,-286.05 359.09,-286.05 418.27,-273.15 418.27,-273.15 424.14,-271.88 435.86,-271.88 441.73,-273.15 441.73,-273.15 500.91,-286.05 500.91,-286.05 506.77,-287.32 506.77,-289.88 500.91,-291.15 500.91,-291.15 441.73,-304.05 441.73,-304.05 435.86,-305.32 424.14,-305.32 418.27,-304.05"/>
<text xml:space="preserve" text-anchor="middle" x="430" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vet&#45;Neighborhood</text>
</g>
<!-- veterinarian&#45;&gt;vet_neighborhood -->
<g id="edge15" class="edge">
<title>veterinarian&#45;&gt;vet_neighborhood</title>
<path fill="none" stroke="black" d="M466.24,-402.07C466.24,-402.07 466.24,-310.63 466.24,-310.63"/>
<polygon fill="black" stroke="black" points="469.74,-310.63 466.24,-300.63 462.74,-310.63 469.74,-310.63"/>
</g>
<!-- veterinarian&#45;&gt;servicerequest -->
<!-- specialty -->
<g id="node31" class="node">
<title>specialty</title>
<path fill="#cfd8dc" stroke="black" d="M1622.29,-306.6C1622.29,-306.6 1589.71,-306.6 1589.71,-306.6 1583.71,-306.6 1577.71,-300.6 1577.71,-294.6 1577.71,-294.6 1577.71,-282.6 1577.71,-282.6 1577.71,-276.6 1583.71,-270.6 1589.71,-270.6 1589.71,-270.6 1622.29,-270.6 1622.29,-270.6 1628.29,-270.6 1634.29,-276.6 1634.29,-282.6 1634.29,-282.6 1634.29,-294.6 1634.29,-294.6 1634.29,-300.6 1628.29,-306.6 1622.29,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1606" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Specialty</text>
</g>
<!-- vet_specialty&#45;&gt;specialty -->
<g id="edge41" class="edge">
<title>vet_specialty&#45;&gt;specialty</title>
<path fill="none" stroke="black" d="M262,-270.22C262,-263.27 262,-257 262,-257 262,-257 1591.73,-257 1591.73,-257 1591.73,-257 1591.73,-258.7 1591.73,-258.7"/>
<polygon fill="black" stroke="black" points="1588.23,-258.7 1591.73,-268.7 1595.23,-258.7 1588.23,-258.7"/>
</g>
<!-- neighborhood -->
<g id="node32" class="node">
<title>neighborhood</title>
<path fill="#cfd8dc" stroke="black" d="M1289.41,-306.6C1289.41,-306.6 1234.59,-306.6 1234.59,-306.6 1228.59,-306.6 1222.59,-300.6 1222.59,-294.6 1222.59,-294.6 1222.59,-282.6 1222.59,-282.6 1222.59,-276.6 1228.59,-270.6 1234.59,-270.6 1234.59,-270.6 1289.41,-270.6 1289.41,-270.6 1295.41,-270.6 1301.41,-276.6 1301.41,-282.6 1301.41,-282.6 1301.41,-294.6 1301.41,-294.6 1301.41,-300.6 1295.41,-306.6 1289.41,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1262" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Neighborhood</text>
</g>
<!-- vet_neighborhood&#45;&gt;neighborhood -->
<g id="edge42" class="edge">
<title>vet_neighborhood&#45;&gt;neighborhood</title>
<path fill="none" stroke="black" d="M489.44,-293.99C489.44,-307 489.44,-339 489.44,-339 489.44,-339 1262,-339 1262,-339 1262,-339 1262,-318.59 1262,-318.59"/>
<polygon fill="black" stroke="black" points="1265.5,-318.59 1262,-308.59 1258.5,-318.59 1265.5,-318.59"/>
</g>
<!-- grupo -->
<g id="node11" class="node">
<title>grupo</title>
<path fill="#e1bee7" stroke="black" d="M2316,-702C2316,-702 2286,-702 2286,-702 2280,-702 2274,-696 2274,-690 2274,-690 2274,-678 2274,-678 2274,-672 2280,-666 2286,-666 2286,-666 2316,-666 2316,-666 2322,-666 2328,-672 2328,-678 2328,-678 2328,-690 2328,-690 2328,-696 2322,-702 2316,-702"/>
<text xml:space="preserve" text-anchor="middle" x="2301" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00">Group</text>
</g>
<!-- category -->
<g id="node12" class="node">
<title>category</title>
<path fill="#e1bee7" stroke="black" d="M2317.29,-570.2C2317.29,-570.2 2284.71,-570.2 2284.71,-570.2 2278.71,-570.2 2272.71,-564.2 2272.71,-558.2 2272.71,-558.2 2272.71,-546.2 2272.71,-546.2 2272.71,-540.2 2278.71,-534.2 2284.71,-534.2 2284.71,-534.2 2317.29,-534.2 2317.29,-534.2 2323.29,-534.2 2329.29,-540.2 2329.29,-546.2 2329.29,-546.2 2329.29,-558.2 2329.29,-558.2 2329.29,-564.2 2323.29,-570.2 2317.29,-570.2"/>
<text xml:space="preserve" text-anchor="middle" x="2301" y="-549.2" font-family="Helvetica,sans-Serif" font-size="10.00">Category</text>
</g>
<!-- grupo&#45;&gt;category -->
<g id="edge16" class="edge">
<title>grupo&#45;&gt;category</title>
<path fill="none" stroke="black" d="M2301,-665.67C2301,-665.67 2301,-581.88 2301,-581.88"/>
<polygon fill="black" stroke="black" points="2304.5,-581.88 2301,-571.88 2297.5,-581.88 2304.5,-581.88"/>
<text xml:space="preserve" text-anchor="middle" x="2307.22" y="-615.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- service -->
<g id="node13" class="node">
<title>service</title>
<path fill="#ce93d8" stroke="black" d="M2315,-438.4C2315,-438.4 2285,-438.4 2285,-438.4 2279,-438.4 2273,-432.4 2273,-426.4 2273,-426.4 2273,-414.4 2273,-414.4 2273,-408.4 2279,-402.4 2285,-402.4 2285,-402.4 2315,-402.4 2315,-402.4 2321,-402.4 2327,-408.4 2327,-414.4 2327,-414.4 2327,-426.4 2327,-426.4 2327,-432.4 2321,-438.4 2315,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="2300" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
</g>
<!-- category&#45;&gt;service -->
<g id="edge17" class="edge">
<title>category&#45;&gt;service</title>
<path fill="none" stroke="black" d="M2300,-533.87C2300,-533.87 2300,-450.08 2300,-450.08"/>
<polygon fill="black" stroke="black" points="2303.5,-450.08 2300,-440.08 2296.5,-450.08 2303.5,-450.08"/>
<text xml:space="preserve" text-anchor="middle" x="2306.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- prices -->
<g id="node14" class="node">
<title>prices</title>
<path fill="#ba68c8" stroke="black" d="M2315,-306.6C2315,-306.6 2285,-306.6 2285,-306.6 2279,-306.6 2273,-300.6 2273,-294.6 2273,-294.6 2273,-282.6 2273,-282.6 2273,-276.6 2279,-270.6 2285,-270.6 2285,-270.6 2315,-270.6 2315,-270.6 2321,-270.6 2327,-276.6 2327,-282.6 2327,-282.6 2327,-294.6 2327,-294.6 2327,-300.6 2321,-306.6 2315,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="2300" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Prices</text>
</g>
<!-- service&#45;&gt;prices -->
<g id="edge18" class="edge">
<title>service&#45;&gt;prices</title>
<path fill="none" stroke="black" d="M2309,-402.07C2309,-402.07 2309,-318.28 2309,-318.28"/>
<polygon fill="black" stroke="black" points="2312.5,-318.28 2309,-308.28 2305.5,-318.28 2312.5,-318.28"/>
<text xml:space="preserve" text-anchor="middle" x="2306.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- discounts -->
<g id="node15" class="node">
<title>discounts</title>
<path fill="#ba68c8" stroke="black" d="M2396.95,-306.6C2396.95,-306.6 2361.05,-306.6 2361.05,-306.6 2355.05,-306.6 2349.05,-300.6 2349.05,-294.6 2349.05,-294.6 2349.05,-282.6 2349.05,-282.6 2349.05,-276.6 2355.05,-270.6 2361.05,-270.6 2361.05,-270.6 2396.95,-270.6 2396.95,-270.6 2402.95,-270.6 2408.95,-276.6 2408.95,-282.6 2408.95,-282.6 2408.95,-294.6 2408.95,-294.6 2408.95,-300.6 2402.95,-306.6 2396.95,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="2379" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Discounts</text>
</g>
<!-- service&#45;&gt;discounts -->
<g id="edge19" class="edge">
<title>service&#45;&gt;discounts</title>
<path fill="none" stroke="black" d="M2327.19,-420C2350.04,-420 2379,-420 2379,-420 2379,-420 2379,-318.38 2379,-318.38"/>
<polygon fill="black" stroke="black" points="2382.5,-318.38 2379,-308.38 2375.5,-318.38 2382.5,-318.38"/>
<text xml:space="preserve" text-anchor="middle" x="2365.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisit -->
<g id="node25" class="node">
<title>vetvisit</title>
<path fill="#fff9c4" stroke="black" d="M2081,-306.6C2081,-306.6 2051,-306.6 2051,-306.6 2045,-306.6 2039,-300.6 2039,-294.6 2039,-294.6 2039,-282.6 2039,-282.6 2039,-276.6 2045,-270.6 2051,-270.6 2051,-270.6 2081,-270.6 2081,-270.6 2087,-270.6 2093,-276.6 2093,-282.6 2093,-282.6 2093,-294.6 2093,-294.6 2093,-300.6 2087,-306.6 2081,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="2066" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisit</text>
</g>
<!-- service&#45;&gt;vetvisit -->
<!-- cartitem -->
<g id="node17" class="node">
<title>cartitem</title>
<path fill="#80deea" stroke="black" d="M891.17,-306.6C891.17,-306.6 860.83,-306.6 860.83,-306.6 854.83,-306.6 848.83,-300.6 848.83,-294.6 848.83,-294.6 848.83,-282.6 848.83,-282.6 848.83,-276.6 854.83,-270.6 860.83,-270.6 860.83,-270.6 891.17,-270.6 891.17,-270.6 897.17,-270.6 903.17,-276.6 903.17,-282.6 903.17,-282.6 903.17,-294.6 903.17,-294.6 903.17,-300.6 897.17,-306.6 891.17,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="876" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartItem</text>
</g>
<!-- cart&#45;&gt;cartitem -->
<g id="edge20" class="edge">
<title>cart&#45;&gt;cartitem</title>
<path fill="none" stroke="black" d="M1068.78,-413C1011.66,-413 884.49,-413 884.49,-413 884.49,-413 884.49,-318.47 884.49,-318.47"/>
<polygon fill="black" stroke="black" points="887.99,-318.47 884.49,-308.47 880.99,-318.47 887.99,-318.47"/>
<text xml:space="preserve" text-anchor="middle" x="924.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartresumeitem -->
<g id="node18" class="node">
<title>cartresumeitem</title>
<path fill="#80deea" stroke="black" d="M1004.79,-306.6C1004.79,-306.6 937.21,-306.6 937.21,-306.6 931.21,-306.6 925.21,-300.6 925.21,-294.6 925.21,-294.6 925.21,-282.6 925.21,-282.6 925.21,-276.6 931.21,-270.6 937.21,-270.6 937.21,-270.6 1004.79,-270.6 1004.79,-270.6 1010.79,-270.6 1016.79,-276.6 1016.79,-282.6 1016.79,-282.6 1016.79,-294.6 1016.79,-294.6 1016.79,-300.6 1010.79,-306.6 1004.79,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="971" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartResumeItem</text>
</g>
<!-- cart&#45;&gt;cartresumeitem -->
<g id="edge21" class="edge">
<title>cart&#45;&gt;cartresumeitem</title>
<path fill="none" stroke="black" d="M1068.5,-408C1036.87,-408 989.23,-408 989.23,-408 989.23,-408 989.23,-318.51 989.23,-318.51"/>
<polygon fill="black" stroke="black" points="992.73,-318.51 989.23,-308.51 985.73,-318.51 992.73,-318.51"/>
</g>
<!-- cartpetreason -->
<g id="node19" class="node">
<title>cartpetreason</title>
<path fill="#80deea" stroke="black" d="M1111.18,-306.6C1111.18,-306.6 1050.82,-306.6 1050.82,-306.6 1044.82,-306.6 1038.82,-300.6 1038.82,-294.6 1038.82,-294.6 1038.82,-282.6 1038.82,-282.6 1038.82,-276.6 1044.82,-270.6 1050.82,-270.6 1050.82,-270.6 1111.18,-270.6 1111.18,-270.6 1117.18,-270.6 1123.18,-276.6 1123.18,-282.6 1123.18,-282.6 1123.18,-294.6 1123.18,-294.6 1123.18,-300.6 1117.18,-306.6 1111.18,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1081" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartPetReason</text>
</g>
<!-- cart&#45;&gt;cartpetreason -->
<g id="edge22" class="edge">
<title>cart&#45;&gt;cartpetreason</title>
<path fill="none" stroke="black" d="M1096,-402.07C1096,-402.07 1096,-318.28 1096,-318.28"/>
<polygon fill="black" stroke="black" points="1099.5,-318.28 1096,-308.28 1092.5,-318.28 1099.5,-318.28"/>
</g>
<!-- cart&#45;&gt;specialty -->
<!-- cartitem&#45;&gt;service -->
<g id="edge43" class="edge">
<title>cartitem&#45;&gt;service</title>
<path fill="none" stroke="black" d="M866.66,-307.07C866.66,-331.09 866.66,-370 866.66,-370 866.66,-370 2291,-370 2291,-370 2291,-370 2291,-390.41 2291,-390.41"/>
<polygon fill="black" stroke="black" points="2287.5,-390.41 2291,-400.41 2294.5,-390.41 2287.5,-390.41"/>
</g>
<!-- servicerequest&#45;&gt;cart -->
<g id="edge36" class="edge">
<title>servicerequest&#45;&gt;cart</title>
<path fill="none" stroke="black" d="M753.65,-306.82C753.65,-344.61 753.65,-428 753.65,-428 753.65,-428 1057.05,-428 1057.05,-428"/>
<polygon fill="black" stroke="black" points="1057.05,-431.5 1067.05,-428 1057.05,-424.5 1057.05,-431.5"/>
<text xml:space="preserve" text-anchor="middle" x="1349.56" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- statehistory -->
<g id="node21" class="node">
<title>statehistory</title>
<path fill="#ef9a9a" stroke="black" d="M595.23,-175C595.23,-175 548.77,-175 548.77,-175 542.77,-175 536.77,-169 536.77,-163 536.77,-163 536.77,-151 536.77,-151 536.77,-145 542.77,-139 548.77,-139 548.77,-139 595.23,-139 595.23,-139 601.23,-139 607.23,-145 607.23,-151 607.23,-151 607.23,-163 607.23,-163 607.23,-169 601.23,-175 595.23,-175"/>
<text xml:space="preserve" text-anchor="middle" x="572" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">StateHistory</text>
</g>
<!-- servicerequest&#45;&gt;statehistory -->
<g id="edge23" class="edge">
<title>servicerequest&#45;&gt;statehistory</title>
<path fill="none" stroke="black" d="M688.28,-289C640.91,-289 572,-289 572,-289 572,-289 572,-186.87 572,-186.87"/>
<polygon fill="black" stroke="black" points="575.5,-186.87 572,-176.87 568.5,-186.87 575.5,-186.87"/>
<text xml:space="preserve" text-anchor="middle" x="635.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetasked -->
<g id="node22" class="node">
<title>vetasked</title>
<path fill="#ef9a9a" stroke="black" d="M714.86,-175C714.86,-175 641.14,-175 641.14,-175 635.14,-175 629.14,-169 629.14,-163 629.14,-163 629.14,-151 629.14,-151 629.14,-145 635.14,-139 641.14,-139 641.14,-139 714.86,-139 714.86,-139 720.86,-139 726.86,-145 726.86,-151 726.86,-151 726.86,-163 726.86,-163 726.86,-169 720.86,-175 714.86,-175"/>
<text xml:space="preserve" text-anchor="middle" x="678" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VeterinarianAsked</text>
</g>
<!-- servicerequest&#45;&gt;vetasked -->
<g id="edge24" class="edge">
<title>servicerequest&#45;&gt;vetasked</title>
<path fill="none" stroke="black" d="M707.78,-270.3C707.78,-270.3 707.78,-186.66 707.78,-186.66"/>
<polygon fill="black" stroke="black" points="711.28,-186.66 707.78,-176.66 704.28,-186.66 711.28,-186.66"/>
<text xml:space="preserve" text-anchor="middle" x="707.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- payreminder -->
<g id="node24" class="node">
<title>payreminder</title>
<path fill="#e57373" stroke="black" d="M813.29,-175C813.29,-175 760.71,-175 760.71,-175 754.71,-175 748.71,-169 748.71,-163 748.71,-163 748.71,-151 748.71,-151 748.71,-145 754.71,-139 760.71,-139 760.71,-139 813.29,-139 813.29,-139 819.29,-139 825.29,-145 825.29,-151 825.29,-151 825.29,-163 825.29,-163 825.29,-169 819.29,-175 813.29,-175"/>
<text xml:space="preserve" text-anchor="middle" x="787" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">PayReminder</text>
</g>
<!-- servicerequest&#45;&gt;payreminder -->
<g id="edge25" class="edge">
<title>servicerequest&#45;&gt;payreminder</title>
<path fill="none" stroke="black" d="M757.57,-270.3C757.57,-270.3 757.57,-186.66 757.57,-186.66"/>
<polygon fill="black" stroke="black" points="761.07,-186.66 757.57,-176.66 754.07,-186.66 761.07,-186.66"/>
</g>
<!-- campaign -->
<g id="node38" class="node">
<title>campaign</title>
<path fill="#cfd8dc" stroke="black" d="M1196.79,-175C1196.79,-175 1159.21,-175 1159.21,-175 1153.21,-175 1147.21,-169 1147.21,-163 1147.21,-163 1147.21,-151 1147.21,-151 1147.21,-145 1153.21,-139 1159.21,-139 1159.21,-139 1196.79,-139 1196.79,-139 1202.79,-139 1208.79,-145 1208.79,-151 1208.79,-151 1208.79,-163 1208.79,-163 1208.79,-169 1202.79,-175 1196.79,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1178" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Campaign</text>
</g>
<!-- servicerequest&#45;&gt;campaign -->
<!-- vetasked&#45;&gt;veterinarian -->
<g id="edge37" class="edge">
<title>vetasked&#45;&gt;veterinarian</title>
<path fill="none" stroke="black" d="M658.92,-175.35C658.92,-234.76 658.92,-418 658.92,-418 658.92,-418 524.83,-418 524.83,-418"/>
<polygon fill="black" stroke="black" points="524.83,-414.5 514.83,-418 524.83,-421.5 524.83,-414.5"/>
</g>
<!-- vetreminder -->
<g id="node23" class="node">
<title>vetreminder</title>
<path fill="#e57373" stroke="black" d="M703.18,-52C703.18,-52 652.82,-52 652.82,-52 646.82,-52 640.82,-46 640.82,-40 640.82,-40 640.82,-28 640.82,-28 640.82,-22 646.82,-16 652.82,-16 652.82,-16 703.18,-16 703.18,-16 709.18,-16 715.18,-22 715.18,-28 715.18,-28 715.18,-40 715.18,-40 715.18,-46 709.18,-52 703.18,-52"/>
<text xml:space="preserve" text-anchor="middle" x="678" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00">VetReminder</text>
</g>
<!-- vetasked&#45;&gt;vetreminder -->
<g id="edge26" class="edge">
<title>vetasked&#45;&gt;vetreminder</title>
<path fill="none" stroke="black" d="M678,-138.59C678,-138.59 678,-63.65 678,-63.65"/>
<polygon fill="black" stroke="black" points="681.5,-63.65 678,-53.65 674.5,-63.65 681.5,-63.65"/>
</g>
<!-- vetvisit&#45;&gt;servicerequest -->
<g id="edge38" class="edge">
<title>vetvisit&#45;&gt;servicerequest</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M2042.05,-270.38C2042.05,-253.18 2042.05,-230 2042.05,-230 2042.05,-230 766.43,-230 766.43,-230 766.43,-230 766.43,-258.87 766.43,-258.87"/>
<polygon fill="black" stroke="black" points="762.93,-258.87 766.43,-268.87 769.93,-258.87 762.93,-258.87"/>
</g>
<!-- vetvisitreport -->
<g id="node26" class="node">
<title>vetvisitreport</title>
<path fill="#fff59d" stroke="black" d="M1932.96,-175C1932.96,-175 1877.04,-175 1877.04,-175 1871.04,-175 1865.04,-169 1865.04,-163 1865.04,-163 1865.04,-151 1865.04,-151 1865.04,-145 1871.04,-139 1877.04,-139 1877.04,-139 1932.96,-139 1932.96,-139 1938.96,-139 1944.96,-145 1944.96,-151 1944.96,-151 1944.96,-163 1944.96,-163 1944.96,-169 1938.96,-175 1932.96,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1905" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitReport</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitreport -->
<g id="edge28" class="edge">
<title>vetvisit&#45;&gt;vetvisitreport</title>
<path fill="none" stroke="black" d="M2038.69,-289C1992.75,-289 1905,-289 1905,-289 1905,-289 1905,-186.87 1905,-186.87"/>
<polygon fill="black" stroke="black" points="1908.5,-186.87 1905,-176.87 1901.5,-186.87 1908.5,-186.87"/>
<text xml:space="preserve" text-anchor="middle" x="1981.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisitpetreason -->
<g id="node28" class="node">
<title>vetvisitpetreason</title>
<path fill="#fff176" stroke="black" d="M2039.18,-175C2039.18,-175 1978.82,-175 1978.82,-175 1972.82,-175 1966.82,-169 1966.82,-163 1966.82,-163 1966.82,-151 1966.82,-151 1966.82,-145 1972.82,-139 1978.82,-139 1978.82,-139 2039.18,-139 2039.18,-139 2045.18,-139 2051.18,-145 2051.18,-151 2051.18,-151 2051.18,-163 2051.18,-163 2051.18,-169 2045.18,-175 2039.18,-175"/>
<text xml:space="preserve" text-anchor="middle" x="2009" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VisitPetReason</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitpetreason -->
<g id="edge29" class="edge">
<title>vetvisit&#45;&gt;vetvisitpetreason</title>
<path fill="none" stroke="black" d="M2048.14,-270.3C2048.14,-270.3 2048.14,-186.66 2048.14,-186.66"/>
<polygon fill="black" stroke="black" points="2051.64,-186.66 2048.14,-176.66 2044.64,-186.66 2051.64,-186.66"/>
</g>
<!-- visit_pets -->
<g id="node29" class="node">
<title>visit_pets</title>
<path fill="#ffee58" stroke="black" d="M2111.72,-170.9C2111.72,-170.9 2084.76,-161.1 2084.76,-161.1 2079.12,-159.05 2079.12,-154.95 2084.76,-152.9 2084.76,-152.9 2111.72,-143.1 2111.72,-143.1 2117.36,-141.05 2128.64,-141.05 2134.28,-143.1 2134.28,-143.1 2161.24,-152.9 2161.24,-152.9 2166.88,-154.95 2166.88,-159.05 2161.24,-161.1 2161.24,-161.1 2134.28,-170.9 2134.28,-170.9 2128.64,-172.95 2117.36,-172.95 2111.72,-170.9"/>
<text xml:space="preserve" text-anchor="middle" x="2123" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Visit&#45;Pets</text>
</g>
<!-- vetvisit&#45;&gt;visit_pets -->
<g id="edge27" class="edge">
<title>vetvisit&#45;&gt;visit_pets</title>
<path fill="none" stroke="black" d="M2083.24,-270.3C2083.24,-270.3 2083.24,-172.32 2083.24,-172.32"/>
<polygon fill="black" stroke="black" points="2086.74,-172.32 2083.24,-162.32 2079.74,-172.32 2086.74,-172.32"/>
</g>
<!-- receipt -->
<g id="node30" class="node">
<title>receipt</title>
<path fill="#f8bbd9" stroke="black" d="M2237,-175C2237,-175 2207,-175 2207,-175 2201,-175 2195,-169 2195,-163 2195,-163 2195,-151 2195,-151 2195,-145 2201,-139 2207,-139 2207,-139 2237,-139 2237,-139 2243,-139 2249,-145 2249,-151 2249,-151 2249,-163 2249,-163 2249,-169 2243,-175 2237,-175"/>
<text xml:space="preserve" text-anchor="middle" x="2222" y="-160" font-family="Helvetica,sans-Serif" font-size="10.00">Receipt</text>
<text xml:space="preserve" text-anchor="middle" x="2222" y="-148" font-family="Helvetica,sans-Serif" font-size="10.00">(AFIP)</text>
</g>
<!-- vetvisit&#45;&gt;receipt -->
<g id="edge31" class="edge">
<title>vetvisit&#45;&gt;receipt</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M2093.17,-289C2137.87,-289 2222,-289 2222,-289 2222,-289 2222,-186.87 2222,-186.87"/>
<polygon fill="black" stroke="black" points="2225.5,-186.87 2222,-176.87 2218.5,-186.87 2225.5,-186.87"/>
</g>
<!-- turnfeegroup -->
<g id="node41" class="node">
<title>turnfeegroup</title>
<path fill="#cfd8dc" stroke="black" d="M1621.79,-175C1621.79,-175 1564.21,-175 1564.21,-175 1558.21,-175 1552.21,-169 1552.21,-163 1552.21,-163 1552.21,-151 1552.21,-151 1552.21,-145 1558.21,-139 1564.21,-139 1564.21,-139 1621.79,-139 1621.79,-139 1627.79,-139 1633.79,-145 1633.79,-151 1633.79,-151 1633.79,-163 1633.79,-163 1633.79,-169 1627.79,-175 1621.79,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1593" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">TurnFeeGroup</text>
</g>
<!-- vetvisit&#45;&gt;turnfeegroup -->
<!-- vetvisitfollowup -->
<g id="node27" class="node">
<title>vetvisitfollowup</title>
<path fill="#fff176" stroke="black" d="M1921.84,-52C1921.84,-52 1888.16,-52 1888.16,-52 1882.16,-52 1876.16,-46 1876.16,-40 1876.16,-40 1876.16,-28 1876.16,-28 1876.16,-22 1882.16,-16 1888.16,-16 1888.16,-16 1921.84,-16 1921.84,-16 1927.84,-16 1933.84,-22 1933.84,-28 1933.84,-28 1933.84,-40 1933.84,-40 1933.84,-46 1927.84,-52 1921.84,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1905" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00">FollowUp</text>
</g>
<!-- vetvisitreport&#45;&gt;vetvisitfollowup -->
<g id="edge30" class="edge">
<title>vetvisitreport&#45;&gt;vetvisitfollowup</title>
<path fill="none" stroke="black" d="M1905,-138.59C1905,-138.59 1905,-63.65 1905,-63.65"/>
<polygon fill="black" stroke="black" points="1908.5,-63.65 1905,-53.65 1901.5,-63.65 1908.5,-63.65"/>
</g>
<!-- specialty&#45;&gt;study -->
<!-- province -->
<g id="node33" class="node">
<title>province</title>
<path fill="#cfd8dc" stroke="black" d="M1366.45,-306.6C1366.45,-306.6 1335.55,-306.6 1335.55,-306.6 1329.55,-306.6 1323.55,-300.6 1323.55,-294.6 1323.55,-294.6 1323.55,-282.6 1323.55,-282.6 1323.55,-276.6 1329.55,-270.6 1335.55,-270.6 1335.55,-270.6 1366.45,-270.6 1366.45,-270.6 1372.45,-270.6 1378.45,-276.6 1378.45,-282.6 1378.45,-282.6 1378.45,-294.6 1378.45,-294.6 1378.45,-300.6 1372.45,-306.6 1366.45,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1351" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Province</text>
</g>
<!-- locality -->
<g id="node34" class="node">
<title>locality</title>
<path fill="#cfd8dc" stroke="black" d="M1464,-306.6C1464,-306.6 1434,-306.6 1434,-306.6 1428,-306.6 1422,-300.6 1422,-294.6 1422,-294.6 1422,-282.6 1422,-282.6 1422,-276.6 1428,-270.6 1434,-270.6 1434,-270.6 1464,-270.6 1464,-270.6 1470,-270.6 1476,-276.6 1476,-282.6 1476,-282.6 1476,-294.6 1476,-294.6 1476,-300.6 1470,-306.6 1464,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1449" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Locality</text>
</g>
<!-- province&#45;&gt;locality -->
<g id="edge2" class="edge">
<title>province&#45;&gt;locality</title>
<path fill="none" stroke="black" d="M1378.82,-289C1378.82,-289 1410.27,-289 1410.27,-289"/>
<polygon fill="black" stroke="black" points="1410.27,-292.5 1420.27,-289 1410.27,-285.5 1410.27,-292.5"/>
</g>
<!-- petbreed -->
<g id="node35" class="node">
<title>petbreed</title>
<path fill="#cfd8dc" stroke="black" d="M1543.85,-306.6C1543.85,-306.6 1510.15,-306.6 1510.15,-306.6 1504.15,-306.6 1498.15,-300.6 1498.15,-294.6 1498.15,-294.6 1498.15,-282.6 1498.15,-282.6 1498.15,-276.6 1504.15,-270.6 1510.15,-270.6 1510.15,-270.6 1543.85,-270.6 1543.85,-270.6 1549.85,-270.6 1555.85,-276.6 1555.85,-282.6 1555.85,-282.6 1555.85,-294.6 1555.85,-294.6 1555.85,-300.6 1549.85,-306.6 1543.85,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1527" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetBreed</text>
</g>
<!-- tag -->
<g id="node39" class="node">
<title>tag</title>
<path fill="#cfd8dc" stroke="black" d="M1432,-175C1432,-175 1402,-175 1402,-175 1396,-175 1390,-169 1390,-163 1390,-163 1390,-151 1390,-151 1390,-145 1396,-139 1402,-139 1402,-139 1432,-139 1432,-139 1438,-139 1444,-145 1444,-151 1444,-151 1444,-163 1444,-163 1444,-169 1438,-175 1432,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1417" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Tag</text>
</g>
<!-- medication -->
<g id="node40" class="node">
<title>medication</title>
<path fill="#cfd8dc" stroke="black" d="M1518.18,-175C1518.18,-175 1477.82,-175 1477.82,-175 1471.82,-175 1465.82,-169 1465.82,-163 1465.82,-163 1465.82,-151 1465.82,-151 1465.82,-145 1471.82,-139 1477.82,-139 1477.82,-139 1518.18,-139 1518.18,-139 1524.18,-139 1530.18,-145 1530.18,-151 1530.18,-151 1530.18,-163 1530.18,-163 1530.18,-169 1524.18,-175 1518.18,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1498" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Medication</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Graph Viewer - AMAR Mascotas</title>
<link rel="stylesheet" href="styles.css">
</head>
<body class="graph-viewer">
<header class="graph-header">
<a href="index.html" class="back-link">← Index</a>
<div class="nav-controls">
<button onclick="navigate(-1)" id="btn-prev" title="Previous (←)"></button>
<span id="nav-position">1 / 4</span>
<button onclick="navigate(1)" id="btn-next" title="Next (→)"></button>
</div>
<h1 id="graph-title">Loading...</h1>
<div class="graph-controls">
<button onclick="setMode('fit')">Fit</button>
<button onclick="setMode('fit-width')">Width</button>
<button onclick="setMode('fit-height')">Height</button>
<button onclick="setMode('actual-size')">100%</button>
<button onclick="downloadSvg()">↓ SVG</button>
</div>
</header>
<div class="graph-container" id="graph-container">
<img id="graph-img" src="" alt="Graph">
</div>
<script>
const graphOrder = [
'04-data-model-simple',
'01-backend-architecture',
'02-frontend-architecture',
'03-data-model'
];
const graphs = {
'04-data-model-simple': {
title: 'Data Model Overview',
file: '04-data-model-simple.svg'
},
'01-backend-architecture': {
title: 'Backend Architecture',
file: '01-backend-architecture.svg'
},
'02-frontend-architecture': {
title: 'Frontend Architecture',
file: '02-frontend-architecture.svg'
},
'03-data-model': {
title: 'Detailed Data Model',
file: '03-data-model.svg'
}
};
const params = new URLSearchParams(window.location.search);
let graphKey = params.get('g') || '04-data-model-simple';
let currentIndex = graphOrder.indexOf(graphKey);
if (currentIndex === -1) currentIndex = 0;
function loadGraph(key) {
const graph = graphs[key];
document.getElementById('graph-title').textContent = graph.title;
document.getElementById('graph-img').src = graph.file;
document.title = graph.title + ' - AMAR Mascotas';
history.replaceState(null, '', '?g=' + key);
graphKey = key;
updateNavHints();
}
function updateNavHints() {
const idx = graphOrder.indexOf(graphKey);
const prevBtn = document.getElementById('btn-prev');
const nextBtn = document.getElementById('btn-next');
prevBtn.disabled = idx === 0;
nextBtn.disabled = idx === graphOrder.length - 1;
document.getElementById('nav-position').textContent = (idx + 1) + ' / ' + graphOrder.length;
}
function navigate(direction) {
const idx = graphOrder.indexOf(graphKey);
const newIdx = idx + direction;
if (newIdx >= 0 && newIdx < graphOrder.length) {
currentIndex = newIdx;
loadGraph(graphOrder[newIdx]);
}
}
function setMode(mode) {
const container = document.getElementById('graph-container');
container.className = 'graph-container ' + mode;
}
function downloadSvg() {
const graph = graphs[graphKey];
const link = document.createElement('a');
link.href = graph.file;
link.download = graph.file;
link.click();
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
navigate(-1);
} else if (e.key === 'ArrowRight') {
navigate(1);
} else if (e.key === 'Escape') {
window.location.href = 'index.html';
}
});
// Initialize
loadGraph(graphOrder[currentIndex]);
setMode('fit');
</script>
</body>
</html>

View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AMAR Mascotas - Architecture & Data Models</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>AMAR Mascotas</h1>
<p class="subtitle">Architecture & Data Model Documentation</p>
</header>
<main>
<!-- Graph Sections -->
<section class="graph-section" id="data-model-simple">
<div class="graph-header-row">
<h2>Data Model Overview</h2>
<a href="graph.html?g=04-data-model-simple" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=04-data-model-simple" class="graph-preview">
<img src="04-data-model-simple.svg" alt="Data Model Overview">
</a>
<div class="graph-details">
<p>High-level entity relationships without field details. Shows the main actors, workflow, and data flow.</p>
<h4>Clusters</h4>
<ul>
<li><strong>Users & Auth</strong>: Django auth.User as central identity</li>
<li><strong>Pet Owners & Pets</strong>: Clients, their pets, vaccines, studies</li>
<li><strong>Veterinarians</strong>: Vets with availability, specialties, coverage areas</li>
<li><strong>Services & Pricing</strong>: Service catalog with dynamic pricing</li>
<li><strong>Cart & Checkout</strong>: Shopping cart workflow</li>
<li><strong>Service Requests</strong>: Order lifecycle with state machine</li>
<li><strong>Veterinary Visits</strong>: Scheduled visits, reports, AFIP invoicing</li>
<li><strong>Reference Data</strong>: Lookups (specialties, neighborhoods, vaccines, etc.)</li>
</ul>
</div>
</section>
<section class="graph-section" id="backend">
<div class="graph-header-row">
<h2>Backend Architecture</h2>
<a href="graph.html?g=01-backend-architecture" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=01-backend-architecture" class="graph-preview">
<img src="01-backend-architecture.svg" alt="Backend Architecture">
</a>
<div class="graph-details">
<p>Django apps structure: mascotas, productos, solicitudes, common, payments, and external integrations.</p>
<h4>Celery Tasks</h4>
<table class="details-table">
<thead>
<tr><th>Task</th><th>App</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>send_veterinarian_followup</code></td>
<td>solicitudes</td>
<td>Re-sends availability request to vet if still pending</td>
</tr>
<tr>
<td><code>run_payment_reminder_cron</code></td>
<td>solicitudes</td>
<td>Cron job to send payment reminders</td>
</tr>
<tr>
<td><code>create_vetvisit_in_sheet</code></td>
<td>mascotas</td>
<td>Creates row in Google Sheets for visit</td>
</tr>
<tr>
<td><code>update_vetvisit_in_sheet</code></td>
<td>mascotas</td>
<td>Updates Google Sheets row</td>
</tr>
<tr>
<td><code>create_event_calendar_vetvisit</code></td>
<td>mascotas</td>
<td>Creates Google Calendar event</td>
</tr>
<tr>
<td><code>update_event_calendar_vetvisit</code></td>
<td>mascotas</td>
<td>Updates Google Calendar event</td>
</tr>
<tr>
<td><code>create_user_owner</code></td>
<td>mascotas</td>
<td>Creates Django user for PetOwner + welcome email</td>
</tr>
<tr>
<td><code>generate_vetvisit_invoice</code></td>
<td>mascotas</td>
<td>Generates AFIP invoice and PDF</td>
</tr>
<tr>
<td><code>fetch_mp_notification_details</code></td>
<td>payments</td>
<td>Fetches MercadoPago webhook details</td>
</tr>
</tbody>
</table>
<p class="note">Celery handles async operations: external APIs (Google, MercadoPago, AFIP), scheduled reminders, and heavy processing (invoices, emails).</p>
</div>
</section>
<section class="graph-section" id="frontend">
<div class="graph-header-row">
<h2>Frontend Architecture</h2>
<a href="graph.html?g=02-frontend-architecture" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=02-frontend-architecture" class="graph-preview">
<img src="02-frontend-architecture.svg" alt="Frontend Architecture">
</a>
<div class="graph-details">
<p>Next.js 13+ App Router structure with public pages, backoffice, shared components, and services layer.</p>
<h4>Key Areas</h4>
<ul>
<li><strong>Public Pages</strong>: Landing, service catalog, booking flow</li>
<li><strong>Backoffice</strong>: Role-based dashboards (admin, vet, petowner)</li>
<li><strong>Services Layer</strong>: API clients for backend communication</li>
<li><strong>State Management</strong>: Redux store for cart, auth, UI state</li>
<li><strong>Shared Components</strong>: Forms, tables, modals, navigation</li>
</ul>
</div>
</section>
<section class="graph-section" id="data-model-detailed">
<div class="graph-header-row">
<h2>Detailed Data Model</h2>
<a href="graph.html?g=03-data-model" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=03-data-model" class="graph-preview">
<img src="03-data-model.svg" alt="Detailed Data Model">
</a>
<div class="graph-details">
<p>Complete entity-relationship diagram with all fields, types, and relationships.</p>
<h4>Data Patterns</h4>
<ul>
<li><strong>Soft delete</strong>: <code>deleted</code> flag on most models</li>
<li><strong>Audit trail</strong>: <code>StateHistory</code> for service requests</li>
<li><strong>Geographic</strong>: PostGIS polygons for coverage areas</li>
<li><strong>Versioned pricing</strong>: Date ranges on <code>Prices</code></li>
</ul>
</div>
</section>
<!-- Findings Section -->
<section class="findings-section">
<h2>Key Findings</h2>
<div class="findings-grid">
<article class="finding-card">
<h3>User Types</h3>
<p>All users connect to <code>auth.User</code>:</p>
<ul>
<li><strong>PetOwner</strong>: Optional 1:1 link (created lazily)</li>
<li><strong>Veterinarian</strong>: Required 1:1 link to User</li>
<li><strong>Staff</strong>: Direct Django users with <code>is_staff=True</code></li>
</ul>
</article>
<article class="finding-card">
<h3>Core Workflow</h3>
<div class="workflow-diagram">
<code>PetOwner → Cart → ServiceRequest → VeterinarianAsked → VetVisit → Report</code>
</div>
<p><strong>State Machine:</strong></p>
<div class="state-flow">
pending → vet_asked → vet_accepted → coordinated → payed → Confirmado
</div>
</article>
<article class="finding-card">
<h3>Pricing Logic</h3>
<ul>
<li>Base price × <code>neighborhood.distance_coefficient</code></li>
<li>Optional vet-specific pricing via <code>Prices.veterinarian_id</code></li>
<li>Turn fee surcharge via <code>IndividualTurnFeeGroup</code></li>
<li>Time-based discounts via <code>Discounts</code> model</li>
</ul>
</article>
<article class="finding-card">
<h3>External Integrations</h3>
<ul>
<li><strong>MercadoPago</strong>: Payment processing</li>
<li><strong>Google Calendar</strong>: Visit synchronization</li>
<li><strong>Google Sheets</strong>: Visit tracking spreadsheet</li>
<li><strong>Mercately</strong>: WhatsApp notifications</li>
<li><strong>AFIP</strong>: Argentine tax invoicing</li>
</ul>
</article>
</div>
</section>
<!-- Tech Stack Section -->
<section class="tech-section">
<h2>Technology Stack</h2>
<div class="tech-grid">
<div class="tech-column">
<h3>Backend</h3>
<ul>
<li>Django 4.x</li>
<li>Django REST Framework</li>
<li>PostgreSQL + PostGIS</li>
<li>Celery (Redis)</li>
<li>JWT Authentication</li>
<li>django-afip</li>
</ul>
</div>
<div class="tech-column">
<h3>Frontend</h3>
<ul>
<li>Next.js 13+ (App Router)</li>
<li>React 18+</li>
<li>TypeScript</li>
<li>Redux</li>
<li>Axios</li>
<li>Tailwind CSS</li>
</ul>
</div>
<div class="tech-column">
<h3>Infrastructure</h3>
<ul>
<li>Docker</li>
<li>Nginx</li>
<li>AWS S3 (storage)</li>
<li>MercadoPago API</li>
<li>Google APIs</li>
<li>WhatsApp Business</li>
</ul>
</div>
</div>
</section>
</main>
<footer>
<p>AMAR Mascotas Architecture Documentation</p>
<p class="date">Generated: <time datetime="2024-12-14">December 2024</time></p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,565 @@
/* Reset and base */
*, *::before, *::after {
box-sizing: border-box;
}
:root {
--color-bg: #0f0f0f;
--color-surface: #1a1a1a;
--color-surface-hover: #252525;
--color-border: #333;
--color-text: #e0e0e0;
--color-text-muted: #888;
--color-accent: #4A90D9;
--color-accent-light: #6BA3E0;
--radius: 8px;
--shadow: 0 2px 8px rgba(0,0,0,0.3);
}
html {
font-size: 16px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
background: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
margin: 0;
padding: 0;
min-height: 100vh;
}
/* Header */
header {
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
padding: 2rem;
text-align: center;
}
header h1 {
margin: 0;
font-size: 2rem;
font-weight: 600;
color: var(--color-text);
}
header .subtitle {
margin: 0.5rem 0 0;
color: var(--color-text-muted);
font-size: 1rem;
}
/* Main content */
main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
section {
margin-bottom: 3rem;
}
section h2 {
font-size: 1.5rem;
font-weight: 600;
margin: 0 0 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--color-border);
}
/* Card Grid - max 3 columns */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
@media (min-width: 1000px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 700px) and (max-width: 999px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Graph Cards */
.card {
display: block;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
overflow: hidden;
text-decoration: none;
color: inherit;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
border-color: var(--color-accent);
}
.card-preview {
aspect-ratio: 4/3;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 1rem;
}
.card-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.card-content {
padding: 1rem;
}
.card-content h3 {
margin: 0 0 0.5rem;
font-size: 1.1rem;
font-weight: 600;
color: var(--color-accent-light);
}
.card-content p {
margin: 0;
font-size: 0.9rem;
color: var(--color-text-muted);
}
/* Findings Grid */
.findings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
@media (min-width: 1000px) {
.findings-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.finding-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.25rem;
}
.finding-card h3 {
margin: 0 0 0.75rem;
font-size: 1rem;
font-weight: 600;
color: var(--color-accent-light);
}
.finding-card p,
.finding-card ul {
margin: 0 0 0.75rem;
font-size: 0.9rem;
}
.finding-card ul {
padding-left: 1.25rem;
}
.finding-card li {
margin-bottom: 0.25rem;
}
.finding-card code {
background: #2a2a2a;
padding: 0.1em 0.4em;
border-radius: 3px;
font-size: 0.85em;
color: #f0f0f0;
}
.workflow-diagram {
background: #2a2a2a;
padding: 0.75rem;
border-radius: 4px;
margin-bottom: 0.75rem;
overflow-x: auto;
}
.workflow-diagram code {
background: none;
padding: 0;
font-size: 0.8rem;
white-space: nowrap;
}
.state-flow {
font-family: monospace;
font-size: 0.8rem;
color: var(--color-text-muted);
word-break: break-all;
}
.stats-table {
width: 100%;
font-size: 0.85rem;
border-collapse: collapse;
}
.stats-table td {
padding: 0.35rem 0;
border-bottom: 1px solid var(--color-border);
}
.stats-table td:first-child {
font-family: monospace;
color: var(--color-accent-light);
}
.stats-table td:last-child {
color: var(--color-text-muted);
text-align: right;
}
/* Tech Stack Grid */
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
@media (min-width: 700px) {
.tech-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.tech-column {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.25rem;
}
.tech-column h3 {
margin: 0 0 0.75rem;
font-size: 1rem;
font-weight: 600;
color: var(--color-accent-light);
}
.tech-column ul {
margin: 0;
padding-left: 1.25rem;
font-size: 0.9rem;
}
.tech-column li {
margin-bottom: 0.25rem;
}
/* Footer */
footer {
background: var(--color-surface);
border-top: 1px solid var(--color-border);
padding: 1.5rem 2rem;
text-align: center;
color: var(--color-text-muted);
font-size: 0.9rem;
}
footer p {
margin: 0.25rem 0;
}
/* Graph Viewer Page */
.graph-viewer {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.graph-header {
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
padding: 1rem 2rem;
display: flex;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.graph-header .back-link {
color: var(--color-accent);
text-decoration: none;
font-size: 0.9rem;
}
.graph-header .back-link:hover {
text-decoration: underline;
}
.nav-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-controls button {
background: var(--color-surface-hover);
border: 1px solid var(--color-border);
color: var(--color-text);
padding: 0.4rem 0.7rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.nav-controls button:hover:not(:disabled) {
background: var(--color-border);
}
.nav-controls button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.nav-controls span {
font-size: 0.85rem;
color: var(--color-text-muted);
min-width: 3rem;
text-align: center;
}
.graph-header h1 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
flex: 1;
}
.graph-controls {
display: flex;
gap: 0.5rem;
}
.graph-controls button {
background: var(--color-surface-hover);
border: 1px solid var(--color-border);
color: var(--color-text);
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.graph-controls button:hover {
background: var(--color-border);
}
.graph-container {
flex: 1;
overflow: auto;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.graph-container img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.graph-container.fit-width img {
width: 100%;
height: auto;
max-height: none;
}
.graph-container.fit-height img {
height: calc(100vh - 80px);
width: auto;
max-width: none;
}
.graph-container.actual-size img {
max-width: none;
max-height: none;
}
/* Graph Sections (index page) */
.graph-section {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
margin-bottom: 2rem;
overflow: hidden;
}
.graph-header-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.graph-header-row h2 {
margin: 0;
padding: 0;
border: none;
font-size: 1.25rem;
}
.view-btn {
background: var(--color-accent);
color: #fff;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
}
.view-btn:hover {
background: var(--color-accent-light);
}
.graph-section .graph-preview {
display: block;
background: #fff;
max-height: 400px;
overflow: hidden;
}
.graph-section .graph-preview img {
width: 100%;
height: auto;
object-fit: contain;
object-position: top left;
}
.graph-details {
padding: 1.5rem;
border-top: 1px solid var(--color-border);
}
.graph-details p {
margin: 0 0 1rem;
color: var(--color-text-muted);
}
.graph-details h4 {
margin: 1.5rem 0 0.75rem;
font-size: 0.95rem;
font-weight: 600;
color: var(--color-accent-light);
}
.graph-details h4:first-child {
margin-top: 0;
}
.graph-details ul {
margin: 0;
padding-left: 1.25rem;
font-size: 0.9rem;
}
.graph-details li {
margin-bottom: 0.35rem;
}
.graph-details code {
background: #2a2a2a;
padding: 0.1em 0.4em;
border-radius: 3px;
font-size: 0.85em;
}
/* Details Table */
.details-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
margin: 0.5rem 0 1rem;
}
.details-table th {
text-align: left;
padding: 0.5rem;
background: #2a2a2a;
border-bottom: 1px solid var(--color-border);
font-weight: 600;
color: var(--color-text);
}
.details-table td {
padding: 0.5rem;
border-bottom: 1px solid var(--color-border);
vertical-align: top;
}
.details-table code {
background: #2a2a2a;
padding: 0.15em 0.4em;
border-radius: 3px;
font-size: 0.85em;
white-space: nowrap;
}
.note {
font-size: 0.85rem;
color: var(--color-text-muted);
font-style: italic;
margin-top: 0.5rem;
}
/* Mobile adjustments */
@media (max-width: 600px) {
main {
padding: 1rem;
}
header {
padding: 1.5rem 1rem;
}
header h1 {
font-size: 1.5rem;
}
.card-grid,
.findings-grid {
grid-template-columns: 1fr;
}
.graph-header {
flex-wrap: wrap;
padding: 1rem;
}
.graph-header h1 {
order: -1;
width: 100%;
margin-bottom: 0.5rem;
}
.graph-controls {
flex-wrap: wrap;
}
}

View File

@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drive Index - Amar Mascotas</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.5;
padding: 2rem;
}
.container { max-width: 900px; margin: 0 auto; }
.back { font-size: 0.85rem; margin-bottom: 1rem; }
.back a { color: #6366f1; text-decoration: none; }
.back a:hover { text-decoration: underline; }
header {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 1.5rem;
border-radius: 12px;
margin-bottom: 1.5rem;
}
header h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
header p { opacity: 0.9; font-size: 0.85rem; }
.stats { display: flex; gap: 0.75rem; margin-top: 0.75rem; flex-wrap: wrap; }
.stat { background: rgba(255,255,255,0.2); padding: 0.35rem 0.75rem; border-radius: 6px; font-size: 0.8rem; }
.notice {
background: #fef3c7;
border: 1px solid #fcd34d;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
font-size: 0.85rem;
color: #92400e;
}
.notice strong { display: block; margin-bottom: 0.5rem; }
.notice ul { margin: 0.5rem 0 0 1.25rem; }
.notice li { margin-bottom: 0.25rem; }
details {
background: white;
border-radius: 8px;
margin-bottom: 0.5rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
summary {
padding: 0.75rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
list-style: none;
}
summary::-webkit-details-marker { display: none; }
summary:hover { background: #f9fafb; border-radius: 8px; }
summary .icon {
font-size: 0.7rem;
color: #6366f1;
transition: transform 0.15s;
}
details[open] summary .icon { transform: rotate(90deg); }
summary .name { font-weight: 500; font-size: 0.9rem; flex: 1; }
summary .count {
background: #e0e7ff;
color: #4f46e5;
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 500;
}
.folder-content {
padding: 0.5rem 1rem 0.75rem 2rem;
border-top: 1px solid #f0f0f0;
}
.folder-desc {
font-size: 0.8rem;
color: #666;
font-style: italic;
margin-bottom: 0.5rem;
}
.file-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.file {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: #555;
}
.file-ext {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
padding: 0.15rem 0.35rem;
border-radius: 3px;
min-width: 32px;
text-align: center;
}
.file-ext.xlsx { background: #dcfce7; color: #166534; }
.file-ext.docx { background: #dbeafe; color: #1e40af; }
.file-ext.pdf { background: #fee2e2; color: #991b1b; }
.file-ext.png, .file-ext.jpg { background: #fef3c7; color: #92400e; }
.file-ext.mov, .file-ext.mp4 { background: #f3e8ff; color: #7c3aed; }
.file-ext.pptx { background: #ffedd5; color: #c2410c; }
.file-ext.ttf { background: #f1f5f9; color: #475569; }
footer { text-align: center; padding: 2rem; color: #999; font-size: 0.75rem; }
</style>
</head>
<body>
<div class="container">
<div class="back"><a href="/">&larr; Album</a></div>
<header>
<h1>Drive Index</h1>
<p>Amar Mascotas - Company Drive Structure</p>
<div class="stats">
<span class="stat">12 folders</span>
<span class="stat">~235 files</span>
</div>
</header>
<div class="notice">
<strong>Preview Mode</strong>
This shows the Drive folder structure with placeholder filenames. Real file data will be available once <strong>Google Vein</strong> (Google Drive sync) is implemented in Artery:
<ul>
<li>Auto-sync folder structure and file metadata</li>
<li>Direct links to files (requires Google Workspace login)</li>
<li>Live updates when Drive contents change</li>
</ul>
</div>
<details>
<summary><span class="icon"></span><span class="name">01. Identidad</span><span class="count">3 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Brand assets, logos, typography</div>
<div class="file-list">
<div class="file"><span class="file-ext png">png</span> logo_main.png</div>
<div class="file"><span class="file-ext png">png</span> logo_alt.png</div>
<div class="file"><span class="file-ext ttf">ttf</span> brand_font.ttf</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">02. Marketing Contenidos</span><span class="count">8 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Social media content, templates, testimonials</div>
<div class="file-list">
<div class="file"><span class="file-ext docx">docx</span> social_template.docx</div>
<div class="file"><span class="file-ext png">png</span> post_image1.png</div>
<div class="file"><span class="file-ext png">png</span> post_image2.png</div>
<div class="file"><span class="file-ext mov">mov</span> event_video.mov</div>
<div class="file"><span class="file-ext png">png</span> testimonials.png</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">03. Marketing Growth</span><span class="count">8 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Paid media campaigns, performance dashboards</div>
<div class="file-list">
<div class="file"><span class="file-ext xlsx">xlsx</span> dashboard_report.xlsx</div>
<div class="file"><span class="file-ext png">png</span> ad_1080x1080.png</div>
<div class="file"><span class="file-ext png">png</span> ad_1200x628.png</div>
<div class="file"><span class="file-ext png">png</span> ad_stories.png</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">05. ATC - Operaciones</span><span class="count">12 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Process manuals, workflows, support docs</div>
<div class="file-list">
<div class="file"><span class="file-ext docx">docx</span> manual_procesos.docx</div>
<div class="file"><span class="file-ext jpg">jpg</span> workflow_diagram.jpg</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> contacts_list.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> postal_codes.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> tasks_tracker.xlsx</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">06. Supply</span><span class="count">7 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Vet network, lab partnerships, onboarding</div>
<div class="file-list">
<div class="file"><span class="file-ext pdf">pdf</span> lab_prices.pdf</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> vet_onboarding.xlsx</div>
<div class="file"><span class="file-ext pdf">pdf</span> vet_cv_sample.pdf</div>
<div class="file"><span class="file-ext mp4">mp4</span> meeting_recording.mp4</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">07. Finanzas</span><span class="count">7 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Invoices, accounting, finance tracking</div>
<div class="file-list">
<div class="file"><span class="file-ext xlsx">xlsx</span> finance_tracker.xlsx</div>
<div class="file"><span class="file-ext pdf">pdf</span> invoice_may.pdf</div>
<div class="file"><span class="file-ext pdf">pdf</span> invoice_jun.pdf</div>
<div class="file"><span class="file-ext pdf">pdf</span> invoice_jul.pdf</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">08. IT y Producto</span><span class="count">57 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Product specs, feature docs, references</div>
<div class="file-list">
<div class="file"><span class="file-ext docx">docx</span> product_index.docx</div>
<div class="file"><span class="file-ext docx">docx</span> feature_specs.docx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> platform_states.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> medications_db.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> events_mapping.xlsx</div>
<div class="file"><span class="file-ext pptx">pptx</span> benchmark.pptx</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">09. DATA</span><span class="count">2 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Database exports, data backups</div>
<div class="file-list">
<div class="file"><span class="file-ext xlsx">xlsx</span> pets_database.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> pets_backup.xlsx</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">Clientes - ventas</span><span class="count">22 files</span></summary>
<div class="folder-content">
<div class="folder-desc">CRM exports, sales data, client info</div>
<div class="file-list">
<div class="file"><span class="file-ext xlsx">xlsx</span> crm_export.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> churn_analysis.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> leads_2023.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> leads_2024.xlsx</div>
<div class="file"><span class="file-ext pdf">pdf</span> monthly_report.pdf</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">HR</span><span class="count">17 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Onboarding, job profiles, team docs</div>
<div class="file-list">
<div class="file"><span class="file-ext pptx">pptx</span> onboarding_deck.pptx</div>
<div class="file"><span class="file-ext docx">docx</span> job_profile_sales.docx</div>
<div class="file"><span class="file-ext pdf">pdf</span> job_profile_growth.pdf</div>
<div class="file"><span class="file-ext jpg">jpg</span> team_photo.jpg</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">Pitch Decks</span><span class="count">24 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Investor presentations, company decks</div>
<div class="file-list">
<div class="file"><span class="file-ext pptx">pptx</span> pitch_deck_latest.pptx</div>
<div class="file"><span class="file-ext docx">docx</span> video_script.docx</div>
<div class="file"><span class="file-ext pptx">pptx</span> product_timeline.pptx</div>
<div class="file"><span class="file-ext pptx">pptx</span> investor_update.pptx</div>
</div>
</div>
</details>
<details>
<summary><span class="icon"></span><span class="name">z.Backup archivos viejos</span><span class="count">46 files</span></summary>
<div class="folder-content">
<div class="folder-desc">Legacy files, old exports</div>
<div class="file-list">
<div class="file"><span class="file-ext xlsx">xlsx</span> old_vets_list.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> org_chart.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> data_room_2023.xlsx</div>
<div class="file"><span class="file-ext xlsx">xlsx</span> legacy_export.xlsx</div>
</div>
</div>
</details>
<footer>pawprint/album</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,217 @@
# Drive Index - Amar Mascotas
> vault source: `vault/drive/`
> 48 files | Last indexed: 2025-12-08
## Overview by Area
| Area | Focus | Key Files | Workflow Potential |
|------|-------|-----------|-------------------|
| 01. Identidad | Brand assets | Illustrations, Typography | Design handoff |
| 02. Marketing Contenidos | Social/Content | Posts, Event media, Templates | Content calendar |
| 03. Marketing Growth | Paid/Performance | Ad creatives, Dashboards | Campaign tracking |
| 05. ATC - Operaciones | Ops/Support | Process manual, Workflow diagram | Onboarding, SOP |
| 06. Supply | Vets/Labs | Vet tracking, Lab prices, CVs | Vet onboarding |
| 07. Finanzas | Accounting | Invoices, Finance sheet | Reconciliation |
| Clientes | Sales/CRM | Fitsales export | Client tracking |
| Pitch Decks | Presentations | Company pitch | Investor/partner |
---
## 01. Identidad Amar Mascotas
Brand identity assets.
```
Ilustraciones amar PNG/
├── Asset 4@4x.png
└── Asset 11@4x.png
Tipografia/Open_Sans/static/
└── OpenSans_Condensed-Italic.ttf
```
**Workflow relevance**: Design system, brand consistency checks.
---
## 02. Marketing Contenidos
Social media content and production assets.
```
Backup archivos viejos/
├── Contenido evento amar 10 ago 2023/
│ ├── IMG_7309.MOV
│ └── video-output-(...).mov
└── RRSS MSJ PREDETERMINADOS.docx ← Predefined social messages
Produccion audiovisual Jul 24/
└── Fotos Produccion Audiovisual JUL24/
└── Vacunacion Fotos Produccion JUL 24/
└── IMG_4620.JPG
Redes sociales Amar/
├── 02. FEBRERO 25 Finales/12-2/cambios/247.png
├── 08. AGOSTO 24 Finales Amar/
│ ├── 13.8/1.png
│ └── 21.8/3.png
└── Comentarios clientes felices JUN-JUL24/
└── Captura de pantalla (...).png ← Testimonials
```
**Workflow relevance**:
- `RRSS MSJ PREDETERMINADOS.docx` = template for ATC/social responses
- Monthly folders suggest content calendar exists
- Testimonials capture = social proof workflow
---
## 03. Marketing Growth
Paid media campaigns and performance tracking.
```
Paid Media - Pauta digital/
├── 05.MAY24 - ADS Testimonios/
├── 08.AGO24 - ADS 10 OFF/
│ ├── 1080 x 1080-17.png
│ ├── 1200 x 628-32.png
│ ├── 1200 x 628-33.png
│ ├── 1200 x 628-38.png
│ ├── 1200 x 628-39.png
│ └── 960 x 1200-30.png
├── Amar Mascotas - Reporte_Dash general(...)(2).xlsx
└── Amar Mascotas - Reporte_Dash general(...)(3).xlsx
```
**Workflow relevance**:
- Ad size variants = multi-platform campaigns (Meta, Google)
- Dashboard reports = performance tracking exists
- Monthly campaign folders = structured campaign workflow
---
## 05. ATC - Operaciones
Operations, customer service, and legacy data.
```
Operaciones backup viejos/
├── ATC - Manual de Procesos.docx ← KEY: Process documentation
├── work Flow servicio veterinario.jpg ← KEY: Service workflow diagram
├── Clientes a contactar.xlsx
├── codigos_postales_y_barrios (1).xlsx ← Coverage/delivery zones
├── domesticables ultimo.docx
├── domesticables ultimo.pdf
├── INFO.xlsx
├── Reporte DOMESTICABLES 11_5.xlsx
├── Tareas de Leila.xlsx
└── entrega/
├── prestashop/
│ ├── clientes a escribir febrero 2023.csv
│ └── datos web vieja/
│ └── reembolsos por falta de stock.csv
└── vendedores/somos pam/
└── articulos desactivados en marzo 2023.csv
```
**Workflow relevance**:
- `ATC - Manual de Procesos.docx` = existing SOP (Gherkin source?)
- `work Flow servicio veterinario.jpg` = visual process (BDD source?)
- Legacy CSV = migration/history context
- Task tracking = ops workflow exists
---
## 06. Supply (vetes-labo-clinicas)
Veterinarian network and lab partnerships.
```
Comunciacion/
└── Veterinarios-46.jpg
Formularios- Viejos - no usar/
└── Reuniones Matias/
├── Grabación de pantalla 2024-08-20(...).mp4
└── Grabación de pantalla 2024-08-21(...).mp4
Laboratorios/DiagnoTest/
└── Diagnotest-ListaPrecios-Junio2025 (1).pdf ← Current pricing
Veterinarios postulantes/
├── Aldana Becker - Colaboracion Amar Mascotas.docx
├── Ana Christophersen/
│ └── CV - Ana Christophersen.pdf
└── Ingreso Veterinarios.xlsx ← Vet onboarding tracker
```
**Workflow relevance**:
- `Ingreso Veterinarios.xlsx` = vet recruitment/onboarding flow
- Lab price list = service pricing reference
- Meeting recordings = training/knowledge transfer
---
## 07. Finanzas y contabilidad
Invoices and financial tracking.
```
Facturas/
├── May 23/Recibido/
│ ├── FacturaAMARMASCOTASSA(...).pdf
│ └── FacturaAMARMASCOTASSA(...).pdf
├── Jun 23/Recibido/
│ ├── nc 4-29.pdf
│ └── nc 4-30.pdf
└── Jul 23/Recibido/
├── FAC 567 ZUCCONI ALAN EDGARDO.pdf
└── Mercaba carlos roberto cozak.pdf
Finanzas Amar mascotas.xlsx ← Main finance tracker
```
**Workflow relevance**:
- Monthly invoice organization = accounting workflow
- Finance xlsx = reconciliation process
---
## Clientes - ventas - devoluciones
CRM and sales data.
```
Excels Clientes - ventas - devoluciones/
└── Fitsales CRM vinculados hasta el 06 dic.xlsx
```
**Workflow relevance**:
- CRM export = client tracking, sales funnel
---
## Pitch Decks - Presentaciones
Company presentations.
```
Copias Pitch Deck Cata/
└── Copia de Pitch Deck - ABR 2025.pptx
```
**Workflow relevance**:
- Investor/partner communications
---
## Workflow Discovery Summary
### Documents to Review for BDD/Gherkin
1. `05. ATC/ATC - Manual de Procesos.docx` - existing process documentation
2. `05. ATC/work Flow servicio veterinario.jpg` - visual workflow
3. `02. Marketing/RRSS MSJ PREDETERMINADOS.docx` - response templates
### Active Tracking Sheets
- `06. Supply/Ingreso Veterinarios.xlsx` - vet onboarding
- `Clientes/Fitsales CRM vinculados.xlsx` - client tracking
- `07. Finanzas/Finanzas Amar mascotas.xlsx` - finance
### Potential Artery Connections
- Fitsales CRM → pulse for client data
- Finance xlsx → pulse for accounting
- Vet tracker → pulse for supply management

View File

@@ -0,0 +1,60 @@
# Feature Flow Book
## Purpose
Presentation showing the feature standardization pipeline.
## The Pipeline
```
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ OPS TEMPLATES │ -> │ BDD/GHERKIN │ -> │ TESTS │
│ │ │ │ │ │
│ Non-technical │ │ .feature files │ │ Backend: │
│ User flows │ │ Given/When/Then │ │ API contracts │
│ From support │ │ Human readable │ │ Workflows │
│ │ │ │ │ Frontend: │
│ │ │ │ │ Page Objects │
│ │ │ │ │ E2E specs │
└──────────────────┘ └──────────────────┘ └──────────────────┘
```
## Files
- `index-en.html` - English slide presentation (8 slides, arrow keys)
- `index-es.html` - Spanish slide presentation (8 slides, arrow keys)
## Slides Structure
1. Title
2. Pipeline Overview (3 columns)
3. Ops Templates
4. BDD/Gherkin
5. Gherkin File Organization (best practices)
6. Backend Tests (amar_django_back structure)
7. Frontend Tests (amar_frontend structure)
8. Per-Feature Checklist
## Sources
### Ops Templates
- `album/template/ops-flow/plantilla-flujo.md`
- `def/work_plan/21-plantilla-flujos-usuario.md`
### BDD/Gherkin Examples
- `def/work_plan/10-flow-turnero.md` (full gherkin + tests example)
### Test Structure References
- `amar_django_back/tests/contracts/README.md`
- `amar_frontend/tests/README.md`
## Editing
Edit `index-en.html` or `index-es.html` directly.
Slides are `<section>` elements. Arrow keys to navigate.
## Flow Checklist (per feature)
- [ ] Ops template filled by support team
- [ ] Convert to .feature file (Gherkin spec)
- [ ] Backend: API contract tests per endpoint
- [ ] Backend: Workflow test (composition)
- [ ] Frontend: Page Object (if new page)
- [ ] Frontend: E2E spec (Playwright)
- [ ] Wire to CI

View File

@@ -0,0 +1,392 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feature Flow - Standardization Pipeline</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
}
.slide { display: none; min-height: 100vh; padding: 3rem; }
.slide.active { display: flex; flex-direction: column; }
.nav { position: fixed; bottom: 2rem; right: 2rem; display: flex; gap: 0.5rem; z-index: 100; }
.nav button { background: #334155; border: none; color: #e2e8f0; padding: 0.75rem 1.25rem; border-radius: 6px; cursor: pointer; font-size: 1rem; }
.nav button:hover { background: #475569; }
.nav .counter { background: transparent; padding: 0.75rem 1rem; color: #64748b; }
.slide-title { justify-content: center; align-items: center; text-align: center; }
.slide-title h1 { font-size: 3.5rem; font-weight: 700; margin-bottom: 1rem; background: linear-gradient(135deg, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.slide-title p { font-size: 1.5rem; color: #94a3b8; }
.slide-title .subtitle { margin-top: 3rem; font-size: 1rem; color: #64748b; }
.pipeline { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; margin: 2rem 0; }
.pipeline-step { background: #1e293b; border-radius: 12px; padding: 2rem; border-left: 4px solid; }
.pipeline-step.ops { border-color: #22c55e; }
.pipeline-step.bdd { border-color: #6366f1; }
.pipeline-step.tests { border-color: #f59e0b; }
.pipeline-step h3 { font-size: 1.25rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
.pipeline-step .num { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.85rem; font-weight: 700; }
.pipeline-step.ops .num { background: #22c55e; color: #0f172a; }
.pipeline-step.bdd .num { background: #6366f1; color: white; }
.pipeline-step.tests .num { background: #f59e0b; color: #0f172a; }
.pipeline-step ul { list-style: none; color: #94a3b8; font-size: 0.95rem; }
.pipeline-step li { padding: 0.4rem 0; padding-left: 1rem; border-left: 2px solid #334155; margin-bottom: 0.25rem; }
.slide h2 { font-size: 2rem; margin-bottom: 2rem; display: flex; align-items: center; gap: 1rem; }
.slide h2 .badge { padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
.slide h2 .badge.ops { background: #22c55e; color: #0f172a; }
.slide h2 .badge.bdd { background: #6366f1; color: white; }
.slide h2 .badge.tests { background: #f59e0b; color: #0f172a; }
pre { background: #1e293b; padding: 1.5rem; border-radius: 8px; overflow-x: auto; font-size: 0.8rem; line-height: 1.5; margin: 1rem 0; }
.keyword { color: #c084fc; }
.string { color: #4ade80; }
.comment { color: #64748b; }
.decorator { color: #f59e0b; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; flex: 1; }
.col { background: #1e293b; border-radius: 8px; padding: 1.5rem; }
.col h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 1rem; text-transform: uppercase; letter-spacing: 0.05em; }
.checklist { list-style: none; font-size: 1.1rem; }
.checklist li { padding: 0.75rem 0; display: flex; align-items: center; gap: 0.75rem; border-bottom: 1px solid #334155; }
.checklist .check { width: 20px; height: 20px; border: 2px solid #475569; border-radius: 4px; }
.flow-list { list-style: none; }
.flow-list li { padding: 0.4rem 0; color: #94a3b8; font-size: 0.9rem; }
.flow-list li strong { color: #e2e8f0; }
</style>
</head>
<body>
<section class="slide slide-title active" data-slide="0">
<h1>Feature Flow</h1>
<p>Standardization Pipeline</p>
<div class="subtitle">Ops Templates &rarr; BDD/Gherkin &rarr; Backend + Frontend Tests</div>
</section>
<section class="slide" data-slide="1">
<h2>Pipeline Overview</h2>
<div class="pipeline">
<div class="pipeline-step ops">
<h3><span class="num">1</span> Ops Templates</h3>
<ul>
<li>Non-technical language</li>
<li>User perspective flows</li>
<li>From support/ops team</li>
<li>Captures edge cases</li>
<li>Documents known problems</li>
</ul>
</div>
<div class="pipeline-step bdd">
<h3><span class="num">2</span> BDD/Gherkin</h3>
<ul>
<li>.feature files</li>
<li>Given/When/Then syntax</li>
<li>Human readable specs</li>
<li>Single source of truth</li>
<li>Maps to both test types</li>
</ul>
</div>
<div class="pipeline-step tests">
<h3><span class="num">3</span> Tests</h3>
<ul>
<li><strong>Backend:</strong> API contracts</li>
<li><strong>Backend:</strong> Workflows (compositions)</li>
<li><strong>Frontend:</strong> Page Objects</li>
<li><strong>Frontend:</strong> E2E specs (Playwright)</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="2">
<h2><span class="badge ops">1</span> Ops Templates</h2>
<div class="two-col">
<div class="col">
<h4>Template Structure</h4>
<pre>
<span class="keyword">### [Flow Name]</span>
<span class="comment">User type:</span> Pet Owner / Vet / Admin
<span class="comment">Entry point:</span> Page/button/link
<span class="comment">Goal:</span> One sentence
<span class="keyword">Steps:</span>
1. First action
2. Second action
3. ...
<span class="keyword">Expected result:</span>
- What should happen
<span class="keyword">Common problems:</span>
- Problem 1
<span class="keyword">Edge cases:</span>
- Special case 1
</pre>
</div>
<div class="col">
<h4>Source</h4>
<ul class="flow-list">
<li><strong>Template:</strong> album/template/ops-flow/</li>
<li><strong>Reference:</strong> def/work_plan/21-plantilla</li>
</ul>
<h4 style="margin-top: 1.5rem;">Who Fills This</h4>
<ul class="flow-list">
<li>Support team (daily user contact)</li>
<li>Ops team (knows workarounds)</li>
<li>Product (requirements)</li>
</ul>
<h4 style="margin-top: 1.5rem;">Output</h4>
<ul class="flow-list">
<li>One .md per flow</li>
<li>Organized by user type</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="3">
<h2><span class="badge bdd">2</span> BDD/Gherkin</h2>
<div class="two-col">
<div class="col">
<h4>.feature File</h4>
<pre>
<span class="keyword">Feature:</span> Turnero - Book appointment
<span class="keyword">Scenario:</span> Book vaccination for cat
<span class="decorator">Given</span> I am on the turnero page
<span class="decorator">When</span> I enter address <span class="string">"Av Santa Fe 1234"</span>
<span class="decorator">And</span> I click <span class="string">"Next"</span>
<span class="decorator">Then</span> a guest user should be created
<span class="decorator">When</span> I add pet <span class="string">"Koshka"</span> type <span class="string">"Cat"</span>
<span class="decorator">And</span> I select <span class="string">"Vaccination"</span>
<span class="decorator">Then</span> <span class="string">"Clinical consult"</span> is auto-added
<span class="keyword">Scenario:</span> Services filtered by pet type
<span class="decorator">Given</span> I added a cat
<span class="decorator">Then</span> I see cat vaccines
<span class="decorator">And</span> I dont see dog vaccines
</pre>
</div>
<div class="col">
<h4>Keywords</h4>
<ul class="flow-list">
<li><strong>Feature</strong> = one capability</li>
<li><strong>Scenario</strong> = one behavior</li>
<li><strong>Given</strong> = precondition</li>
<li><strong>When</strong> = action</li>
<li><strong>Then</strong> = expected result</li>
</ul>
<h4 style="margin-top: 1.5rem;">Reference</h4>
<ul class="flow-list">
<li>def/work_plan/10-flow-turnero.md</li>
<li>Full example with Gherkin, API tests, Page Objects</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="4">
<h2><span class="badge bdd">2b</span> Gherkin File Organization</h2>
<div class="two-col">
<div class="col">
<h4>Correct: One Feature = One File</h4>
<pre>
<span class="keyword">features/</span>
├── pet-owner/
│ ├── registro.feature <span class="comment"># 6-8 scenarios</span>
│ ├── reservar-turno.feature <span class="comment"># 10-15 scenarios</span>
│ ├── gestion-mascotas.feature
│ └── pago.feature
├── veterinarian/
│ └── ...
└── backoffice/
└── ...
</pre>
<h4 style="margin-top: 1rem;">Anti-pattern: One Scenario = One File</h4>
<pre style="border-left: 3px solid #ef4444;">
<span class="comment"># DON'T do this</span>
features/pet-owner/registro/
├── registro-exitoso.feature
├── registro-email-invalido.feature
├── registro-password-corto.feature
└── <span class="comment">... (dozens of tiny files)</span>
</pre>
</div>
<div class="col">
<h4>Why Multiple Scenarios per File</h4>
<ul class="flow-list">
<li><strong>Feature = Capability</strong> - one file describes one capability with all its behaviors</li>
<li><strong>Context stays together</strong> - Background, Rules share context</li>
<li><strong>Tooling expects it</strong> - test runners, reports, IDE navigation</li>
</ul>
<h4 style="margin-top: 1.5rem;">When to Split</h4>
<pre>
<span class="comment"># Scenarios per file:</span>
5-20 <span class="string">Normal, keep as is</span>
20-40 <span class="string">Consider splitting</span>
40+ <span class="string">Definitely split</span>
</pre>
<h4 style="margin-top: 1rem;">Folder Depth</h4>
<ul class="flow-list">
<li><strong>Good:</strong> 1-2 levels max</li>
<li><strong>Avoid:</strong> deep nesting</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="5">
<h2><span class="badge tests">3a</span> Backend Tests</h2>
<div class="two-col">
<div class="col">
<h4>Structure (amar_django_back)</h4>
<pre>
tests/contracts/
├── base.py <span class="comment"># mode switcher</span>
├── endpoints.py <span class="comment"># API paths (single source)</span>
├── helpers.py <span class="comment"># test data</span>
├── mascotas/ <span class="comment"># app tests</span>
│ ├── test_pet_owners.py
│ ├── test_pets.py
│ └── test_coverage.py
├── productos/
│ ├── test_services.py
│ └── test_cart.py
├── solicitudes/
│ └── test_service_requests.py
└── <span class="keyword">workflows/</span> <span class="comment"># compositions</span>
└── test_turnero_general.py
</pre>
</div>
<div class="col">
<h4>Two Test Modes</h4>
<pre>
<span class="comment"># Fast (Django test client)</span>
pytest tests/contracts/
<span class="comment"># Live (real HTTP)</span>
CONTRACT_TEST_MODE=live pytest
</pre>
<h4 style="margin-top: 1rem;">Workflow = Composition</h4>
<pre>
<span class="comment"># Calls endpoints in sequence:</span>
1. Check coverage
2. Create pet owner
3. Create pet
4. Get services
5. Create request
</pre>
<h4 style="margin-top: 1rem;">Key Files</h4>
<ul class="flow-list">
<li><strong>endpoints.py</strong> - change paths here only</li>
<li><strong>helpers.py</strong> - sample data</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="6">
<h2><span class="badge tests">3b</span> Frontend Tests</h2>
<div class="two-col">
<div class="col">
<h4>Structure (amar_frontend)</h4>
<pre>
tests/e2e/
├── <span class="keyword">pages/</span> <span class="comment"># Page Objects</span>
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ └── index.ts
└── login.spec.ts <span class="comment"># E2E test</span>
</pre>
<h4 style="margin-top: 1rem;">Page Object Pattern</h4>
<pre>
<span class="keyword">export class</span> LoginPage <span class="keyword">extends</span> BasePage {
<span class="keyword">get</span> emailInput() {
<span class="keyword">return</span> this.page.getByLabel(<span class="string">'Email'</span>);
}
<span class="keyword">async</span> login(email, password) {
<span class="keyword">await</span> this.emailInput.fill(email);
<span class="keyword">await</span> this.passwordInput.fill(password);
<span class="keyword">await</span> this.submitButton.click();
}
}
</pre>
</div>
<div class="col">
<h4>Running Tests</h4>
<pre>
<span class="comment"># All tests</span>
npx playwright test
<span class="comment"># With UI</span>
npx playwright test --ui
<span class="comment"># Specific file</span>
npx playwright test login.spec.ts
</pre>
<h4 style="margin-top: 1rem;">Locator Priority</h4>
<ul class="flow-list">
<li>1. getByRole() - buttons, links</li>
<li>2. getByLabel() - form fields</li>
<li>3. getByText() - visible text</li>
<li>4. getByTestId() - data-testid</li>
</ul>
<h4 style="margin-top: 1rem;">Avoid</h4>
<ul class="flow-list">
<li>CSS class selectors</li>
<li>Complex XPath</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="7">
<h2>Per-Feature Checklist</h2>
<ul class="checklist">
<li><span class="check"></span> Ops template filled (support team)</li>
<li><span class="check"></span> Convert to .feature file (Gherkin spec)</li>
<li><span class="check"></span> Backend: API contract tests per endpoint</li>
<li><span class="check"></span> Backend: Workflow test (composition)</li>
<li><span class="check"></span> Frontend: Page Object (if new page)</li>
<li><span class="check"></span> Frontend: E2E spec (Playwright)</li>
<li><span class="check"></span> Wire to CI</li>
</ul>
<div style="margin-top: 2rem; color: #64748b; font-size: 0.9rem;">
<p><strong>Full example:</strong> def/work_plan/10-flow-turnero.md</p>
<p><strong>Backend README:</strong> amar_django_back/tests/contracts/README.md</p>
<p><strong>Frontend README:</strong> amar_frontend/tests/README.md</p>
</div>
</section>
<div class="nav">
<button onclick="prevSlide()">&#8592;</button>
<span class="counter"><span id="current">1</span>/<span id="total">8</span></span>
<button onclick="nextSlide()">&#8594;</button>
</div>
<script>
let current = 0;
const slides = document.querySelectorAll('.slide');
const total = slides.length;
document.getElementById('total').textContent = total;
function showSlide(n) {
slides.forEach(s => s.classList.remove('active'));
current = (n + total) % total;
slides[current].classList.add('active');
document.getElementById('current').textContent = current + 1;
}
function nextSlide() { showSlide(current + 1); }
function prevSlide() { showSlide(current - 1); }
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === ' ') nextSlide();
if (e.key === 'ArrowLeft') prevSlide();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,470 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feature Flow - Pipeline de Estandarizacion</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
}
.slide {
display: none;
min-height: 100vh;
padding: 3rem;
}
.slide.active { display: flex; flex-direction: column; }
.nav {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
gap: 0.5rem;
z-index: 100;
}
.nav button {
background: #334155;
border: none;
color: #e2e8f0;
padding: 0.75rem 1.25rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
}
.nav button:hover { background: #475569; }
.nav .counter {
background: transparent;
padding: 0.75rem 1rem;
color: #64748b;
}
.slide-title {
justify-content: center;
align-items: center;
text-align: center;
}
.slide-title h1 {
font-size: 3.5rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(135deg, #6366f1, #a855f7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.slide-title p { font-size: 1.5rem; color: #94a3b8; }
.slide-title .subtitle { margin-top: 3rem; font-size: 1rem; color: #64748b; }
.pipeline {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin: 2rem 0;
}
.pipeline-step {
background: #1e293b;
border-radius: 12px;
padding: 2rem;
border-left: 4px solid;
}
.pipeline-step.ops { border-color: #22c55e; }
.pipeline-step.bdd { border-color: #6366f1; }
.pipeline-step.tests { border-color: #f59e0b; }
.pipeline-step h3 {
font-size: 1.25rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.pipeline-step .num {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 700;
}
.pipeline-step.ops .num { background: #22c55e; color: #0f172a; }
.pipeline-step.bdd .num { background: #6366f1; color: white; }
.pipeline-step.tests .num { background: #f59e0b; color: #0f172a; }
.pipeline-step ul { list-style: none; color: #94a3b8; font-size: 0.95rem; }
.pipeline-step li { padding: 0.4rem 0; padding-left: 1rem; border-left: 2px solid #334155; margin-bottom: 0.25rem; }
.slide h2 {
font-size: 2rem;
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 1rem;
}
.slide h2 .badge {
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.slide h2 .badge.ops { background: #22c55e; color: #0f172a; }
.slide h2 .badge.bdd { background: #6366f1; color: white; }
.slide h2 .badge.tests { background: #f59e0b; color: #0f172a; }
pre {
background: #1e293b;
padding: 1.5rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.6;
margin: 1rem 0;
}
.keyword { color: #c084fc; }
.string { color: #4ade80; }
.comment { color: #64748b; }
.decorator { color: #f59e0b; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; flex: 1; }
.col { background: #1e293b; border-radius: 8px; padding: 1.5rem; }
.col h4 { font-size: 0.9rem; color: #94a3b8; margin-bottom: 1rem; text-transform: uppercase; letter-spacing: 0.05em; }
.checklist { list-style: none; font-size: 1.1rem; }
.checklist li { padding: 0.75rem 0; display: flex; align-items: center; gap: 0.75rem; border-bottom: 1px solid #334155; }
.checklist .check { width: 20px; height: 20px; border: 2px solid #475569; border-radius: 4px; }
.flow-list { list-style: none; }
.flow-list li { padding: 0.5rem 0; color: #94a3b8; }
.flow-list li strong { color: #e2e8f0; }
</style>
</head>
<body>
<section class="slide slide-title active" data-slide="0">
<h1>Feature Flow</h1>
<p>Pipeline de Estandarizacion</p>
<div class="subtitle">Templates Ops &rarr; BDD/Gherkin &rarr; Tests Backend + Frontend</div>
</section>
<section class="slide" data-slide="1">
<h2>Vision General del Pipeline</h2>
<div class="pipeline">
<div class="pipeline-step ops">
<h3><span class="num">1</span> Templates Ops</h3>
<ul>
<li>Lenguaje no tecnico</li>
<li>Flujos desde el usuario</li>
<li>Del equipo de soporte/ops</li>
<li>Captura casos borde</li>
<li>Documenta problemas conocidos</li>
</ul>
</div>
<div class="pipeline-step bdd">
<h3><span class="num">2</span> BDD/Gherkin</h3>
<ul>
<li>Archivos .feature</li>
<li>Sintaxis Given/When/Then</li>
<li>Specs legibles</li>
<li>Fuente unica de verdad</li>
<li>Mapea a ambos tipos de test</li>
</ul>
</div>
<div class="pipeline-step tests">
<h3><span class="num">3</span> Tests</h3>
<ul>
<li><strong>Backend:</strong> Contratos API</li>
<li><strong>Backend:</strong> Workflows (composiciones)</li>
<li><strong>Frontend:</strong> Page Objects</li>
<li><strong>Frontend:</strong> E2E specs (Playwright)</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="2">
<h2><span class="badge ops">1</span> Templates Ops</h2>
<div class="two-col">
<div class="col">
<h4>Estructura de la Plantilla</h4>
<pre>
<span class="keyword">### [Nombre del Flujo]</span>
<span class="comment">Tipo de usuario:</span> Dueno / Vet / Admin
<span class="comment">Donde empieza:</span> Pagina/boton/link
<span class="comment">Objetivo:</span> Una oracion
<span class="keyword">Pasos:</span>
1. Primera accion
2. Segunda accion
3. ...
<span class="keyword">Que deberia pasar:</span>
- Resultado esperado
<span class="keyword">Problemas comunes:</span>
- Problema 1
<span class="keyword">Casos especiales:</span>
- Caso especial 1
</pre>
</div>
<div class="col">
<h4>Fuente</h4>
<ul class="flow-list">
<li><strong>Plantilla:</strong> album/template/ops-flow/</li>
<li><strong>Referencia:</strong> def/work_plan/21-plantilla</li>
</ul>
<h4 style="margin-top: 1.5rem;">Quien Completa Esto</h4>
<ul class="flow-list">
<li>Equipo de soporte (contacto diario)</li>
<li>Equipo de ops (conoce workarounds)</li>
<li>Producto (requerimientos)</li>
</ul>
<h4 style="margin-top: 1.5rem;">Output</h4>
<ul class="flow-list">
<li>Un .md por flujo</li>
<li>Organizado por tipo de usuario</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="3">
<h2><span class="badge bdd">2</span> BDD/Gherkin</h2>
<div class="two-col">
<div class="col">
<h4>Archivo .feature</h4>
<pre>
<span class="keyword">Feature:</span> Turnero - Reservar turno
<span class="keyword">Scenario:</span> Reservar vacuna para gato
<span class="decorator">Given</span> estoy en la pagina del turnero
<span class="decorator">When</span> ingreso direccion <span class="string">"Av Santa Fe 1234"</span>
<span class="decorator">And</span> hago click en <span class="string">"Siguiente"</span>
<span class="decorator">Then</span> se crea un usuario invitado
<span class="decorator">When</span> agrego mascota <span class="string">"Koshka"</span> tipo <span class="string">"Gato"</span>
<span class="decorator">And</span> selecciono <span class="string">"Vacunacion"</span>
<span class="decorator">Then</span> <span class="string">"Consulta clinica"</span> se agrega auto
<span class="keyword">Scenario:</span> Servicios filtrados por tipo
<span class="decorator">Given</span> agregue un gato
<span class="decorator">Then</span> veo vacunas felinas
<span class="decorator">And</span> no veo vacunas caninas
</pre>
</div>
<div class="col">
<h4>Palabras Clave</h4>
<ul class="flow-list">
<li><strong>Feature</strong> = una funcionalidad</li>
<li><strong>Scenario</strong> = un comportamiento</li>
<li><strong>Given</strong> = precondicion</li>
<li><strong>When</strong> = accion</li>
<li><strong>Then</strong> = resultado esperado</li>
</ul>
<h4 style="margin-top: 1.5rem;">Referencia</h4>
<ul class="flow-list">
<li>def/work_plan/10-flow-turnero.md</li>
<li>Ejemplo completo con Gherkin, tests API, Page Objects</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="4">
<h2><span class="badge bdd">2b</span> Organizacion de Archivos Gherkin</h2>
<div class="two-col">
<div class="col">
<h4>Correcto: Una Feature = Un Archivo</h4>
<pre>
<span class="keyword">features/</span>
├── pet-owner/
│ ├── registro.feature <span class="comment"># 6-8 escenarios</span>
│ ├── reservar-turno.feature <span class="comment"># 10-15 escenarios</span>
│ ├── gestion-mascotas.feature
│ └── pago.feature
├── veterinarian/
│ └── ...
└── backoffice/
└── ...
</pre>
<h4 style="margin-top: 1rem;">Anti-patron: Un Escenario = Un Archivo</h4>
<pre style="border-left: 3px solid #ef4444;">
<span class="comment"># NO hacer esto</span>
features/pet-owner/registro/
├── registro-exitoso.feature
├── registro-email-invalido.feature
├── registro-password-corto.feature
└── <span class="comment">... (docenas de archivos pequeños)</span>
</pre>
</div>
<div class="col">
<h4>Por Que Multiples Escenarios por Archivo</h4>
<ul class="flow-list">
<li><strong>Feature = Capacidad</strong> - un archivo describe una capacidad con todos sus comportamientos</li>
<li><strong>Contexto junto</strong> - Background, Rules comparten contexto</li>
<li><strong>Tooling lo espera</strong> - test runners, reportes, navegacion IDE</li>
</ul>
<h4 style="margin-top: 1.5rem;">Cuando Dividir</h4>
<pre>
<span class="comment"># Escenarios por archivo:</span>
5-20 <span class="string">Normal, mantener</span>
20-40 <span class="string">Considerar dividir</span>
40+ <span class="string">Definitivamente dividir</span>
</pre>
<h4 style="margin-top: 1rem;">Profundidad de Carpetas</h4>
<ul class="flow-list">
<li><strong>Bien:</strong> 1-2 niveles max</li>
<li><strong>Evitar:</strong> anidamiento profundo</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="5">
<h2><span class="badge tests">3a</span> Tests Backend</h2>
<div class="two-col">
<div class="col">
<h4>Estructura (amar_django_back)</h4>
<pre>
tests/contracts/
├── base.py <span class="comment"># switcher de modo</span>
├── endpoints.py <span class="comment"># paths API (fuente unica)</span>
├── helpers.py <span class="comment"># datos de prueba</span>
├── mascotas/ <span class="comment"># tests por app</span>
│ ├── test_pet_owners.py
│ ├── test_pets.py
│ └── test_coverage.py
├── productos/
│ ├── test_services.py
│ └── test_cart.py
├── solicitudes/
│ └── test_service_requests.py
└── <span class="keyword">workflows/</span> <span class="comment"># composiciones</span>
└── test_turnero_general.py
</pre>
</div>
<div class="col">
<h4>Dos Modos de Test</h4>
<pre>
<span class="comment"># Rapido (Django test client)</span>
pytest tests/contracts/
<span class="comment"># Live (HTTP real)</span>
CONTRACT_TEST_MODE=live pytest
</pre>
<h4 style="margin-top: 1rem;">Workflow = Composicion</h4>
<pre>
<span class="comment"># Llama endpoints en secuencia:</span>
1. Check cobertura
2. Crear pet owner
3. Crear mascota
4. Obtener servicios
5. Crear solicitud
</pre>
<h4 style="margin-top: 1rem;">Archivos Clave</h4>
<ul class="flow-list">
<li><strong>endpoints.py</strong> - cambiar paths solo aca</li>
<li><strong>helpers.py</strong> - datos de ejemplo</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="6">
<h2><span class="badge tests">3b</span> Tests Frontend</h2>
<div class="two-col">
<div class="col">
<h4>Estructura (amar_frontend)</h4>
<pre>
tests/e2e/
├── <span class="keyword">pages/</span> <span class="comment"># Page Objects</span>
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ └── index.ts
└── login.spec.ts <span class="comment"># test E2E</span>
</pre>
<h4 style="margin-top: 1rem;">Patron Page Object</h4>
<pre>
<span class="keyword">export class</span> LoginPage <span class="keyword">extends</span> BasePage {
<span class="keyword">get</span> emailInput() {
<span class="keyword">return</span> this.page.getByLabel(<span class="string">'Email'</span>);
}
<span class="keyword">async</span> login(email, password) {
<span class="keyword">await</span> this.emailInput.fill(email);
<span class="keyword">await</span> this.passwordInput.fill(password);
<span class="keyword">await</span> this.submitButton.click();
}
}
</pre>
</div>
<div class="col">
<h4>Ejecutar Tests</h4>
<pre>
<span class="comment"># Todos los tests</span>
npx playwright test
<span class="comment"># Con UI</span>
npx playwright test --ui
<span class="comment"># Archivo especifico</span>
npx playwright test login.spec.ts
</pre>
<h4 style="margin-top: 1rem;">Prioridad de Locators</h4>
<ul class="flow-list">
<li>1. getByRole() - botones, links</li>
<li>2. getByLabel() - campos de form</li>
<li>3. getByText() - texto visible</li>
<li>4. getByTestId() - data-testid</li>
</ul>
<h4 style="margin-top: 1rem;">Evitar</h4>
<ul class="flow-list">
<li>Selectores de clases CSS</li>
<li>XPath complejos</li>
</ul>
</div>
</div>
</section>
<section class="slide" data-slide="7">
<h2>Checklist por Feature</h2>
<ul class="checklist">
<li><span class="check"></span> Template ops completado (equipo soporte)</li>
<li><span class="check"></span> Convertir a archivo .feature (spec Gherkin)</li>
<li><span class="check"></span> Backend: Tests de contrato API por endpoint</li>
<li><span class="check"></span> Backend: Test workflow (composicion)</li>
<li><span class="check"></span> Frontend: Page Object (si es pagina nueva)</li>
<li><span class="check"></span> Frontend: E2E spec (Playwright)</li>
<li><span class="check"></span> Conectar a CI</li>
</ul>
<div style="margin-top: 2rem; color: #64748b; font-size: 0.9rem;">
<p><strong>Ejemplo completo:</strong> def/work_plan/10-flow-turnero.md</p>
<p><strong>README Backend:</strong> amar_django_back/tests/contracts/README.md</p>
<p><strong>README Frontend:</strong> amar_frontend/tests/README.md</p>
</div>
</section>
<div class="nav">
<button onclick="prevSlide()"></button>
<span class="counter"><span id="current">1</span>/<span id="total">8</span></span>
<button onclick="nextSlide()"></button>
</div>
<script>
let current = 0;
const slides = document.querySelectorAll('.slide');
const total = slides.length;
document.getElementById('total').textContent = total;
function showSlide(n) {
slides.forEach(s => s.classList.remove('active'));
current = (n + total) % total;
slides[current].classList.add('active');
document.getElementById('current').textContent = current + 1;
}
function nextSlide() { showSlide(current + 1); }
function prevSlide() { showSlide(current - 1); }
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === ' ') nextSlide();
if (e.key === 'ArrowLeft') prevSlide();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,79 @@
# Ops Templates Sample Book
## Purpose
**SAMPLE DATA** - Example filled ops/support templates for demonstration and testing purposes. These are realistic examples to work with while actual definitions are pending from ops/support team.
Use these to:
- Understand the template structure
- Test tooling and workflows
- Demo the feature-flow pipeline
Real templates will follow the same structure but with validated content.
## Structure
```
ops-templates-sample/
├── pet-owner/ # 5 flows for pet owners
│ ├── 01-registro.md
│ ├── 02-reservar-turno.md
│ ├── 03-gestion-mascotas.md
│ ├── 04-pago-turno.md
│ └── 05-historial-medico.md
├── veterinarian/ # 5 flows for vets
│ ├── 01-aceptar-solicitud.md
│ ├── 02-gestion-agenda.md
│ ├── 03-realizar-visita.md
│ ├── 04-zonas-cobertura.md
│ └── 05-historial-pacientes.md
└── backoffice/ # 5 flows for admins
├── 01-gestion-solicitudes.md
├── 02-gestion-usuarios.md
├── 03-gestion-servicios.md
├── 04-reembolsos.md
└── 05-reportes.md
```
## Template Format
Each file follows the standard ops template:
```markdown
# [Nombre del Flujo]
## Tipo de usuario
## Donde empieza
## Que quiere hacer el usuario
## Pasos
## Que deberia pasar
## Problemas comunes
## Casos especiales
## Flujos relacionados
## Notas tecnicas
```
## Corresponding Gherkin
Each template has corresponding `.feature` files in:
- `album/book/gherkin-sample/es/` - Spanish keywords (Dado/Cuando/Entonces)
- `album/book/gherkin-sample/en/` - English keywords (Given/When/Then)
## Usage
1. **Support/Ops**: Use templates to document new flows
2. **Dev**: Convert templates to Gherkin specs
3. **QA**: Use Gherkin to derive test cases
4. **Product**: Review templates for completeness
## Source Template
The base template is at: `album/template/ops-flow/plantilla-flujo.md`
## Coverage
| Area | Flows | Status |
|------|-------|--------|
| Pet Owner | 5 | Complete |
| Veterinarian | 5 | Complete |
| Backoffice | 5 | Complete |
| **Total** | **15** | |

View File

@@ -0,0 +1,335 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ filename }} - Ops Template</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f8fafc;
color: #1e293b;
line-height: 1.6;
}
.container { max-width: 800px; margin: 0 auto; padding: 2rem 1rem; }
header { margin-bottom: 1.5rem; }
.breadcrumb { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; }
.breadcrumb a { color: #15803d; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
h1 { font-size: 1.5rem; color: #15803d; }
.meta {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
font-size: 0.8rem;
}
.meta span {
background: #f1f5f9;
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: #64748b;
}
.meta .sample { background: #fef3c7; color: #92400e; }
/* Form-like styling */
.form-card {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
overflow: hidden;
}
.form-header {
background: linear-gradient(135deg, #15803d, #22c55e);
color: white;
padding: 1rem 1.5rem;
}
.form-header h2 { font-size: 1.1rem; font-weight: 600; }
.form-body { padding: 1.5rem; }
.field {
margin-bottom: 1.25rem;
}
.field:last-child { margin-bottom: 0; }
.field-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin-bottom: 0.35rem;
}
.field-label .edit-icon {
cursor: pointer;
opacity: 0.5;
font-size: 0.85rem;
}
.field-label .edit-icon:hover {
opacity: 1;
}
.field-value {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 0.75rem;
font-size: 0.95rem;
color: #1e293b;
min-height: 2.5rem;
}
.field-value:focus-within {
border-color: #15803d;
outline: none;
box-shadow: 0 0 0 3px rgba(21, 128, 61, 0.1);
}
.field-value.multiline {
min-height: 4rem;
}
.field-value ul, .field-value ol {
margin: 0;
padding-left: 1.25rem;
}
.field-value li {
margin-bottom: 0.25rem;
}
.field-value p {
margin: 0 0 0.5rem 0;
}
.field-value p:last-child { margin-bottom: 0; }
.field-textarea {
display: none;
width: 100%;
min-height: 100px;
padding: 0.75rem;
font-size: 0.95rem;
font-family: inherit;
border: 1px solid #15803d;
border-radius: 6px;
resize: vertical;
box-shadow: 0 0 0 3px rgba(21, 128, 61, 0.1);
}
.field.editing .field-value { display: none; }
.field.editing .field-textarea { display: block; }
/* Special field styles */
.field-steps .field-value {
counter-reset: step;
padding-left: 0.5rem;
}
.field-steps ol {
list-style: none;
padding-left: 0;
}
.field-steps li {
position: relative;
padding-left: 2rem;
margin-bottom: 0.5rem;
}
.field-steps li::before {
counter-increment: step;
content: counter(step);
position: absolute;
left: 0;
width: 1.5rem;
height: 1.5rem;
background: #15803d;
color: white;
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.field-problems .field-value {
background: #fef2f2;
border-color: #fecaca;
}
.field-problems li::before {
content: '!';
color: #dc2626;
font-weight: bold;
margin-right: 0.5rem;
}
.field-special .field-value {
background: #fffbeb;
border-color: #fde68a;
}
.field-technical .field-value {
background: #f0fdf4;
border-color: #bbf7d0;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
}
.field-technical code {
background: #dcfce7;
padding: 0.1rem 0.35rem;
border-radius: 3px;
font-size: 0.85em;
}
.sidebar {
margin-top: 1.5rem;
padding: 1rem;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
}
.sidebar h3 { font-size: 0.8rem; color: #15803d; margin-bottom: 0.75rem; text-transform: uppercase; }
.sidebar a {
display: block;
color: #15803d;
text-decoration: none;
padding: 0.35rem 0;
font-size: 0.9rem;
}
.sidebar a:hover { text-decoration: underline; }
footer {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
font-size: 0.85rem;
}
footer a { color: #15803d; text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="breadcrumb">
<a href="/">Album</a> / <a href="/book/ops-templates/">Ops Templates</a> / {{ user_type }}
</div>
<h1>{{ filename.replace('.md', '').replace('-', ' ').title() }}</h1>
<div class="meta">
<span>{{ user_type }}</span>
<span class="sample">Sample</span>
</div>
</header>
<div class="form-card">
<div class="form-header">
<h2>User Flow Template</h2>
</div>
<div class="form-body" id="form-content">
<!-- Content will be parsed and inserted here -->
</div>
</div>
<div class="sidebar">
<h3>Related</h3>
<a href="/book/gherkin/es/{{ user_type }}/{{ filename.replace('.md', '.feature') }}">Gherkin (Spanish)</a>
<a href="/book/gherkin/en/{{ user_type }}/{{ filename.replace('.md', '.feature') }}">Gherkin (English)</a>
<a href="/book/feature-flow/">Feature Flow Pipeline</a>
</div>
<footer>
<a href="/book/ops-templates/">&larr; All Templates</a>
</footer>
</div>
<script>
const rawContent = {{ content | tojson }};
function parseMarkdown(md) {
const sections = {};
let currentSection = null;
let currentContent = [];
const lines = md.split('\n');
for (const line of lines) {
if (line.startsWith('## ')) {
if (currentSection) {
sections[currentSection] = currentContent.join('\n').trim();
}
currentSection = line.replace('## ', '').trim();
currentContent = [];
} else if (line.startsWith('# ')) {
sections['_title'] = line.replace('# ', '').trim();
} else if (currentSection) {
currentContent.push(line);
}
}
if (currentSection) {
sections[currentSection] = currentContent.join('\n').trim();
}
return sections;
}
function formatContent(content) {
if (!content) return '';
// Convert markdown lists to HTML
let html = content
.replace(/^(\d+)\.\s+(.*)$/gm, '<li>$2</li>')
.replace(/^-\s+(.*)$/gm, '<li>$1</li>')
.replace(/`([^`]+)`/g, '<code>$1</code>');
// Wrap consecutive li elements in ul/ol
if (html.includes('<li>')) {
if (content.match(/^\d+\./m)) {
html = '<ol>' + html + '</ol>';
} else {
html = '<ul>' + html + '</ul>';
}
}
// Convert line breaks to paragraphs for non-list content
if (!html.includes('<li>')) {
html = html.split('\n').filter(l => l.trim()).map(l => '<p>' + l + '</p>').join('');
}
return html;
}
function createField(label, content, className = '') {
if (!content) return '';
return `
<div class="field ${className}">
<label class="field-label">${label} <span class="edit-icon" onclick="toggleEdit(this)" title="Edit field">&#9998;</span></label>
<div class="field-value ${content.includes('\n') ? 'multiline' : ''}">${formatContent(content)}</div>
<textarea class="field-textarea">${content}</textarea>
</div>
`;
}
function toggleEdit(icon) {
const field = icon.closest('.field');
field.classList.toggle('editing');
if (field.classList.contains('editing')) {
field.querySelector('.field-textarea').focus();
}
}
const sections = parseMarkdown(rawContent);
const container = document.getElementById('form-content');
const fieldMap = [
{ key: 'Tipo de usuario', label: 'Tipo de Usuario', class: '' },
{ key: 'Donde empieza', label: 'Punto de Entrada', class: '' },
{ key: 'Que quiere hacer el usuario', label: 'Objetivo del Usuario', class: '' },
{ key: 'Pasos', label: 'Pasos', class: 'field-steps' },
{ key: 'Que deberia pasar', label: 'Resultado Esperado', class: '' },
{ key: 'Problemas comunes', label: 'Problemas Comunes', class: 'field-problems' },
{ key: 'Casos especiales', label: 'Casos Especiales', class: 'field-special' },
{ key: 'Flujos relacionados', label: 'Flujos Relacionados', class: '' },
{ key: 'Notas tecnicas', label: 'Notas Tecnicas', class: 'field-technical' },
];
let html = '';
for (const field of fieldMap) {
if (sections[field.key]) {
html += createField(field.label, sections[field.key], field.class);
}
}
container.innerHTML = html;
</script>
</body>
</html>

View File

@@ -0,0 +1,92 @@
# Gestion de Solicitudes de Servicio
## Tipo de usuario
Administrador / Operaciones
## Donde empieza
Backoffice `/admin/` -> Dashboard o Seccion "Solicitudes"
## Que quiere hacer el usuario
Ver, filtrar y gestionar todas las solicitudes de servicio del sistema
## Pasos - Ver solicitudes
1. Acceder al backoffice
2. Ir a seccion "Solicitudes"
3. Ver listado con todas las solicitudes
4. Usar filtros para encontrar solicitudes especificas:
- Por estado (Pendiente, Coordinado, Pagado, Completado, Cancelado)
- Por fecha
- Por barrio/zona
- Por veterinario
- Por dueno
## Pasos - Ver detalle de solicitud
1. Click en una solicitud del listado
2. Ver toda la informacion:
- Datos del dueno
- Datos de la mascota
- Servicios solicitados
- Fechas preferidas
- Veterinario asignado (si hay)
- Estado de pago
- Historial de cambios de estado
## Pasos - Asignar veterinario manualmente
1. Abrir solicitud en estado "Pendiente"
2. Click en "Asignar veterinario"
3. Ver lista de vets disponibles en la zona
4. Seleccionar veterinario
5. Seleccionar fecha y hora
6. Confirmar asignacion
7. Sistema notifica al vet y al dueno
## Pasos - Cambiar estado manualmente
1. Abrir solicitud
2. Click en "Cambiar estado"
3. Seleccionar nuevo estado
4. Ingresar motivo (si aplica)
5. Confirmar
6. Se registra en el historial
## Que deberia pasar
- Vision completa de todas las solicitudes
- Capacidad de intervenir cuando algo falla
- Trazabilidad de cambios
- Notificaciones a las partes afectadas
## Problemas comunes
- Solicitud sin vet disponible en la zona
- Usuario pago pero webhook fallo
- Vet no puede asistir y hay que reasignar
- Dueno quiere cancelar pero ya pago
- Solicitudes duplicadas del mismo dueno
## Casos especiales
- Solicitud urgente sin vets disponibles
- Reasignacion a ultimo momento
- Cancelacion con reembolso
- Solicitud con datos incompletos
- Dueno no responde a confirmaciones
## Flujos relacionados
- Proceso de reembolso
- Gestion de usuarios
- Reportes
## Notas tecnicas
- APIs:
- `GET /solicitudes/api/v1/service-requests/` (listado con filtros)
- `GET /solicitudes/api/v1/service-requests/{id}/` (detalle)
- `PATCH /solicitudes/api/v1/service-requests/{id}/` (asignar vet)
- `PATCH /solicitudes/api/v1/change-service-request-state/` (cambiar estado)
- Estados validos: pending, coordinated, payed, completed, cancelled
- Permisos: solo staff puede ver todas las solicitudes

View File

@@ -0,0 +1,106 @@
# Gestion de Usuarios
## Tipo de usuario
Administrador / Operaciones
## Donde empieza
Backoffice `/admin/` -> Seccion "Usuarios" o "Duenos" / "Veterinarios"
## Que quiere hacer el usuario
Administrar duenos de mascotas y veterinarios del sistema
## Pasos - Ver/Buscar duenos
1. Ir a "Duenos de mascotas"
2. Ver listado de todos los duenos
3. Usar buscador (nombre, email, telefono)
4. Filtrar por:
- Tipo (Registrado / Invitado)
- Fecha de registro
- Con/sin turnos activos
## Pasos - Ver detalle de dueno
1. Click en un dueno
2. Ver informacion completa:
- Datos personales
- Direcciones registradas
- Mascotas
- Historial de solicitudes
- Historial de pagos
## Pasos - Editar dueno
1. En el detalle, click "Editar"
2. Modificar campos permitidos:
- Nombre
- Telefono
- Email
- Direccion
3. Guardar cambios
## Pasos - Gestionar veterinarios
1. Ir a "Veterinarios"
2. Ver listado con:
- Nombre y matricula
- Zonas de cobertura
- Estado (Activo/Inactivo)
- Turnos del mes
## Pasos - Agregar nuevo veterinario
1. Click en "Agregar veterinario"
2. Completar datos:
- Nombre completo
- Numero de matricula
- Email
- Telefono
- Especialidades
- Zonas de cobertura inicial
3. Crear credenciales de acceso
4. Guardar
## Pasos - Desactivar veterinario
1. Abrir perfil del vet
2. Click en "Desactivar"
3. Confirmar
4. Vet deja de recibir solicitudes
5. Visitas ya aceptadas se mantienen o se reasignan
## Que deberia pasar
- Gestion completa de usuarios del sistema
- Capacidad de corregir datos erroneos
- Alta/baja de veterinarios
- Vision del estado de cada usuario
## Problemas comunes
- Duenos duplicados (se registraron con email diferente)
- Vet quiere darse de baja pero tiene citas pendientes
- Email incorrecto y no reciben notificaciones
- Cuenta de invitado que quiere convertirse en registrada
## Casos especiales
- Fusionar cuentas duplicadas
- Transferir mascota a otro dueno
- Vet de vacaciones temporalmente
- Cambio de numero de matricula
## Flujos relacionados
- Gestion de solicitudes (ver historial por usuario)
- Reportes (filtrar por usuario)
## Notas tecnicas
- APIs:
- `GET /mascotas/api/v1/pet-owners/` (duenos)
- `GET /mascotas/api/v1/veterinarians/` (vets)
- `POST /mascotas/api/v1/veterinarians/` (crear vet)
- `PATCH /mascotas/api/v1/veterinarians/{id}/` (editar/desactivar)
- Permisos: solo staff puede modificar usuarios
- Soft delete para mantener historial

View File

@@ -0,0 +1,98 @@
# Gestion de Servicios y Precios
## Tipo de usuario
Administrador
## Donde empieza
Backoffice `/admin/` -> Seccion "Servicios" o Django Admin -> Productos
## Que quiere hacer el usuario
Administrar el catalogo de servicios veterinarios y sus precios
## Pasos - Ver servicios
1. Ir a "Servicios"
2. Ver listado de todos los servicios
3. Filtrar por:
- Categoria (Vacunacion, Consulta, Cirugia, etc.)
- Tipo de mascota (Perro, Gato, Ambos)
- Estado (Activo/Inactivo)
## Pasos - Agregar nuevo servicio
1. Click en "Agregar servicio"
2. Completar datos:
- Nombre del servicio
- Descripcion
- Categoria
- Tipo de mascota aplicable
- Precio base
- Duracion estimada
- Requiere otros servicios (dependencias)
3. Guardar
## Pasos - Editar servicio
1. Abrir servicio existente
2. Modificar campos
3. Guardar
4. Cambios aplican a nuevas solicitudes
## Pasos - Gestionar precios
1. Abrir servicio
2. Ir a seccion "Precios"
3. Ver precio actual
4. Para cambiar precio:
- Click "Agregar precio"
- Ingresar monto
- Definir fecha de vigencia
- Guardar
5. Precio anterior queda en historial
## Pasos - Configurar combos
1. Ir a "Combos" o "Paquetes"
2. Crear nuevo combo:
- Nombre del combo
- Servicios incluidos
- Precio del combo (con descuento)
- Condiciones de aplicacion
3. Guardar
## Que deberia pasar
- Catalogo actualizado de servicios
- Precios correctos en el turnero
- Combos aplicados automaticamente
- Historial de precios para facturacion
## Problemas comunes
- Cambio de precio afecta solicitudes ya creadas
- Servicio deprecado pero con solicitudes pendientes
- Combo no se aplica automaticamente
- Precio diferente por zona/vet
## Casos especiales
- Servicio con precio variable (depende de peso mascota)
- Servicio estacional (solo ciertas epocas)
- Promocion temporal
- Precio especial por vet
## Flujos relacionados
- Turnero (muestra servicios disponibles)
- Facturacion (usa precios)
## Notas tecnicas
- APIs:
- `GET /productos/api/v1/services/` (servicios)
- `POST /productos/api/v1/services/` (crear)
- `PATCH /productos/api/v1/services/{id}/` (editar)
- `GET /productos/api/v1/prices/` (precios)
- `POST /productos/api/v1/prices/` (nuevo precio)
- Modelo Price tiene fecha de vigencia
- Precio se congela al momento de crear solicitud

View File

@@ -0,0 +1,82 @@
# Proceso de Reembolso
## Tipo de usuario
Administrador / Operaciones
## Donde empieza
Backoffice -> Solicitud pagada -> "Procesar reembolso" o Seccion "Pagos"
## Que quiere hacer el usuario
Procesar un reembolso para un turno que fue cancelado despues del pago
## Pasos - Identificar solicitud a reembolsar
1. Buscar la solicitud por:
- ID de solicitud
- Nombre del dueno
- Numero de operacion de Mercado Pago
2. Verificar que esta en estado "Pagado"
3. Verificar que el pago fue exitoso
## Pasos - Procesar reembolso
1. Abrir detalle de la solicitud
2. Click en "Procesar reembolso"
3. Ver informacion del pago:
- Monto pagado
- Fecha de pago
- Metodo de pago
- ID de operacion MP
4. Seleccionar tipo de reembolso:
- Total
- Parcial (ingresar monto)
5. Ingresar motivo del reembolso
6. Confirmar
7. Sistema procesa reembolso en Mercado Pago
8. Actualizar estado de la solicitud
## Pasos - Verificar reembolso
1. Ir a seccion "Pagos" o historial de la solicitud
2. Verificar que el reembolso fue procesado
3. Ver estado del reembolso en Mercado Pago
## Que deberia pasar
- Reembolso procesado en Mercado Pago
- Dueno recibe el dinero (segun tiempos de MP)
- Solicitud actualiza estado
- Registro en el historial
- Dueno notificado por email
## Problemas comunes
- Reembolso parcial no soportado por el metodo de pago
- Plazo de reembolso vencido (politica de MP)
- Dueno reclama que no recibio el dinero
- Reembolso duplicado por error
- Pago original en disputa
## Casos especiales
- Reembolso por mala atencion
- Reembolso parcial (solo algunos servicios)
- Visita incompleta (reembolso proporcional)
- Dueno quiere credito en vez de reembolso
- Reembolso fuera de plazo (solucion alternativa)
## Flujos relacionados
- Gestion de solicitudes
- Cancelacion de turno
- Atencion al cliente
## Notas tecnicas
- API Mercado Pago: `POST /v1/payments/{id}/refunds`
- Tiempos de acreditacion:
- Tarjeta credito: 1-2 estados de cuenta
- Tarjeta debito: 5-10 dias habiles
- Dinero en cuenta MP: inmediato
- Plazo maximo para reembolso: 180 dias desde el pago
- Se debe guardar log de todos los reembolsos

View File

@@ -0,0 +1,99 @@
# Reportes y Dashboard
## Tipo de usuario
Administrador / Gerencia
## Donde empieza
Backoffice `/admin/` -> Dashboard o Seccion "Reportes"
## Que quiere hacer el usuario
Ver metricas del negocio, generar reportes y analizar datos
## Pasos - Ver dashboard
1. Acceder al backoffice
2. Ver dashboard principal con:
- Solicitudes del dia/semana/mes
- Ingresos del periodo
- Visitas completadas
- Conversion (solicitudes -> pagos)
- Vets activos
## Pasos - Generar reporte de solicitudes
1. Ir a "Reportes" -> "Solicitudes"
2. Seleccionar rango de fechas
3. Filtrar por:
- Estado
- Zona
- Veterinario
- Tipo de servicio
4. Click "Generar"
5. Ver tabla con resultados
6. Opcion de exportar (CSV/Excel)
## Pasos - Generar reporte de ingresos
1. Ir a "Reportes" -> "Ingresos"
2. Seleccionar periodo
3. Agrupar por:
- Dia/Semana/Mes
- Veterinario
- Servicio
- Zona
4. Ver grafico y tabla
5. Exportar si es necesario
## Pasos - Reporte de veterinarios
1. Ir a "Reportes" -> "Veterinarios"
2. Ver para cada vet:
- Visitas completadas
- Calificacion promedio
- Ingresos generados
- Tasa de cancelacion
3. Filtrar por periodo
4. Ordenar por metrica
## Pasos - Exportar datos
1. En cualquier reporte, click "Exportar"
2. Seleccionar formato (CSV, Excel)
3. Descargar archivo
4. Usar para analisis externo o contabilidad
## Que deberia pasar
- Vision clara del estado del negocio
- Datos para toma de decisiones
- Reportes exportables para contabilidad
- Identificacion de tendencias
## Problemas comunes
- Datos no actualizados en tiempo real
- Filtros que no devuelven lo esperado
- Discrepancia entre reportes y realidad
- Exportacion con formato incorrecto
## Casos especiales
- Reporte de prueba vs produccion
- Datos historicos antes de implementar filtro
- Comparativa año vs año
- Reporte personalizado para inversores
## Flujos relacionados
- Gestion de solicitudes (fuente de datos)
- Facturacion AFIP
- Google Sheets sync
## Notas tecnicas
- API:
- `GET /mascotas/api/v1/stats-summary` (metricas principales)
- `GET /solicitudes/api/v1/service-requests/?date_from=X&date_to=Y` (datos crudos)
- Google Sheets: sincronizacion automatica via Celery
- Dashboard actualiza cada 5 minutos
- Reportes pesados: generar async y notificar cuando estan listos

View File

@@ -0,0 +1,55 @@
# Registro de Usuario
## Tipo de usuario
Dueno de mascota (nuevo)
## Donde empieza
Pagina principal -> Boton "Registrarse" o `/register`
## Que quiere hacer el usuario
Crear una cuenta nueva para poder reservar turnos y gestionar sus mascotas
## Pasos
1. Click en "Registrarse" en la pagina principal
2. Ingresar email
3. Ingresar contraseña
4. Confirmar contraseña
5. Aceptar terminos y condiciones
6. Click en "Crear cuenta"
7. Recibir email de verificacion
8. Click en el link de verificacion
9. Cuenta activada, redirige al dashboard
## Que deberia pasar
- Usuario creado en el sistema
- Email de bienvenida enviado
- Puede iniciar sesion inmediatamente
- Ve su dashboard vacio (sin mascotas, sin turnos)
## Problemas comunes
- El email ya esta registrado pero el usuario no lo recuerda
- El email de verificacion llega a spam
- La contraseña no cumple los requisitos minimos
- El usuario cierra el browser antes de verificar y no encuentra el email despues
- Usuarios intentan registrarse con email de otra persona
## Casos especiales
- Si el email existe como usuario invitado (del turnero), deberia linkear las cuentas
- Si el usuario ya tiene cuenta pero no verificada, reenviar email de verificacion
- Registro desde el flujo de turnero (ya ingreso datos, solo falta contraseña)
## Flujos relacionados
- Login (despues de registrarse)
- Turnero (puede disparar registro)
- Recuperar contraseña (si ya tenia cuenta y no recuerda)
## Notas tecnicas
- API: `POST /common/users/create/`
- Validacion de contraseña: minimo 8 caracteres, 1 numero, 1 mayuscula
- Verificacion expira en 24 horas

View File

@@ -0,0 +1,77 @@
# Reservar Turno (Turnero)
## Tipo de usuario
Dueno de mascota (invitado, registrado, o recurrente)
## Donde empieza
Pagina principal -> Boton "Agendar visita" o enlace directo `/turnero`
## Que quiere hacer el usuario
Reservar un turno veterinario a domicilio para su mascota
## Pasos
1. Click en "Agendar visita"
2. Ingresar direccion en el popup de cobertura
3. Sistema verifica si hay cobertura en esa zona
4. Si hay cobertura, click "Siguiente"
5. Completar datos de la mascota:
- Nombre
- Tipo (Perro/Gato)
- Raza (opcional)
- Edad aproximada
- Peso aproximado
- Si esta castrado/a
6. Seleccionar categoria de servicio (Vacunacion, Consulta, etc.)
7. Seleccionar servicios especificos
8. Ver que algunos servicios se agregan automaticamente (ej: consulta clinica)
9. Seleccionar fechas preferidas (hasta 3)
10. Seleccionar franja horaria preferida
11. Ingresar datos de contacto:
- Nombre completo
- Telefono
- Email
12. Revisar resumen
13. Enviar solicitud
## Que deberia pasar
- Solicitud creada con estado "Pendiente"
- Email de confirmacion al usuario
- Notificacion a veterinarios de la zona
- Aparece en "Mis turnos" del usuario
## Problemas comunes
- No hay cobertura en la zona del usuario
- El autocompletado de direcciones no encuentra la calle
- Usuario no puede quitar "Consulta clinica" cuando selecciona vacunacion
- Confunden "fecha preferida" con "fecha confirmada"
- No entienden por que no pueden elegir hora exacta
- El total no se muestra hasta el final y sorprende al usuario
## Casos especiales
- Usuario invitado: se crea como guest, puede registrarse despues
- Usuario logueado: datos pre-llenados, mascota puede ser existente
- Usuario recurrente (mismo email): sistema detecta y sugiere linkear
- Mascota ya castrada: no mostrar servicio de castracion
- Servicios combo: algunos incluyen otros automaticamente
- Zona sin cobertura: mostrar formulario para avisar cuando haya
## Flujos relacionados
- Ver mis turnos (despues de reservar)
- Pagar turno (cuando se coordina)
- Cancelar turno
- Agregar mascota (si ya tiene cuenta)
## Notas tecnicas
- APIs:
- `POST /mascotas/api/v1/coverage-check/` (verificar cobertura)
- `POST /mascotas/api/v1/pet-owners/` (crear dueno invitado)
- `POST /mascotas/api/v1/pets/` (crear mascota)
- `GET /productos/api/v1/services/` (obtener servicios)
- `POST /solicitudes/api/v1/service-requests/` (crear solicitud)
- Estados de solicitud: pending -> coordinated -> payed -> completed

View File

@@ -0,0 +1,79 @@
# Gestion de Mascotas
## Tipo de usuario
Dueno de mascota (registrado)
## Donde empieza
Dashboard -> Seccion "Mis mascotas" o `/user/pets`
## Que quiere hacer el usuario
Agregar, editar o ver informacion de sus mascotas
## Pasos - Agregar mascota
1. Ir a "Mis mascotas"
2. Click en "Agregar mascota"
3. Completar formulario:
- Nombre (obligatorio)
- Tipo: Perro o Gato (obligatorio)
- Raza (opcional, lista segun tipo)
- Fecha de nacimiento o edad aproximada
- Peso en kg (opcional)
- Sexo
- Esta castrado/a (checkbox)
- Foto (opcional)
4. Guardar
## Pasos - Editar mascota
1. Ir a "Mis mascotas"
2. Click en la tarjeta de la mascota
3. Click en "Editar"
4. Modificar campos deseados
5. Guardar
## Pasos - Ver historial medico
1. Ir a "Mis mascotas"
2. Click en la tarjeta de la mascota
3. Ver seccion "Historial de visitas"
4. Click en una visita para ver el informe completo
## Que deberia pasar
- Mascota aparece en el listado
- Disponible para seleccionar en el turnero
- Historial medico accesible
- Foto visible en la tarjeta
## Problemas comunes
- Usuarios quieren agregar mascotas de otros tipos (conejo, ave) - no soportado
- No encuentran donde ver el historial medico
- Quieren eliminar mascota pero tiene turnos pendientes
- Foto muy grande no sube
- Confunden edad con fecha de nacimiento
## Casos especiales
- Mascota creada desde turnero como invitado: aparece cuando linkea cuenta
- Mascota fallecida: deberia poder marcarse (soft delete)
- Multiples mascotas con mismo nombre: permitido pero confuso
- Cambio de peso significativo: registrar historial?
## Flujos relacionados
- Reservar turno (seleccionar mascota existente)
- Ver historial medico
- Registro (crear primera mascota)
## Notas tecnicas
- APIs:
- `GET /mascotas/api/v1/pets/` (listar)
- `POST /mascotas/api/v1/pets/` (crear)
- `PATCH /mascotas/api/v1/pets/{id}/` (editar)
- `DELETE /mascotas/api/v1/pets/{id}/` (soft delete)
- `GET /mascotas/api/v1/vet-visits/?pet={id}` (historial)
- Tipos soportados: solo Perro y Gato por ahora
- Razas: lista predefinida por tipo

View File

@@ -0,0 +1,66 @@
# Pago de Turno
## Tipo de usuario
Dueno de mascota (con turno coordinado)
## Donde empieza
Email de confirmacion de turno -> Link "Pagar" o Dashboard -> Mis turnos -> Boton "Pagar"
## Que quiere hacer el usuario
Pagar un turno que ya fue coordinado con fecha y veterinario asignado
## Pasos
1. Recibir email/notificacion de que el turno fue coordinado
2. Ver detalle del turno con fecha, hora y veterinario asignado
3. Verificar el monto total
4. Click en "Pagar"
5. Redireccion a Mercado Pago
6. Seleccionar metodo de pago:
- Tarjeta de credito/debito
- Dinero en cuenta MP
- Transferencia bancaria
- Efectivo (Rapipago/PagoFacil)
7. Completar pago
8. Redireccion de vuelta a la plataforma
9. Ver confirmacion de pago exitoso
## Que deberia pasar
- Estado del turno cambia a "Pagado"
- Email de confirmacion de pago
- Comprobante disponible
- Veterinario notificado
- Turno aparece como confirmado en el calendario
## Problemas comunes
- Usuario cierra el browser durante el pago y no sabe si se proceso
- Pago rechazado por fondos insuficientes
- Link de pago expira y no pueden pagr
- Quieren pagar en efectivo el dia de la visita
- Precio cambio entre coordinacion y pago
- Quieren aplicar descuento/cupon y no encuentran donde
## Casos especiales
- Pago parcial: no soportado actualmente
- Pago con tarjeta rechazada: puede reintentar
- Pago por transferencia: demora en acreditarse (pendiente de confirmacion)
- Usuario paga pero webhook falla: requiere intervencion manual
- Turno cancelado despues de pagar: proceso de reembolso
## Flujos relacionados
- Ver mis turnos (antes y despues de pagar)
- Cancelar turno (si decide no pagar)
- Contactar soporte (problemas con pago)
## Notas tecnicas
- APIs:
- `POST /payments/api/v1/requests/{id}/preference/` (generar link MP)
- Webhook: `POST /payments/mp/webhook/` (notificacion de MP)
- Integracion: Mercado Pago Checkout Pro
- Estados: coordinated -> payed (despues de webhook exitoso)
- Timeout de preferencia: 24 horas

View File

@@ -0,0 +1,79 @@
# Ver Historial Medico
## Tipo de usuario
Dueno de mascota (registrado, con visitas completadas)
## Donde empieza
Dashboard -> Mis mascotas -> Mascota -> "Ver historial" o Mis turnos -> Turno completado -> "Ver informe"
## Que quiere hacer el usuario
Ver los informes medicos de las visitas veterinarias de su mascota
## Pasos - Desde mascotas
1. Ir a "Mis mascotas"
2. Seleccionar la mascota
3. Ver seccion "Historial de visitas"
4. Lista de visitas ordenadas por fecha (mas reciente primero)
5. Click en una visita para ver detalle
## Pasos - Desde turnos
1. Ir a "Mis turnos"
2. Filtrar por "Completados"
3. Click en un turno completado
4. Click en "Ver informe medico"
## Que muestra el informe
- Fecha de la visita
- Veterinario que atendio
- Motivo de consulta / servicios realizados
- Examen fisico:
- Peso registrado
- Temperatura
- Frecuencia cardiaca
- Observaciones generales
- Diagnostico
- Tratamiento indicado
- Medicamentos recetados (nombre, dosis, frecuencia, duracion)
- Estudios solicitados
- Proximos pasos / seguimiento recomendado
- Observaciones adicionales
## Que deberia pasar
- Usuario puede ver todos los informes de sus mascotas
- Puede descargar/imprimir el informe
- Informacion clara y entendible
## Problemas comunes
- Visita completada pero informe no cargado todavia
- Usuario quiere editar algo del informe (no puede, es del vet)
- Terminologia medica confusa
- No encuentra informe de visita antigua
- Quiere compartir informe con otro veterinario
## Casos especiales
- Visita sin informe: vet no lo cargo (deberia ser obligatorio)
- Informe incompleto: campos vacios
- Multiples visitas mismo dia: mostrar todas
- Mascota transferida de otro dueno: historial previo?
- Estudios pendientes: mostrar estado
## Flujos relacionados
- Gestion de mascotas
- Reservar turno de seguimiento
- Contactar veterinario (dudas sobre informe)
## Notas tecnicas
- APIs:
- `GET /mascotas/api/v1/vet-visits/?pet={id}` (listar visitas)
- `GET /mascotas/api/v1/vet-visits/{id}/` (detalle visita)
- `GET /mascotas/api/v1/vetvisitreport/?vet_visit={id}` (informe)
- PDF: generado on-demand o pre-generado?
- Permisos: solo dueno de la mascota puede ver

View File

@@ -0,0 +1,77 @@
# Aceptar/Rechazar Solicitud de Servicio
## Tipo de usuario
Veterinario
## Donde empieza
Dashboard veterinario `/vet/` -> Seccion "Solicitudes pendientes"
## Que quiere hacer el usuario
Revisar las solicitudes de servicio en su zona y decidir si las acepta o rechaza
## Pasos - Aceptar solicitud
1. Ingresar al dashboard veterinario
2. Ver listado de solicitudes pendientes en su zona de cobertura
3. Click en una solicitud para ver detalle:
- Datos del dueno (nombre, telefono, direccion)
- Datos de la mascota (nombre, tipo, edad, historial previo)
- Servicios solicitados
- Fechas preferidas por el dueno
4. Verificar disponibilidad en esas fechas
5. Click en "Aceptar"
6. Seleccionar fecha y hora disponible
7. Confirmar aceptacion
## Pasos - Rechazar solicitud
1. Ver detalle de la solicitud
2. Click en "Rechazar"
3. Opcionalmente seleccionar motivo:
- No tengo disponibilidad
- Fuera de mi zona
- No realizo este servicio
- Otro
4. Confirmar rechazo
## Que deberia pasar
Al aceptar:
- Solicitud pasa a estado "Coordinado"
- Vet queda asignado a la solicitud
- Dueno recibe notificacion con fecha/hora y datos del vet
- Aparece en el calendario del vet
Al rechazar:
- Solicitud sigue disponible para otros vets
- Si la rechazo yo, ya no la veo en mi lista
## Problemas comunes
- Multiples vets aceptan la misma solicitud casi simultaneamente
- El vet acepta pero el dueno no puede en esa fecha
- No hay suficientes vets en la zona y la solicitud queda sin atender
- Vet acepta por error y quiere deshacer
- Las fechas preferidas ya pasaron
## Casos especiales
- Solicitud con mascota que el vet ya atendio antes: mostrar historial
- Solicitud urgente/emergencia: destacar visualmente
- Vet con agenda completa: no deberia poder aceptar
- Solicitud con multiples mascotas: revisar tiempo necesario
- Zona limite: solicitud que aparece para vets de zonas adyacentes
## Flujos relacionados
- Ver mi agenda (despues de aceptar)
- Realizar visita
- Cancelar visita aceptada
## Notas tecnicas
- APIs:
- `GET /solicitudes/api/v1/service-requests/?state=pending&veterinarian_area=X`
- `PATCH /solicitudes/api/v1/service-requests/{id}/` (aceptar/rechazar)
- Race condition: backend debe manejar caso de aceptacion simultanea
- Push notification al dueno cuando se acepta

View File

@@ -0,0 +1,87 @@
# Gestion de Agenda
## Tipo de usuario
Veterinario
## Donde empieza
Dashboard veterinario `/vet/` -> Menu "Mi agenda" o `/vet/turnos`
## Que quiere hacer el usuario
Ver su calendario de visitas, gestionar disponibilidad y marcar dias no disponibles
## Pasos - Ver agenda
1. Ir a "Mi agenda"
2. Ver calendario con:
- Visitas confirmadas (con detalle al hacer click)
- Dias/horarios bloqueados
- Disponibilidad por franja horaria
3. Navegar entre semanas/meses
4. Filtrar por estado (pendientes, confirmadas, completadas)
## Pasos - Configurar disponibilidad semanal
1. Ir a "Configuracion" -> "Mi disponibilidad"
2. Para cada dia de la semana:
- Marcar si trabajo ese dia
- Definir franja horaria (ej: 9:00-18:00)
- Definir pausa (ej: 13:00-14:00)
3. Guardar configuracion
## Pasos - Bloquear dias especificos
1. En el calendario, click en un dia
2. Seleccionar "Marcar como no disponible"
3. Opcionalmente agregar motivo (vacaciones, curso, etc.)
4. Confirmar
5. Ese dia no aparece como opcion para nuevas solicitudes
## Pasos - Ver detalle de visita
1. Click en una visita en el calendario
2. Ver informacion completa:
- Dueno: nombre, telefono, direccion
- Mascota: nombre, tipo, edad, foto
- Servicios a realizar
- Historial previo de esa mascota
- Estado de pago
- Notas especiales
## Que deberia pasar
- Calendario siempre actualizado con visitas
- No puedo recibir solicitudes en dias bloqueados
- Cambios de disponibilidad afectan solo solicitudes futuras
- Puedo ver toda la informacion necesaria para la visita
## Problemas comunes
- Vet olvida desbloquear dia despues de terminar vacaciones
- Visitas muy seguidas en zonas distantes (logistica)
- Vet quiere mover visita a otro horario
- Visita cancelada a ultimo momento
- Vet no marco indisponibilidad y le llegan solicitudes que no puede atender
## Casos especiales
- Visita reprogramada: mostrar historial de cambios
- Visita cancelada por el dueno: liberar espacio en agenda
- Emergencia: poder agregar visita fuera de horario normal
- Feriados: bloquear automaticamente?
- Multiples visitas mismo dia/zona: optimizar ruta?
## Flujos relacionados
- Aceptar solicitud (agrega a la agenda)
- Realizar visita (inicia desde la agenda)
- Gestionar zonas de cobertura
## Notas tecnicas
- APIs:
- `GET /mascotas/api/v1/vet-visits/?veterinarian=X`
- `GET /mascotas/api/v1/vet_availabilities/`
- `PUT /mascotas/api/v1/vet_availabilities/{id}/`
- `POST /mascotas/api/v1/vet_unavailabilities/` (dias bloqueados)
- Vista calendario: usar libreria tipo FullCalendar
- Colores por estado: pendiente (amarillo), pagado (verde), completado (azul)

View File

@@ -0,0 +1,97 @@
# Realizar Visita y Crear Informe
## Tipo de usuario
Veterinario
## Donde empieza
Agenda del dia -> Visita programada -> "Iniciar visita" o `/vet/visita/{id}`
## Que quiere hacer el usuario
Registrar la visita medica, crear el informe clinico y completar el flujo
## Pasos - Antes de la visita
1. Ver agenda del dia
2. Click en la visita programada
3. Ver datos del dueno (direccion, telefono) para llegar
4. Ver datos de la mascota e historial
5. Preparar insumos segun servicios solicitados
## Pasos - Iniciar visita
1. Al llegar al domicilio, click "Iniciar visita"
2. Estado cambia a "En progreso"
3. Se registra hora de inicio
## Pasos - Crear informe medico
1. Durante o despues de la atencion, ir a "Crear informe"
2. Completar examen fisico:
- Peso actual
- Temperatura
- Frecuencia cardiaca
- Frecuencia respiratoria
- Estado general (mucosas, hidratacion, etc.)
3. Registrar hallazgos clinicos
4. Escribir diagnostico
5. Indicar tratamiento:
- Descripcion del tratamiento
- Medicamentos recetados (nombre, dosis, frecuencia, duracion)
- Indicaciones especiales
6. Solicitar estudios si es necesario:
- Tipo de estudio
- Laboratorio sugerido
7. Programar seguimiento:
- Fecha sugerida de control
- Motivo del seguimiento
8. Agregar observaciones adicionales
9. Guardar informe
## Pasos - Completar visita
1. Verificar que el informe este guardado
2. Click en "Completar visita"
3. Confirmar finalizacion
4. Se genera factura automaticamente (AFIP)
5. Dueno recibe notificacion con acceso al informe
## Que deberia pasar
- Informe queda guardado y asociado a la mascota
- Dueno puede ver el informe desde su cuenta
- Se genera factura electronica
- Visita se marca como "Completada"
- Vet puede ver la visita en su historial
## Problemas comunes
- Vet olvida completar el informe y cierra la visita
- Mala conexion a internet durante la visita
- Informe muy largo y se pierde por timeout
- Dueno no esta en el domicilio
- Mascota agresiva, no se puede atender
- Necesita derivar a clinica
## Casos especiales
- Visita cancelada in situ: registrar motivo, posible cargo
- Emergencia durante visita rutinaria: agregar servicios
- Multiples mascotas en la visita: un informe por cada una
- Vet necesita segundo opinion: derivar a especialista
- Fallecimiento durante la visita: protocolo especial
## Flujos relacionados
- Ver historial de paciente (antes de atender)
- Cobro de la visita (Mercado Pago del vet)
- Seguimiento post-visita
## Notas tecnicas
- APIs:
- `PATCH /mascotas/api/v1/vet-visits/{id}/` (cambiar estado)
- `POST /mascotas/api/v1/vetvisitreport/` (crear informe)
- `GET /mascotas/api/v1/vet-visits/?pet=X` (historial previo)
- Integracion AFIP via django_afip
- Guardado automatico del informe cada 30 segundos
- Modo offline: guardar localmente y sincronizar despues

View File

@@ -0,0 +1,66 @@
# Gestionar Zonas de Cobertura
## Tipo de usuario
Veterinario
## Donde empieza
Dashboard veterinario -> Menu "Mi cobertura" o Perfil -> "Zonas de atencion"
## Que quiere hacer el usuario
Definir en que barrios/zonas esta dispuesto a atender visitas a domicilio
## Pasos - Ver zonas actuales
1. Ir a "Mi cobertura"
2. Ver mapa con zonas actualmente cubiertas
3. Ver listado de barrios seleccionados
## Pasos - Agregar zona
1. En el mapa o listado, buscar el barrio
2. Click en el barrio para seleccionarlo
3. El barrio se agrega a mi cobertura
4. Guardar cambios
## Pasos - Quitar zona
1. En mi listado de zonas
2. Click en "X" o deseleccionar el barrio
3. Confirmar eliminacion
4. Guardar cambios
## Que deberia pasar
- Solo recibo solicitudes de las zonas que seleccione
- Si agrego zona, empiezo a ver solicitudes pendientes de esa zona
- Si quito zona, dejo de ver nuevas solicitudes de esa zona
- Visitas ya aceptadas no se afectan
## Problemas comunes
- Vet quiere zona muy especifica (solo algunas calles)
- Zonas con poca demanda, vet no recibe solicitudes
- Zonas con mucha demanda, vet saturado
- Limites de zona confusos (calle pertenece a 2 barrios)
- Vet quiere cobertura condicional (solo ciertos dias)
## Casos especiales
- Zona nueva no listada: solicitar a ops que la agregue
- Cobertura temporal (ej: cubriendo a otro vet)
- Cobertura por horario (mañana en zona A, tarde en zona B)
- Vet se muda: actualizar todas las zonas
## Flujos relacionados
- Aceptar solicitud (solo veo solicitudes de mis zonas)
- Ver agenda (optimizar ruta por zonas)
## Notas tecnicas
- APIs:
- `GET /common/api/v1/neighborhoods/` (lista de barrios)
- `GET /mascotas/api/v1/veterinarians/{id}/` (zonas actuales)
- `PATCH /mascotas/api/v1/veterinarians/{id}/` (actualizar zonas)
- Modelo: Veterinarian tiene M2M con Neighborhood
- Coeficiente de distancia por barrio para calcular viabilidad

View File

@@ -0,0 +1,81 @@
# Ver Historial de Pacientes
## Tipo de usuario
Veterinario
## Donde empieza
Dashboard veterinario -> "Historia clinica" o `/vet/historia-clinica`
## Que quiere hacer el usuario
Buscar y revisar el historial medico de pacientes que ha atendido
## Pasos - Buscar paciente
1. Ir a "Historia clinica"
2. Usar el buscador:
- Por nombre del dueno
- Por nombre de la mascota
- Por telefono
- Por email
3. Ver resultados de busqueda
4. Click en el paciente deseado
## Pasos - Ver historial de mascota
1. Seleccionar la mascota
2. Ver ficha de la mascota:
- Datos basicos (nombre, tipo, raza, edad)
- Foto
- Peso historico
- Estado de vacunacion
3. Ver listado de visitas (todas, no solo las mias)
4. Click en una visita para ver el informe completo
## Pasos - Revisar informe anterior
1. En el listado de visitas, click en una
2. Ver informe completo:
- Fecha y veterinario que atendio
- Examen fisico
- Diagnostico
- Tratamiento
- Medicamentos indicados
- Estudios solicitados
- Seguimiento
## Que deberia pasar
- Puedo ver historial completo de pacientes que atendi
- Puedo ver informes de otros vets (para continuidad de atencion)
- Tengo contexto para tomar decisiones clinicas
## Problemas comunes
- Busqueda no encuentra al paciente (nombre mal escrito)
- Mascota atendida por otro vet, no tengo acceso?
- Historial muy extenso, dificil encontrar info relevante
- Informes anteriores incompletos o ilegibles
- Duplicados de pacientes (mismo dueno registrado 2 veces)
## Casos especiales
- Mascota nueva sin historial
- Mascota atendida en otra clinica (historial externo)
- Dueno tiene multiples mascotas
- Mascota transferida a otro dueno
- Paciente de urgencia/emergencia sin cita previa
## Flujos relacionados
- Realizar visita (consultar historial antes/durante)
- Aceptar solicitud (ver si ya lo atendi antes)
## Notas tecnicas
- APIs:
- `GET /mascotas/api/v1/pet-owners/?search=X`
- `GET /mascotas/api/v1/pets/?pet_owner=X`
- `GET /mascotas/api/v1/vet-visits/?pet=X`
- `GET /mascotas/api/v1/vetvisitreport/?vet_visit=X`
- Permisos: vet puede ver historial de cualquier paciente que haya atendido
- Busqueda: usar indice de texto completo para rendimiento

View File

@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ops Templates Sample - Album</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f8fafc;
color: #1e293b;
line-height: 1.6;
}
.container { max-width: 1000px; margin: 0 auto; padding: 2rem 1rem; }
header { margin-bottom: 2rem; }
.breadcrumb { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; }
.breadcrumb a { color: #15803d; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
h1 { font-size: 2rem; color: #15803d; }
.subtitle { color: #64748b; margin-top: 0.5rem; }
.sample-badge {
display: inline-block;
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
margin-top: 0.5rem;
}
.user-type {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.user-type h2 {
font-size: 1.25rem;
color: #15803d;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-type h2::before {
content: '';
width: 8px;
height: 8px;
background: #22c55e;
border-radius: 50%;
}
.templates-list { list-style: none; }
.templates-list li {
border-bottom: 1px solid #e2e8f0;
}
.templates-list li:last-child { border-bottom: none; }
.templates-list a {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
text-decoration: none;
color: #1e293b;
}
.templates-list a:hover { color: #15803d; }
.templates-list .name { font-weight: 500; }
.templates-list .arrow { color: #94a3b8; }
.info-box {
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
padding: 1rem;
margin-top: 2rem;
font-size: 0.9rem;
}
.info-box h3 { font-size: 0.9rem; color: #15803d; margin-bottom: 0.5rem; }
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
font-size: 0.85rem;
}
footer a { color: #15803d; text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="breadcrumb"><a href="/">Album</a> / Ops Templates</div>
<h1>Ops Templates Sample</h1>
<p class="subtitle">User flow documentation templates - non-technical format for support/ops teams</p>
<span class="sample-badge">Sample Data</span>
</header>
<div class="user-type">
<h2>Pet Owner</h2>
<ul class="templates-list">
<li><a href="/book/feature-form-samples/larder/pet-owner/01-registro.md"><span class="name">01. Registro de Usuario</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/pet-owner/02-reservar-turno.md"><span class="name">02. Reservar Turno</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/pet-owner/03-gestion-mascotas.md"><span class="name">03. Gestion de Mascotas</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/pet-owner/04-pago-turno.md"><span class="name">04. Pago de Turno</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/pet-owner/05-historial-medico.md"><span class="name">05. Historial Medico</span><span class="arrow">&rarr;</span></a></li>
</ul>
</div>
<div class="user-type">
<h2>Veterinarian</h2>
<ul class="templates-list">
<li><a href="/book/feature-form-samples/larder/veterinarian/01-aceptar-solicitud.md"><span class="name">01. Aceptar/Rechazar Solicitud</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/veterinarian/02-gestion-agenda.md"><span class="name">02. Gestion de Agenda</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/veterinarian/03-realizar-visita.md"><span class="name">03. Realizar Visita</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/veterinarian/04-zonas-cobertura.md"><span class="name">04. Zonas de Cobertura</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/veterinarian/05-historial-pacientes.md"><span class="name">05. Historial de Pacientes</span><span class="arrow">&rarr;</span></a></li>
</ul>
</div>
<div class="user-type">
<h2>Backoffice</h2>
<ul class="templates-list">
<li><a href="/book/feature-form-samples/larder/backoffice/01-gestion-solicitudes.md"><span class="name">01. Gestion de Solicitudes</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/backoffice/02-gestion-usuarios.md"><span class="name">02. Gestion de Usuarios</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/backoffice/03-gestion-servicios.md"><span class="name">03. Gestion de Servicios</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/backoffice/04-reembolsos.md"><span class="name">04. Reembolsos</span><span class="arrow">&rarr;</span></a></li>
<li><a href="/book/feature-form-samples/larder/backoffice/05-reportes.md"><span class="name">05. Reportes</span><span class="arrow">&rarr;</span></a></li>
</ul>
</div>
<div class="info-box">
<h3>Template Structure</h3>
<p>Each template includes: User type, Entry point, Goal, Steps, Expected result, Common problems, Edge cases, Related flows, Technical notes.</p>
<p style="margin-top: 0.5rem;">These templates transform into <a href="/book/gherkin-samples/">Gherkin specs</a> for testing.</p>
</div>
<footer>
<a href="/">&larr; Album</a>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,91 @@
# Plantilla: Documentacion de Flujos
## Para: Equipo de Soporte/Operaciones
Usa esta plantilla para documentar cualquier accion/flujo del sistema.
---
## Plantilla
```
### [Nombre del Flujo]
**Tipo de usuario:** [Dueno de mascota / Veterinario / Admin]
**Donde empieza:** [Que pagina/boton/link]
**Que quiere hacer el usuario:** [Objetivo en una oracion]
**Pasos:**
1. [Primera cosa que hace el usuario]
2. [Segunda cosa que hace el usuario]
3. [etc.]
**Que deberia pasar:** [Resultado esperado cuando todo funciona]
**Problemas comunes:**
- [Problema 1]
- [Problema 2]
**Casos especiales:**
- [Caso especial 1]
- [Caso especial 2]
**Flujos relacionados:** [Otros flujos que se conectan con este]
```
---
## Ejemplo Completo
### Reservar turno de vacunacion
**Tipo de usuario:** Dueno de mascota
**Donde empieza:** Pagina principal -> Boton "Agendar visita"
**Que quiere hacer el usuario:** Reservar un turno de vacunacion para su gato
**Pasos:**
1. Click en "Agendar visita" en la pagina principal
2. Ingresar direccion en el popup
3. Click en "Siguiente"
4. Completar datos mascota: nombre, tipo (gato), edad
5. Seleccionar categoria "Vacunacion"
6. Elegir la vacuna especifica
7. Ver que "Consulta clinica" se agrega automaticamente
8. Elegir fechas preferidas
9. Ingresar datos de contacto (nombre, telefono, email)
10. Enviar la solicitud
**Que deberia pasar:**
- Ver mensaje de confirmacion
- Recibir email de confirmacion
- La solicitud aparece en "Mis turnos" con estado "Pendiente"
**Problemas comunes:**
- Los usuarios intentan quitar "Consulta clinica" y no pueden
- El autocompletado de direcciones a veces no encuentra su calle
- Algunos usuarios no ven el link "Ya soy cliente" y crean cuentas duplicadas
**Casos especiales:**
- Si la mascota ya esta castrada, el servicio de castracion no deberia aparecer
- Si el usuario abandona a mitad del flujo, sus datos se guardan como "invitado"
- Algunos barrios no tienen cobertura - deberia mostrar un error claro
**Flujos relacionados:**
- "Ver mis turnos" (para ver el estado despues de reservar)
- "Pagar turno" (cuando se solicita el pago)
---
## Preguntas a Considerar
1. **Camino feliz:** Que pasa cuando todo funciona perfecto?
2. **Validacion:** Que errores puede ver el usuario?
3. **Permisos:** Quien puede hacer esto?
4. **Estados:** Esta accion cambia segun algun estado?
5. **Dependencias:** Esto requiere que algo mas haya pasado antes?
6. **Efectos secundarios:** Esto dispara emails, notificaciones?
7. **Deshacer:** El usuario puede revertir esta accion?

View File

@@ -0,0 +1,114 @@
# Gherkin Sample Book
## Purpose
**SAMPLE DATA** - Example BDD/Gherkin `.feature` files for demonstration and testing purposes. These show realistic examples of how feature files will look when actual ops templates are defined.
Use these to:
- Understand Gherkin structure and patterns
- Compare Spanish vs English keywords
- Test tooling (behave, pytest-bdd, playwright-bdd)
- Demo the feature-flow pipeline
Real feature files will be derived from validated ops templates.
## Structure
```
gherkin-sample/
├── es/ # Spanish keywords (Dado/Cuando/Entonces)
│ ├── pet-owner/
│ ├── veterinarian/
│ └── backoffice/
└── en/ # English keywords (Given/When/Then)
├── pet-owner/
├── veterinarian/
└── backoffice/
```
## Language Versions
**Spanish Keywords (`es/`)**
- Uses `# language: es` directive
- Keywords: Característica, Escenario, Dado, Cuando, Entonces, Y, Pero
- Keywords: Esquema del escenario, Ejemplos, Antecedentes, Regla
- For teams preferring Spanish BDD syntax
**English Keywords (`en/`)**
- Standard Gherkin syntax
- Keywords: Feature, Scenario, Given, When, Then, And, But
- Keywords: Scenario Outline, Examples, Background, Rule
- Same Spanish content, different keywords
## Feature Files
### Pet Owner (5 files)
| File | Description |
|------|-------------|
| 01-registro.feature | User registration flow |
| 02-reservar-turno.feature | Book appointment (turnero) |
| 03-gestion-mascotas.feature | Pet management |
| 04-pago-turno.feature | Payment flow |
| 05-historial-medico.feature | Medical history |
### Veterinarian (5 files)
| File | Description |
|------|-------------|
| 01-aceptar-solicitud.feature | Accept/reject requests |
| 02-gestion-agenda.feature | Schedule management |
| 03-realizar-visita.feature | Conduct visit & report |
| 04-zonas-cobertura.feature | Coverage area management |
| 05-historial-pacientes.feature | Patient history |
### Backoffice (5 files)
| File | Description |
|------|-------------|
| 01-gestion-solicitudes.feature | Request management |
| 02-gestion-usuarios.feature | User management |
| 03-gestion-servicios.feature | Services & pricing |
| 04-reembolsos.feature | Refund process |
| 05-reportes.feature | Reports & dashboard |
## Gherkin Patterns Used
### Basic
- Feature, Scenario, Given/When/Then
### Intermediate
- **Background**: Shared setup for all scenarios
- **Scenario Outline + Examples**: Test variations without duplication
- **Data Tables**: Multiple items in one step
### Advanced
- **Rule**: Group related scenarios
- **Doc Strings**: Large text blocks (diagnoses, reports)
- **Tags**: For filtering (@smoke, @critical, @wip)
## Comment Headers
Each file includes metadata comments:
```gherkin
# Fuente: album/book/ops-templates/...
# Drive: [Google Drive reference]
# Tests Backend: pytest tests/contracts/...
# Tests Frontend: npx playwright test ...
```
## Tools
To run/validate these files:
**Python**
- behave
- pytest-bdd
**JavaScript/TypeScript**
- playwright-bdd
- jest-cucumber
**IDE Support**
- VS Code: Cucumber (Gherkin) Full Support extension
- JetBrains: Built-in Gherkin plugin
## Source Templates
Sample ops templates at: `album/book/ops-templates-sample/`

View File

@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ filename }} - Gherkin</title>
<link rel="stylesheet" href="/static/prism/prism-tomorrow.min.css">
<link rel="stylesheet" href="/static/prism/prism-line-numbers.min.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.container { max-width: 900px; margin: 0 auto; padding: 2rem 1rem; }
header { margin-bottom: 2rem; }
.breadcrumb { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; }
.breadcrumb a { color: #818cf8; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
h1 { font-size: 1.5rem; color: #e2e8f0; }
.meta {
display: flex;
gap: 0.75rem;
margin-top: 0.75rem;
font-size: 0.85rem;
flex-wrap: wrap;
}
.meta span {
background: #1e293b;
padding: 0.25rem 0.75rem;
border-radius: 4px;
color: #94a3b8;
}
.meta .lang-es { border-left: 3px solid #f59e0b; }
.meta .lang-en { border-left: 3px solid #3b82f6; }
.meta .sample { background: #422006; color: #fbbf24; }
.content {
background: #1e293b;
border-radius: 12px;
overflow: hidden;
}
.content-header {
background: #334155;
padding: 0.75rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
}
.content-header .filename { color: #94a3b8; }
.content-header .copy-btn {
background: #475569;
border: none;
color: #e2e8f0;
padding: 0.35rem 0.75rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.content-header .copy-btn:hover { background: #64748b; }
pre[class*="language-"] {
margin: 0;
border-radius: 0;
font-size: 0.85rem;
line-height: 1.7;
}
code[class*="language-"] {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
}
.sidebar {
margin-top: 2rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.sidebar-box {
background: #1e293b;
border-radius: 8px;
padding: 1rem;
}
.sidebar-box h3 { font-size: 0.8rem; color: #64748b; text-transform: uppercase; margin-bottom: 0.75rem; }
.sidebar-box a {
display: block;
color: #818cf8;
text-decoration: none;
padding: 0.35rem 0;
font-size: 0.9rem;
}
.sidebar-box a:hover { text-decoration: underline; }
footer {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #334155;
font-size: 0.85rem;
}
footer a { color: #818cf8; text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="breadcrumb">
<a href="/">Album</a> / <a href="/book/gherkin/">Gherkin</a> / {{ lang }} / {{ user_type }}
</div>
<h1>{{ filename.replace('.feature', '').replace('-', ' ').title() }}</h1>
<div class="meta">
<span class="lang-{{ lang }}">{{ 'Espanol' if lang == 'es' else 'English' }}</span>
<span>{{ user_type }}</span>
<span class="sample">Sample</span>
</div>
</header>
<div class="content">
<div class="content-header">
<span class="filename">{{ filename }}</span>
<button class="copy-btn" onclick="copyContent()">Copy</button>
</div>
<pre class="line-numbers"><code id="gherkin-content" class="language-gherkin">{{ content }}</code></pre>
</div>
<div class="sidebar">
<div class="sidebar-box">
<h3>Source</h3>
<a href="/book/ops-templates/{{ user_type }}/{{ filename.replace('.feature', '.md') }}">Ops Template</a>
</div>
<div class="sidebar-box">
<h3>Other Language</h3>
{% if lang == 'es' %}
<a href="/book/gherkin/en/{{ user_type }}/{{ filename }}">English Version</a>
{% else %}
<a href="/book/gherkin/es/{{ user_type }}/{{ filename }}">Spanish Version</a>
{% endif %}
</div>
<div class="sidebar-box">
<h3>Pipeline</h3>
<a href="/book/feature-flow/">Feature Flow</a>
</div>
</div>
<footer>
<a href="/book/gherkin/">&larr; All Gherkin Files</a>
</footer>
</div>
<script src="/static/prism/prism.min.js"></script>
<script src="/static/prism/prism-gherkin.min.js"></script>
<script src="/static/prism/prism-line-numbers.min.js"></script>
<script>
function copyContent() {
const content = document.getElementById('gherkin-content').textContent;
navigator.clipboard.writeText(content);
const btn = document.querySelector('.copy-btn');
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 2000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,104 @@
# Fuente: album/book/ops-templates/backoffice/01-gestion-solicitudes.md
# Drive: 05. ATC - Operaciones/Procedimientos
# Tests Backend: pytest tests/contracts/solicitudes/test_service_requests.py
# Tests Frontend: npx playwright test admin-requests.spec.ts
Feature: Gestion de solicitudes de servicio
Como administrador del backoffice
Quiero gestionar todas las solicitudes del sistema
Para asegurar que se atiendan correctamente
Background:
Given que estoy logueado como administrador
And estoy en la seccion "Solicitudes" del backoffice
# ============================================
# VER Y FILTRAR SOLICITUDES
# ============================================
Scenario: Ver listado de todas las solicitudes
When cargo la pagina de solicitudes
Then deberia ver un listado paginado
And cada solicitud deberia mostrar:
| campo |
| ID |
| Dueno |
| Mascota |
| Servicios |
| Estado |
| Fecha |
Scenario Outline: Filtrar solicitudes por estado
When filtro por estado "<estado>"
Then solo deberia ver solicitudes con estado "<estado>"
Examples:
| estado |
| Pendiente |
| Coordinado |
| Pagado |
| Completado |
| Cancelado |
# ============================================
# ASIGNAR VETERINARIO MANUALMENTE
# ============================================
Scenario: Asignar veterinario a solicitud pendiente
Given que hay una solicitud pendiente en "Palermo"
And no fue aceptada por ningun veterinario
When abro el detalle de la solicitud
And hago click en "Asignar veterinario"
Then deberia ver lista de veterinarios con cobertura en Palermo
When selecciono "Dra. Garcia"
And selecciono fecha "15 de enero" hora "10:00"
And confirmo la asignacion
Then la solicitud deberia pasar a estado "Coordinado"
And deberia estar asignada a Dra. Garcia
And el dueno deberia recibir notificacion
And el veterinario deberia recibir notificacion
# ============================================
# CAMBIAR ESTADO MANUALMENTE
# ============================================
Scenario: Cambiar estado de solicitud
Given que hay una solicitud en estado "Coordinado"
And el pago se proceso pero el webhook fallo
When abro la solicitud
And hago click en "Cambiar estado"
And selecciono "Pagado"
And ingreso motivo "Pago confirmado manualmente - ID MP: 12345"
And confirmo el cambio
Then la solicitud deberia pasar a "Pagado"
And el cambio deberia registrarse en el historial
Rule: Solo ciertos cambios de estado son validos
Scenario: No puedo volver a estado anterior
Given que hay una solicitud en estado "Completado"
When intento cambiar el estado a "Pagado"
Then deberia ver error "No se puede volver a un estado anterior"
Scenario: Puedo cancelar desde cualquier estado
Given que hay una solicitud en estado "Coordinado"
When cambio el estado a "Cancelado"
And ingreso motivo de cancelacion
Then la solicitud deberia cancelarse
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Reasignar veterinario
Given que hay una solicitud asignada a "Dr. Lopez"
And Dr. Lopez no puede asistir
When abro la solicitud
And hago click en "Reasignar"
And selecciono otro veterinario
And confirmo
Then Dr. Lopez deberia ser notificado de la desasignacion
And el nuevo vet deberia ser notificado
And el dueno deberia ser notificado del cambio

View File

@@ -0,0 +1,88 @@
# Fuente: album/book/ops-templates/backoffice/02-gestion-usuarios.md
# Drive: 05. ATC - Operaciones/Procedimientos
# Tests Backend: pytest tests/contracts/mascotas/test_pet_owners.py
# Tests Frontend: npx playwright test admin-users.spec.ts
Feature: Gestion de usuarios
Como administrador del backoffice
Quiero gestionar duenos y veterinarios
Para mantener la base de usuarios actualizada
Background:
Given que estoy logueado como administrador
And estoy en el backoffice
# ============================================
# GESTION DE DUENOS
# ============================================
Scenario: Buscar dueno de mascota
Given que estoy en la seccion "Duenos"
When busco "maria@ejemplo.com"
Then deberia ver a Maria Garcia en los resultados
And deberia ver sus mascotas listadas
Scenario: Ver perfil completo de dueno
Given que encontre a "Maria Garcia"
When hago click en su perfil
Then deberia ver:
| seccion | contenido |
| Datos personales | Nombre, email, telefono |
| Direcciones | Direcciones registradas |
| Mascotas | Lista de mascotas |
| Historial | Solicitudes anteriores |
Scenario: Editar datos de dueno
Given que estoy viendo el perfil de un dueno
When hago click en "Editar"
And cambio el telefono a "1155559999"
And guardo los cambios
Then el telefono deberia actualizarse
# ============================================
# GESTION DE VETERINARIOS
# ============================================
Scenario: Agregar nuevo veterinario
Given que estoy en la seccion "Veterinarios"
When hago click en "Agregar veterinario"
And completo los datos:
| campo | valor |
| Nombre | Dr. Juan Perez |
| Matricula | MV-12345 |
| Email | jperez@ejemplo.com |
| Telefono | 1155551234 |
And selecciono zonas de cobertura:
| zona |
| Palermo |
| Recoleta |
And genero credenciales de acceso
And guardo
Then deberia crearse el veterinario
And deberia poder loguearse con sus credenciales
Scenario: Intentar desactivar veterinario con citas pendientes
Given que el veterinario "Dra. Garcia" tiene 3 citas pendientes
When intento desactivarla
Then deberia ver advertencia "Tiene 3 citas pendientes"
And deberia ver opciones:
| opcion |
| Reasignar citas y desactivar |
| Cancelar |
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Fusionar cuentas duplicadas
Given que hay dos cuentas para el mismo dueno:
| cuenta | email | mascotas |
| Cuenta1 | maria@gmail.com | Luna |
| Cuenta2 | maria@hotmail.com | Rocky |
When selecciono ambas cuentas
And hago click en "Fusionar"
And elijo Cuenta1 como principal
And confirmo
Then deberia existir solo Cuenta1
And deberia tener ambas mascotas

View File

@@ -0,0 +1,91 @@
# Fuente: album/book/ops-templates/backoffice/03-gestion-servicios.md
# Drive: 08. IT y Producto/Catalogo Servicios
# Tests Backend: pytest tests/contracts/productos/test_services.py
# Tests Frontend: npx playwright test admin-services.spec.ts
Feature: Gestion de servicios y precios
Como administrador
Quiero gestionar el catalogo de servicios
Para mantener la oferta actualizada
Background:
Given que estoy logueado como administrador
And estoy en la seccion "Servicios"
# ============================================
# VER CATALOGO DE SERVICIOS
# ============================================
Scenario: Ver listado de servicios
When cargo la pagina de servicios
Then deberia ver todos los servicios organizados por categoria
And cada servicio deberia mostrar:
| campo |
| Nombre |
| Categoria |
| Tipo mascota |
| Precio actual |
| Estado |
# ============================================
# CREAR Y EDITAR SERVICIOS
# ============================================
Scenario: Agregar nuevo servicio
When hago click en "Agregar servicio"
And completo los datos:
| campo | valor |
| Nombre | Vacuna Quintuple Felina |
| Descripcion | Protege contra 5 enfermedades |
| Categoria | Vacunacion |
| Tipo mascota | Gato |
| Precio | 15000 |
And guardo el servicio
Then el servicio deberia crearse
And deberia aparecer en el turnero para gatos
Scenario: Desactivar servicio
Given que existe el servicio "Servicio Antiguo"
When abro el servicio
And hago click en "Desactivar"
And confirmo
Then el servicio no deberia aparecer en el turnero
# ============================================
# GESTION DE PRECIOS
# ============================================
Scenario: Actualizar precio de servicio
Given que "Consulta clinica" tiene precio actual de 10000
When abro el servicio
And voy a la seccion "Precios"
And hago click en "Agregar precio"
And ingreso nuevo precio 12000
And selecciono fecha de vigencia "01/02/2024"
And guardo
Then deberia crearse el nuevo precio
And el precio anterior deberia quedar en historial
Rule: El precio se congela al crear la solicitud
Scenario: Cambio de precio no afecta solicitudes existentes
Given que hay una solicitud pendiente con "Consulta clinica" a 10000
When cambio el precio de "Consulta clinica" a 12000
Then la solicitud deberia mantener el precio de 10000
# ============================================
# CONFIGURAR COMBOS
# ============================================
Scenario: Crear combo de servicios
When voy a "Combos"
And hago click en "Agregar combo"
And configuro:
| campo | valor |
| Nombre | Plan Preventivo Felino |
| Servicios | Vacunacion, Desparasitacion |
| Precio combo | 25000 |
And guardo
Then el combo deberia crearse
And deberia aplicarse automaticamente en el turnero

View File

@@ -0,0 +1,86 @@
# Fuente: album/book/ops-templates/backoffice/04-reembolsos.md
# Drive: 07. Finanzas y contabilidad/Reembolsos
# Tests Backend: pytest tests/contracts/payments/test_refunds.py
# Tests Frontend: npx playwright test admin-refunds.spec.ts
Feature: Proceso de reembolso
Como administrador
Quiero procesar reembolsos de pagos
Para resolver cancelaciones y problemas
Background:
Given que estoy logueado como administrador
And estoy en el backoffice
# ============================================
# PROCESAR REEMBOLSO
# ============================================
Scenario: Reembolso total exitoso
Given que hay una solicitud pagada por 15000
And la visita fue cancelada
When abro la solicitud
And hago click en "Procesar reembolso"
And selecciono "Reembolso total"
And ingreso motivo "Cancelacion por indisponibilidad del veterinario"
And confirmo el reembolso
Then deberia enviarse la solicitud de reembolso a Mercado Pago
And deberia ver mensaje "Reembolso en proceso"
And el dueno deberia recibir email de confirmacion
Scenario: Reembolso parcial
Given que hay una solicitud pagada por 20000
And solo se realizo parte del servicio
When proceso reembolso parcial por 10000
And ingreso motivo "Servicio parcialmente completado"
And confirmo
Then deberia procesarse reembolso por 10000
And deberia quedar registro del monto reembolsado
# ============================================
# TIEMPOS DE ACREDITACION
# ============================================
Scenario Outline: Informar tiempo de acreditacion segun metodo
Given que el pago original fue con "<metodo>"
When proceso el reembolso
Then deberia informar al usuario:
"""
El reembolso se acreditara en <tiempo>
"""
Examples:
| metodo | tiempo |
| Tarjeta credito | 1-2 resumenes de cuenta |
| Tarjeta debito | 5-10 dias habiles |
| Dinero en cuenta MP | forma inmediata |
# ============================================
# VALIDACIONES
# ============================================
Scenario: No puedo reembolsar mas del monto pagado
Given que hay una solicitud pagada por 15000
When intento reembolsar 20000
Then deberia ver error "El monto supera el pago original"
Scenario: Reembolso duplicado
Given que ya procese un reembolso total para una solicitud
When intento procesar otro reembolso
Then deberia ver error "Esta solicitud ya fue reembolsada"
Scenario: Pago fuera de plazo de reembolso
Given que hay un pago de hace 200 dias
When intento reembolsar
Then deberia ver advertencia "Fuera de plazo de reembolso de MP (180 dias)"
# ============================================
# SEGUIMIENTO
# ============================================
Scenario: Ver estado del reembolso
Given que procese un reembolso hace 2 dias
When veo el detalle del reembolso
Then deberia ver el estado actual en Mercado Pago
And deberia ver historial de estados

View File

@@ -0,0 +1,97 @@
# Fuente: album/book/ops-templates/backoffice/05-reportes.md
# Drive: 07. Finanzas y contabilidad/Reportes
# Tests Backend: pytest tests/contracts/mascotas/test_stats.py
# Tests Frontend: npx playwright test admin-reports.spec.ts
Feature: Reportes y dashboard
Como administrador o gerente
Quiero ver metricas y generar reportes
Para tomar decisiones informadas
Background:
Given que estoy logueado como administrador
And estoy en el backoffice
# ============================================
# DASHBOARD
# ============================================
Scenario: Ver dashboard principal
When accedo al dashboard
Then deberia ver metricas resumidas:
| metrica | periodo |
| Solicitudes nuevas | Hoy |
| Visitas completadas | Semana |
| Ingresos | Mes |
| Veterinarios activos | Actual |
| Tasa de conversion | Mes |
Scenario: Ver grafico de tendencia
When veo el grafico de solicitudes
Then deberia ver la evolucion de los ultimos 30 dias
And deberia poder comparar con periodo anterior
# ============================================
# REPORTE DE SOLICITUDES
# ============================================
Scenario: Generar reporte de solicitudes
When voy a "Reportes" -> "Solicitudes"
And selecciono periodo "Enero 2024"
And hago click en "Generar"
Then deberia ver tabla con solicitudes del periodo
And deberia ver totales por estado
Scenario: Filtrar reporte por multiples criterios
When genero reporte con filtros:
| filtro | valor |
| Periodo | Enero 2024 |
| Zona | Palermo |
| Veterinario | Dra. Garcia |
Then deberia ver solo solicitudes que cumplan todos los criterios
# ============================================
# REPORTE DE INGRESOS
# ============================================
Scenario: Ver ingresos por periodo
When voy a "Reportes" -> "Ingresos"
And selecciono "Ultimo trimestre"
Then deberia ver:
| dato |
| Ingresos totales |
| Cantidad de pagos |
| Ticket promedio |
| Reembolsos realizados |
| Ingreso neto |
Scenario: Ingresos agrupados por veterinario
When agrupo el reporte por "Veterinario"
Then deberia ver para cada vet:
| dato |
| Visitas completadas |
| Ingresos generados |
| Porcentaje del total |
# ============================================
# EXPORTACION
# ============================================
Scenario Outline: Exportar reporte en diferentes formatos
Given que tengo un reporte generado
When hago click en "Exportar"
And selecciono formato "<formato>"
Then deberia descargarse el archivo en formato <formato>
Examples:
| formato |
| CSV |
| Excel |
| PDF |
Scenario: Exportar reporte grande de forma asincrona
Given que genere un reporte con mas de 10000 registros
When hago click en "Exportar"
Then deberia ver mensaje "Generando exportacion..."
And deberia recibir notificacion cuando este listo

View File

@@ -0,0 +1,91 @@
# Fuente: album/book/ops-templates/pet-owner/01-registro.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/common/test_users.py
# Tests Frontend: npx playwright test auth.spec.ts
Feature: Registro de usuario
Como visitante de la plataforma
Quiero poder crear una cuenta
Para gestionar mis mascotas y reservar turnos
# ============================================
# CAMINO FELIZ
# ============================================
Scenario: Registro exitoso con datos validos
Given que estoy en la pagina de registro
When ingreso email "nuevo@ejemplo.com"
And ingreso contraseña "Password123"
And confirmo contraseña "Password123"
And acepto los terminos y condiciones
And hago click en "Crear cuenta"
Then deberia ver mensaje "Te enviamos un email de verificacion"
And deberia recibir email de verificacion
Scenario: Verificar email y activar cuenta
Given que me registre con email "nuevo@ejemplo.com"
And recibi el email de verificacion
When hago click en el link de verificacion
Then mi cuenta deberia estar activa
And deberia ser redirigido al dashboard
# ============================================
# VALIDACIONES
# ============================================
Scenario Outline: Registro con datos invalidos
Given que estoy en la pagina de registro
When ingreso email "<email>"
And ingreso contraseña "<password>"
And confirmo contraseña "<confirmacion>"
And hago click en "Crear cuenta"
Then deberia ver error "<mensaje_error>"
Examples:
| email | password | confirmacion | mensaje_error |
| invalido | Password123 | Password123 | Email invalido |
| test@test.com | 123 | 123 | Contraseña muy corta |
| test@test.com | password | password | Debe contener al menos un numero |
| test@test.com | Password123 | Diferente123 | Las contraseñas no coinciden |
Scenario: Registro con email ya existente
Given que existe un usuario con email "existente@ejemplo.com"
And estoy en la pagina de registro
When ingreso email "existente@ejemplo.com"
And completo el resto del formulario correctamente
And hago click en "Crear cuenta"
Then deberia ver error "Este email ya esta registrado"
And deberia ver link "Recuperar contraseña"
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Registro linkea con cuenta invitado existente
# Usuario que reservo turno como invitado y ahora quiere registrarse
Given que existe un usuario invitado con email "invitado@ejemplo.com"
And ese usuario tiene una mascota "Luna" registrada
And estoy en la pagina de registro
When me registro con email "invitado@ejemplo.com"
And verifico mi cuenta
Then deberia ver mi mascota "Luna" en el dashboard
And deberia ver mis turnos anteriores
Scenario: Registro desde flujo de turnero
# Usuario empezo a reservar turno y decide crear cuenta
Given que estoy en el paso final del turnero
And ingrese mis datos de contacto
When hago click en "Crear cuenta para guardar mis datos"
Then deberia ver formulario simplificado
And mi email ya deberia estar pre-llenado
And solo deberia ingresar contraseña
Scenario: Reenviar email de verificacion
Given que me registre pero no verifique mi cuenta
And estoy en la pagina de login
When intento iniciar sesion
Then deberia ver "Tu cuenta no esta verificada"
And deberia ver boton "Reenviar email"
When hago click en "Reenviar email"
Then deberia recibir nuevo email de verificacion

View File

@@ -0,0 +1,140 @@
# Fuente: album/book/ops-templates/pet-owner/02-reservar-turno.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/workflows/test_turnero_general.py
# Tests Frontend: npx playwright test turnero.spec.ts
# Relacionado: def/work_plan/10-flow-turnero.md
Feature: Reservar turno veterinario (Turnero)
Como dueno de mascota
Quiero reservar un turno veterinario a domicilio
Para que atiendan a mi mascota sin salir de casa
Background:
Given que estoy en la pagina del turnero
# ============================================
# VERIFICACION DE COBERTURA
# ============================================
Scenario: Verificar cobertura en zona disponible
When ingreso direccion "Av Santa Fe 1234, CABA"
Then deberia ver mensaje "Tenemos cobertura en tu zona"
And deberia poder continuar al siguiente paso
Scenario: Zona sin cobertura
When ingreso direccion "Calle Principal 100, Ushuaia"
Then deberia ver mensaje "Aun no tenemos cobertura en tu zona"
And deberia ver formulario "Avisame cuando lleguen"
# ============================================
# FLUJO COMPLETO POR TIPO DE USUARIO
# ============================================
Scenario Outline: Reservar turno como <tipo_usuario>
Given que soy un usuario <tipo_usuario>
And tengo cobertura en mi zona
When completo los datos de mi mascota:
| campo | valor |
| nombre | Luna |
| tipo | Gato |
| edad | 2 años |
| castrada | Si |
And selecciono servicios:
| servicio |
| Vacunacion |
And selecciono fechas preferidas:
| fecha | franja |
| 2024-01-15 | Mañana |
| 2024-01-16 | Tarde |
And completo datos de contacto con email "<email>"
And envio la solicitud
Then deberia crearse una solicitud en estado "Pendiente"
And el dueno deberia ser <estado_dueno>
And deberia recibir email de confirmacion
Examples:
| tipo_usuario | email | estado_dueno |
| invitado | nuevo@test.com | creado como invitado |
| registrado | user@test.com | mi cuenta existente |
| recurrente | conocido@test.com | identificado por email |
# ============================================
# SELECCION DE SERVICIOS
# ============================================
Scenario: Servicios filtrados por tipo de mascota
Given que agregue una mascota tipo "Gato"
When veo los servicios disponibles
Then deberia ver "Vacuna triple felina"
And deberia ver "Vacuna antirabica"
But no deberia ver "Vacuna sextuple canina"
Scenario: Consulta clinica se agrega automaticamente con vacunacion
Given que estoy seleccionando servicios
When selecciono "Vacunacion"
Then "Consulta clinica" deberia agregarse automaticamente
And deberia ver nota "Incluye revision general"
And no deberia poder quitar "Consulta clinica"
Scenario: Servicios combo con descuento
Given que estoy seleccionando servicios
When agrego los siguientes servicios:
| servicio |
| Vacunacion |
| Desparasitacion |
| Antipulgas |
Then deberia ver "Plan preventivo completo"
And el total deberia incluir descuento de combo
Scenario: Castracion no disponible para mascota castrada
Given que mi mascota esta marcada como castrada
When veo los servicios disponibles
Then no deberia ver "Castracion"
# ============================================
# DATOS DE CONTACTO Y CUENTA
# ============================================
Scenario: Pre-llenado de datos para usuario logueado
Given que estoy logueado como "maria@ejemplo.com"
And tengo registrada mascota "Firulais"
When inicio el flujo de turnero
Then mi direccion deberia estar pre-llenada
And deberia poder seleccionar "Firulais" de mis mascotas
And mis datos de contacto ya deberian estar completos
Scenario: Detectar usuario existente por email
Given que soy usuario invitado
And existe una cuenta con email "existente@ejemplo.com"
When ingreso email "existente@ejemplo.com" en datos de contacto
Then deberia ver "Ya tenes cuenta con este email"
And deberia ver opciones:
| opcion |
| Iniciar sesion |
| Continuar como invitado |
# ============================================
# EDGE CASES
# ============================================
Scenario: Usuario abandona flujo a mitad
Given que complete los datos de mascota
And cerre el navegador sin enviar
When vuelvo a la pagina del turnero
Then deberia poder recuperar mi progreso
# Nota: datos guardados en localStorage o session
Scenario: Multiples mascotas en una solicitud
Given que quiero atender a 2 mascotas
When agrego mascota "Luna" tipo "Gato"
And agrego mascota "Rocky" tipo "Perro"
And selecciono servicios para cada una
Then deberia crearse una solicitud con 2 mascotas
And el precio deberia reflejar ambas
Scenario: Franja horaria especifica
Given que solo puedo por la mañana
When selecciono franja "Mañana (9-12hs)"
Then la solicitud deberia registrar esa preferencia
# Nota: Es preferencia, no garantia

View File

@@ -0,0 +1,153 @@
# Fuente: album/book/ops-templates/pet-owner/03-gestion-mascotas.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_pets.py
# Tests Frontend: npx playwright test pets.spec.ts
Feature: Gestion de mascotas
Como dueno de mascota registrado
Quiero gestionar la informacion de mis mascotas
Para tenerla actualizada y acceder a su historial medico
Background:
Given que estoy logueado como dueno de mascota
And estoy en la seccion "Mis mascotas"
# ============================================
# AGREGAR MASCOTA
# ============================================
Scenario: Agregar mascota con datos minimos
When hago click en "Agregar mascota"
And completo el formulario:
| campo | valor |
| nombre | Luna |
| tipo | Gato |
And hago click en "Guardar"
Then deberia ver "Luna" en mi lista de mascotas
And Luna deberia aparecer disponible en el turnero
Scenario: Agregar mascota con datos completos
When hago click en "Agregar mascota"
And completo el formulario:
| campo | valor |
| nombre | Rocky |
| tipo | Perro |
| raza | Labrador |
| fecha_nacimiento | 2020-03-15 |
| peso | 25 |
| sexo | Macho |
| castrado | Si |
And subo una foto de Rocky
And hago click en "Guardar"
Then deberia ver "Rocky" con su foto en mi lista
And deberia ver badge "Castrado"
Scenario Outline: Validacion de datos de mascota
When intento agregar mascota con <campo> igual a "<valor>"
Then deberia ver error "<mensaje>"
Examples:
| campo | valor | mensaje |
| nombre | | El nombre es obligatorio |
| nombre | A | Nombre muy corto |
| tipo | | Selecciona el tipo |
| peso | -5 | El peso debe ser positivo |
| peso | 500 | Peso fuera de rango |
# ============================================
# EDITAR MASCOTA
# ============================================
Scenario: Editar peso de mascota
Given que tengo una mascota "Luna" con peso 4kg
When edito a Luna
And cambio el peso a 5kg
And guardo los cambios
Then Luna deberia mostrar peso "5 kg"
Scenario: Marcar mascota como castrada
Given que tengo una mascota "Rocky" no castrado
When edito a Rocky
And marco "Esta castrado"
And guardo los cambios
Then Rocky deberia mostrar badge "Castrado"
And el servicio "Castracion" no deberia aparecer para Rocky en el turnero
Scenario: Actualizar foto de mascota
Given que tengo una mascota "Luna" sin foto
When edito a Luna
And subo una nueva foto
And guardo los cambios
Then deberia ver la foto de Luna en su tarjeta
# ============================================
# ELIMINAR MASCOTA
# ============================================
Scenario: Eliminar mascota sin historial
Given que tengo una mascota "Nuevo" sin visitas
When hago click en "Eliminar" para Nuevo
And confirmo la eliminacion
Then Nuevo no deberia aparecer en mi lista
# Nota: Es soft delete
Scenario: Eliminar mascota con historial medico
Given que tengo una mascota "Luna" con visitas anteriores
When hago click en "Eliminar" para Luna
Then deberia ver advertencia "Luna tiene historial medico"
And deberia ver "El historial se conservara pero no podras verlo"
When confirmo la eliminacion
Then Luna no deberia aparecer en mi lista
Scenario: No puedo eliminar mascota con turno pendiente
Given que tengo una mascota "Rocky" con turno pendiente
When intento eliminar a Rocky
Then deberia ver error "Rocky tiene turnos pendientes"
And deberia ver sugerencia "Cancela los turnos primero"
# ============================================
# VER HISTORIAL MEDICO
# ============================================
Scenario: Ver historial de visitas de mascota
Given que tengo una mascota "Luna" con 3 visitas completadas
When hago click en Luna
And voy a la seccion "Historial"
Then deberia ver 3 visitas listadas
And deberian estar ordenadas por fecha descendente
Scenario: Ver detalle de visita
Given que tengo una mascota "Luna" con visitas
When veo el historial de Luna
And hago click en la primera visita
Then deberia ver:
| campo |
| Fecha |
| Veterinario |
| Diagnostico |
| Tratamiento |
| Medicamentos |
Scenario: Mascota sin historial
Given que tengo una mascota "Nuevo" recien agregada
When veo el perfil de Nuevo
Then la seccion "Historial" deberia estar vacia
And deberia ver mensaje "Aun no hay visitas registradas"
And deberia ver boton "Reservar primer turno"
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Mascota heredada de cuenta invitado
Given que me registre con email "juan@test.com"
And previamente reserve turno como invitado para "Firulais"
When voy a "Mis mascotas"
Then deberia ver "Firulais" en mi lista
And deberia ver su historial de visitas previas
Scenario: Razas filtradas por tipo
When agrego una mascota tipo "Gato"
Then las razas disponibles deberian ser razas de gato
And no deberia ver razas de perro como "Labrador"

View File

@@ -0,0 +1,140 @@
# Fuente: album/book/ops-templates/pet-owner/04-pago-turno.md
# Drive: 07. Finanzas y contabilidad/Mercado Pago
# Tests Backend: pytest tests/contracts/payments/test_mercadopago.py
# Tests Frontend: npx playwright test payment.spec.ts
Feature: Pago de turno
Como dueno de mascota con turno coordinado
Quiero pagar mi turno online
Para confirmar la visita del veterinario
Background:
Given que tengo un turno en estado "Coordinado"
And el turno tiene asignado veterinario "Dra. Garcia"
And la fecha asignada es "15 de enero a las 10:00"
# ============================================
# FLUJO DE PAGO EXITOSO
# ============================================
Scenario: Pagar turno con tarjeta de credito
Given que estoy en el detalle de mi turno coordinado
When hago click en "Pagar"
Then deberia ser redirigido a Mercado Pago
And deberia ver el monto correcto
When selecciono "Tarjeta de credito"
And completo los datos de la tarjeta
And confirmo el pago
Then deberia volver a la plataforma
And deberia ver "Pago exitoso"
And el turno deberia estar en estado "Pagado"
Scenario: Recibir confirmacion de pago
Given que complete el pago exitosamente
Then deberia recibir email de confirmacion
And el email deberia contener:
| campo |
| Fecha del turno |
| Direccion |
| Veterinario asignado |
| Monto pagado |
| Numero de operacion |
# ============================================
# METODOS DE PAGO
# ============================================
Scenario Outline: Pagar con diferentes metodos
Given que estoy en Mercado Pago
When selecciono metodo "<metodo>"
And completo el pago
Then el pago deberia ser <resultado>
And el estado de acreditacion deberia ser "<acreditacion>"
Examples:
| metodo | resultado | acreditacion |
| Tarjeta credito | exitoso | inmediata |
| Tarjeta debito | exitoso | inmediata |
| Dinero en cuenta | exitoso | inmediata |
| Transferencia | pendiente | 1-2 dias |
| Rapipago | pendiente | hasta 24hs |
# ============================================
# MANEJO DE ERRORES
# ============================================
Scenario: Pago rechazado por fondos insuficientes
Given que estoy en Mercado Pago
When intento pagar con tarjeta sin fondos
Then deberia ver error "Fondos insuficientes"
And deberia poder reintentar con otra tarjeta
And el turno deberia seguir en estado "Coordinado"
Scenario: Usuario cancela el pago
Given que estoy en Mercado Pago
When hago click en "Volver al sitio"
Then deberia volver a la plataforma
And deberia ver mensaje "El pago fue cancelado"
And deberia ver boton "Reintentar pago"
And el turno deberia seguir en estado "Coordinado"
Scenario: Cierre de browser durante pago
Given que estoy en Mercado Pago
And cierro el navegador accidentalmente
When vuelvo a la plataforma
And voy a "Mis turnos"
Then deberia poder ver el estado real del pago
# Si se proceso: Pagado. Si no: Coordinado con opcion de pagar
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Link de pago expirado
Given que recibi el link de pago hace mas de 24 horas
When hago click en el link
Then deberia ver "Este link ha expirado"
And deberia ver "Contacta a soporte para generar uno nuevo"
Scenario: Intento pagar turno ya pagado
Given que mi turno ya esta en estado "Pagado"
When accedo al link de pago
Then deberia ver "Este turno ya fue pagado"
And deberia ver boton "Ver detalle del turno"
Scenario: Precio cambio desde la coordinacion
# Caso muy raro pero posible
Given que el precio del servicio aumento desde que se coordino
When voy a pagar
Then deberia ver el precio original acordado
# El precio se congela al momento de coordinacion
# ============================================
# INTEGRACION CON WEBHOOK
# ============================================
Rule: El estado del turno se actualiza via webhook de Mercado Pago
Scenario: Webhook confirma pago aprobado
Given que el usuario completo el pago en Mercado Pago
When Mercado Pago envia webhook con status "approved"
Then el turno deberia cambiar a estado "Pagado"
And el veterinario deberia recibir notificacion
And el usuario deberia recibir email de confirmacion
Scenario: Webhook informa pago pendiente
Given que el usuario pago con transferencia bancaria
When Mercado Pago envia webhook con status "pending"
Then el turno deberia quedarse en "Coordinado"
And deberia registrarse el pago pendiente
And el usuario deberia recibir email "Esperando acreditacion"
Scenario: Webhook falla pero pago se proceso
# Caso de error que requiere intervencion manual
Given que el usuario pago exitosamente
But el webhook fallo por error de red
Then el turno seguira en "Coordinado"
And el equipo de ops deberia recibir alerta
And deberian poder actualizar manualmente

View File

@@ -0,0 +1,147 @@
# Fuente: album/book/ops-templates/pet-owner/05-historial-medico.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_visits.py
# Tests Frontend: npx playwright test medical-history.spec.ts
Feature: Ver historial medico
Como dueno de mascota
Quiero ver el historial medico de mis mascotas
Para tener registro de sus visitas y tratamientos
Background:
Given que estoy logueado como dueno de mascota
And tengo una mascota "Luna" con visitas completadas
# ============================================
# ACCESO AL HISTORIAL
# ============================================
Scenario: Acceder al historial desde mascotas
Given que estoy en "Mis mascotas"
When hago click en "Luna"
And voy a la seccion "Historial de visitas"
Then deberia ver lista de visitas de Luna
Scenario: Acceder al historial desde turnos
Given que estoy en "Mis turnos"
And filtro por "Completados"
When hago click en un turno de Luna
And hago click en "Ver informe medico"
Then deberia ver el informe de esa visita
# ============================================
# VISUALIZACION DE VISITAS
# ============================================
Scenario: Ver lista de visitas ordenadas
Given que Luna tiene visitas en las siguientes fechas:
| fecha |
| 2024-01-15 |
| 2023-11-20 |
| 2023-06-10 |
When veo el historial de Luna
Then deberia ver las visitas ordenadas de mas reciente a mas antigua
And la primera deberia ser del "15 de enero 2024"
Scenario: Ver detalle completo de informe
When hago click en una visita completada
Then deberia ver los siguientes datos:
| seccion | contenido |
| Informacion | Fecha, veterinario, servicios |
| Examen fisico | Peso, temperatura, FC, obs |
| Diagnostico | Descripcion del diagnostico |
| Tratamiento | Plan de tratamiento |
| Medicamentos | Lista con dosis y frecuencia |
| Estudios | Estudios solicitados y estado |
| Seguimiento | Recomendaciones, proximo control |
Scenario: Ver informe con medicamentos recetados
Given que la visita incluye medicamentos:
"""
1. Amoxicilina 250mg - 1 comprimido cada 12hs por 7 dias
2. Meloxicam 1.5mg - 1 comprimido por dia por 3 dias
"""
When veo el informe
Then deberia ver la lista de medicamentos
And cada medicamento deberia mostrar:
| campo |
| Nombre |
| Dosis |
| Frecuencia |
| Duracion |
# ============================================
# DESCARGA Y COMPARTIR
# ============================================
Scenario: Descargar informe como PDF
Given que estoy viendo un informe medico
When hago click en "Descargar PDF"
Then deberia descargarse un archivo PDF
And el PDF deberia contener toda la informacion del informe
And deberia tener el logo de Amar Mascotas
Scenario: Imprimir informe
Given que estoy viendo un informe medico
When hago click en "Imprimir"
Then deberia abrirse el dialogo de impresion
And el formato deberia ser optimizado para impresion
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Visita completada sin informe cargado
# El vet no cargo el informe todavia
Given que Luna tiene una visita marcada como "Completada"
But el veterinario no cargo el informe aun
When intento ver el informe
Then deberia ver mensaje "Informe pendiente"
And deberia ver "El veterinario esta completando el informe"
Scenario: Informe con estudios pendientes
Given que el informe solicita estudios de laboratorio
And los resultados aun no estan disponibles
When veo el informe
Then deberia ver seccion "Estudios solicitados"
And deberia ver estado "Pendiente de resultados"
And deberia ver nota "Se notificara cuando esten listos"
Scenario: Mascota con multiples visitas el mismo dia
# Raro pero posible: consulta de urgencia + seguimiento
Given que Luna tuvo 2 visitas el 15 de enero
When veo el historial
Then deberia ver ambas visitas listadas
And deberian estar diferenciadas por hora
# ============================================
# PERMISOS Y PRIVACIDAD
# ============================================
Rule: Solo el dueno puede ver el historial de su mascota
Scenario: Dueno ve historial de su mascota
Given que soy dueno de Luna
When accedo al historial de Luna
Then deberia poder verlo completo
Scenario: No puedo ver historial de mascota ajena
Given que existe mascota "Rocky" de otro dueno
When intento acceder al historial de Rocky
Then deberia ver error "No tienes acceso"
# 403 Forbidden
# ============================================
# BUSQUEDA Y FILTROS
# ============================================
Scenario: Filtrar historial por tipo de servicio
Given que Luna tiene visitas de vacunacion y consulta
When filtro por "Vacunacion"
Then solo deberia ver visitas de vacunacion
Scenario: Buscar en historial por fecha
When busco visitas del "2023"
Then solo deberia ver visitas del 2023
And no deberia ver visitas del 2024

View File

@@ -0,0 +1,109 @@
# Fuente: album/book/ops-templates/veterinarian/01-aceptar-solicitud.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/solicitudes/test_service_requests.py
# Tests Frontend: npx playwright test vet-requests.spec.ts
Feature: Aceptar o rechazar solicitudes de servicio
Como veterinario de la plataforma
Quiero revisar y responder a solicitudes en mi zona
Para gestionar mi agenda de visitas a domicilio
Background:
Given que estoy logueado como veterinario
And tengo cobertura en los barrios "Palermo" y "Recoleta"
And estoy en el dashboard de veterinario
# ============================================
# VER SOLICITUDES PENDIENTES
# ============================================
Scenario: Ver lista de solicitudes en mi zona
When veo la seccion "Solicitudes pendientes"
Then deberia ver solo solicitudes de "Palermo" y "Recoleta"
And no deberia ver solicitudes de otros barrios
Scenario: Ver detalle de solicitud
Given que hay una solicitud pendiente
When hago click en la solicitud
Then deberia ver:
| seccion | contenido |
| Dueno | Nombre, telefono, direccion |
| Mascota | Nombre, tipo, edad, foto |
| Servicios | Lista de servicios solicitados |
| Fechas | Fechas preferidas por el dueno |
| Historial | Visitas anteriores si las hay |
# ============================================
# ACEPTAR SOLICITUD
# ============================================
Scenario: Aceptar solicitud con fecha disponible
Given que hay una solicitud para el barrio "Palermo"
And el dueno prefiere fechas:
| fecha | franja |
| 2024-01-15 | Mañana |
| 2024-01-16 | Tarde |
And tengo disponibilidad el 15 de enero a las 10:00
When hago click en "Aceptar"
And selecciono fecha "15 de enero" hora "10:00"
And confirmo la aceptacion
Then la solicitud deberia pasar a estado "Coordinado"
And deberia quedar asignada a mi
And el dueno deberia recibir notificacion con mis datos
And la visita deberia aparecer en mi agenda
Scenario: Aceptar solicitud con datos de mascota que ya atendi
Given que hay una solicitud para mascota "Luna"
And yo atendi a "Luna" anteriormente
When veo el detalle de la solicitud
Then deberia ver badge "Paciente recurrente"
And deberia ver el historial de mis visitas anteriores a Luna
# ============================================
# RECHAZAR SOLICITUD
# ============================================
Scenario Outline: Rechazar solicitud con motivo
Given que hay una solicitud pendiente
When hago click en "Rechazar"
And selecciono motivo "<motivo>"
And confirmo el rechazo
Then la solicitud deberia desaparecer de mi lista
And deberia seguir visible para otros veterinarios
Examples:
| motivo |
| No tengo disponibilidad |
| Fuera de mi zona |
| No realizo este servicio |
| Otro |
# ============================================
# RACE CONDITIONS
# ============================================
Rule: Solo un veterinario puede aceptar cada solicitud
Scenario: Otro vet acepta mientras estoy viendo
Given que estoy viendo el detalle de una solicitud
And otro veterinario acepta la misma solicitud
When intento aceptarla
Then deberia ver error "Esta solicitud ya fue aceptada"
And deberia ser redirigido al listado
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Solicitud urgente destacada
Given que hay una solicitud marcada como "Urgente"
When veo el listado de solicitudes
Then deberia ver la solicitud con indicador de urgencia
And deberia aparecer primero en la lista
Scenario: No puedo aceptar con agenda completa
Given que tengo mi agenda completa para las fechas de la solicitud
When intento aceptar la solicitud
Then deberia ver advertencia "No tienes disponibilidad en las fechas preferidas"
And deberia poder proponer una fecha alternativa

View File

@@ -0,0 +1,99 @@
# Fuente: album/book/ops-templates/veterinarian/02-gestion-agenda.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_availability.py
# Tests Frontend: npx playwright test vet-schedule.spec.ts
Feature: Gestion de agenda veterinaria
Como veterinario
Quiero gestionar mi calendario y disponibilidad
Para organizar mis visitas a domicilio eficientemente
Background:
Given que estoy logueado como veterinario
And estoy en la seccion "Mi agenda"
# ============================================
# VER AGENDA
# ============================================
Scenario: Ver calendario semanal
When veo mi agenda en vista semanal
Then deberia ver los 7 dias de la semana
And deberia ver mis visitas programadas
And cada visita deberia mostrar hora y nombre del paciente
Scenario: Ver visitas con codigo de color por estado
Given que tengo visitas en diferentes estados
When veo mi agenda
Then las visitas deberian mostrarse con colores:
| estado | color |
| Coordinado | amarillo |
| Pagado | verde |
| En progreso | azul |
| Completado | gris |
# ============================================
# CONFIGURAR DISPONIBILIDAD SEMANAL
# ============================================
Scenario: Configurar horario laboral
When voy a "Configuracion" -> "Mi disponibilidad"
And configuro mi horario:
| dia | trabajo | desde | hasta | pausa_desde | pausa_hasta |
| Lunes | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Martes | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Miercoles | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Jueves | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Viernes | Si | 09:00 | 15:00 | | |
| Sabado | No | | | | |
| Domingo | No | | | | |
And guardo la configuracion
Then mi disponibilidad deberia actualizarse
And no deberia recibir solicitudes fuera de ese horario
# ============================================
# BLOQUEAR DIAS ESPECIFICOS
# ============================================
Scenario: Marcar dia como no disponible
Given que necesito el 20 de enero libre
When hago click en el dia 20 de enero en el calendario
And selecciono "Marcar como no disponible"
And ingreso motivo "Vacaciones"
And confirmo
Then el dia 20 deberia mostrarse como bloqueado
And no deberia poder aceptar solicitudes para ese dia
Scenario: Bloquear rango de fechas
Given que tomo vacaciones del 15 al 22 de enero
When voy a "Agregar indisponibilidad"
And selecciono fecha inicio "15 de enero"
And selecciono fecha fin "22 de enero"
And ingreso motivo "Vacaciones"
And confirmo
Then todos esos dias deberian mostrarse como bloqueados
# ============================================
# IMPACTO EN SOLICITUDES
# ============================================
Rule: Los bloqueos no afectan visitas ya aceptadas
Scenario: Bloquear dia con visita ya aceptada
Given que tengo una visita aceptada para el 15 de enero
When intento bloquear el 15 de enero
Then deberia ver advertencia "Tienes una visita programada ese dia"
And deberia poder elegir:
| opcion |
| Cancelar el bloqueo |
| Bloquear y contactar al dueno |
Rule: Cambios de disponibilidad solo afectan solicitudes futuras
Scenario: Cambiar horario no afecta visitas existentes
Given que tengo una visita a las 17:00 el lunes
When cambio mi horario del lunes para terminar a las 16:00
And guardo los cambios
Then la visita de las 17:00 deberia mantenerse
And deberia ver advertencia sobre la inconsistencia

View File

@@ -0,0 +1,127 @@
# Fuente: album/book/ops-templates/veterinarian/03-realizar-visita.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_visits.py
# Tests Frontend: npx playwright test vet-visit.spec.ts
Feature: Realizar visita y crear informe medico
Como veterinario
Quiero registrar la atencion medica de mis visitas
Para mantener el historial clinico del paciente
Background:
Given que estoy logueado como veterinario
And tengo una visita programada para hoy con mascota "Luna"
And la visita esta en estado "Pagado"
# ============================================
# INICIAR VISITA
# ============================================
Scenario: Ver informacion antes de la visita
When accedo al detalle de la visita
Then deberia ver la direccion para llegar
And deberia ver el telefono del dueno para confirmar
And deberia ver los servicios a realizar
And deberia ver el historial previo de Luna
Scenario: Iniciar visita al llegar
Given que llegue al domicilio
When hago click en "Iniciar visita"
Then el estado deberia cambiar a "En progreso"
And deberia registrarse la hora de inicio
And deberia habilitarse el boton "Crear informe"
# ============================================
# CREAR INFORME MEDICO
# ============================================
Scenario: Completar informe con examen fisico
Given que la visita esta en progreso
When voy a "Crear informe"
And completo el examen fisico:
| campo | valor |
| Peso | 4.5 kg |
| Temperatura | 38.5 °C |
| Frecuencia cardiaca | 120 lpm |
| Frecuencia resp | 25 rpm |
| Mucosas | Rosadas |
| Hidratacion | Normal |
And guardo el informe
Then el examen fisico deberia guardarse
Scenario: Agregar diagnostico y tratamiento
Given que complete el examen fisico
When agrego el diagnostico:
"""
Otitis externa bilateral leve.
Paciente presenta prurito y secrecion ceruminosa.
Sin signos de infeccion secundaria.
"""
And agrego el tratamiento:
"""
Limpieza de oidos con solucion fisiologica.
Aplicacion de gotas oticas antibioticas.
Control en 7 dias.
"""
And guardo el informe
Then el diagnostico y tratamiento deberian guardarse
Scenario: Recetar medicamentos
Given que estoy creando el informe
When agrego medicamentos:
| nombre | dosis | frecuencia | duracion |
| Otomax gotas | 5 gotas | cada 12 horas | 7 dias |
| Meloxicam 1.5mg | 1 comp | cada 24 horas | 3 dias |
And guardo el informe
Then los medicamentos deberian aparecer en el informe
And el dueno podra verlos desde su cuenta
# ============================================
# COMPLETAR VISITA
# ============================================
Scenario: Completar visita con informe
Given que el informe esta completo
When hago click en "Completar visita"
And confirmo la finalizacion
Then la visita deberia pasar a estado "Completado"
And deberia generarse factura electronica (AFIP)
And el dueno deberia recibir notificacion
And el informe deberia ser visible para el dueno
Scenario: No puedo completar sin informe
Given que la visita esta en progreso
But no cree ningun informe
When intento completar la visita
Then deberia ver error "Debes crear el informe antes de completar"
# ============================================
# GUARDADO AUTOMATICO
# ============================================
Rule: El informe se guarda automaticamente
Scenario: Guardado automatico cada 30 segundos
Given que estoy escribiendo el informe
When pasan 30 segundos
Then deberia ver indicador "Guardado automaticamente"
Scenario: Recuperar informe despues de desconexion
Given que estaba escribiendo el informe
And perdi conexion a internet
When recupero la conexion
Then deberia recuperar mi progreso
And no deberia perder lo que escribi
# ============================================
# CASOS ESPECIALES
# ============================================
Scenario: Cancelar visita in situ
Given que llegue al domicilio
But el dueno no esta
When marco la visita como "Cancelada in situ"
And selecciono motivo "Dueno ausente"
Then la visita deberia marcarse como cancelada
And deberia generarse cargo por visita fallida

View File

@@ -0,0 +1,83 @@
# Fuente: album/book/ops-templates/veterinarian/04-zonas-cobertura.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_veterinarians.py
# Tests Frontend: npx playwright test vet-coverage.spec.ts
Feature: Gestionar zonas de cobertura
Como veterinario
Quiero definir en que zonas atiendo
Para recibir solo solicitudes que puedo cubrir
Background:
Given que estoy logueado como veterinario
And estoy en la seccion "Mi cobertura"
# ============================================
# VER ZONAS ACTUALES
# ============================================
Scenario: Ver mapa con mis zonas de cobertura
When cargo la pagina de cobertura
Then deberia ver un mapa de la ciudad
And mis zonas cubiertas deberian estar resaltadas
And deberia ver un listado de barrios seleccionados
Scenario: Ver estadisticas por zona
Given que tengo cobertura en "Palermo" y "Recoleta"
When veo el detalle de mis zonas
Then deberia ver para cada zona:
| metrica |
| Solicitudes este mes |
| Visitas completadas |
| Otros vets en la zona |
# ============================================
# AGREGAR ZONAS
# ============================================
Scenario: Agregar barrio desde el mapa
Given que no tengo cobertura en "Belgrano"
When hago click en "Belgrano" en el mapa
And confirmo agregar la zona
Then "Belgrano" deberia aparecer en mi lista de zonas
And deberia empezar a ver solicitudes de Belgrano
Scenario: Agregar multiples zonas a la vez
When selecciono los barrios:
| barrio |
| Colegiales |
| Chacarita |
| Villa Crespo |
And hago click en "Agregar seleccionados"
Then los 3 barrios deberian agregarse a mi cobertura
# ============================================
# QUITAR ZONAS
# ============================================
Scenario: Quitar zona de cobertura
Given que tengo cobertura en "Recoleta"
When hago click en "X" junto a "Recoleta"
And confirmo quitar la zona
Then "Recoleta" no deberia estar en mi lista
And no deberia ver nuevas solicitudes de Recoleta
# ============================================
# IMPACTO EN SOLICITUDES
# ============================================
Rule: Los cambios de zona afectan solo solicitudes nuevas
Scenario: Agregar zona muestra solicitudes existentes
Given que no tengo cobertura en "Belgrano"
And hay 3 solicitudes pendientes en Belgrano
When agrego "Belgrano" a mi cobertura
Then deberia ver las 3 solicitudes pendientes de Belgrano
Scenario: Quitar zona no afecta visitas aceptadas
Given que tengo cobertura en "Recoleta"
And tengo una visita aceptada en Recoleta para mañana
When quito "Recoleta" de mi cobertura
Then la visita de mañana deberia mantenerse
But no deberia ver nuevas solicitudes de Recoleta

View File

@@ -0,0 +1,100 @@
# Fuente: album/book/ops-templates/veterinarian/05-historial-pacientes.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_visits.py
# Tests Frontend: npx playwright test vet-history.spec.ts
Feature: Ver historial de pacientes
Como veterinario
Quiero acceder al historial medico de pacientes
Para tener contexto clinico en mis atenciones
Background:
Given que estoy logueado como veterinario
And estoy en la seccion "Historia clinica"
# ============================================
# BUSCAR PACIENTES
# ============================================
Scenario Outline: Buscar paciente por diferentes criterios
When busco por <criterio> con valor "<valor>"
Then deberia ver resultados que coincidan
Examples:
| criterio | valor |
| nombre dueno | Maria Garcia |
| nombre mascota | Luna |
| telefono | 1155551234 |
| email | maria@ejemplo.com |
Scenario: Busqueda sin resultados
When busco "ZZZZZ paciente inexistente"
Then deberia ver mensaje "No se encontraron resultados"
And deberia ver sugerencia "Verifica la ortografia"
# ============================================
# VER FICHA DE MASCOTA
# ============================================
Scenario: Ver ficha completa de mascota
Given que encontre a la mascota "Luna"
When hago click en Luna
Then deberia ver la ficha con:
| seccion | contenido |
| Datos basicos | Nombre, tipo, raza, edad, peso |
| Foto | Foto de la mascota |
| Dueno | Nombre y contacto del dueno |
| Vacunacion | Estado de vacunas |
| Historial | Lista de visitas |
# ============================================
# VER HISTORIAL DE VISITAS
# ============================================
Scenario: Ver listado de visitas
Given que estoy viendo la ficha de "Luna"
And Luna tiene 5 visitas completadas
When veo la seccion "Historial de visitas"
Then deberia ver las 5 visitas listadas
And deberian estar ordenadas de mas reciente a mas antigua
Scenario: Ver informe de visita de otro veterinario
Given que Luna fue atendida por "Dra. Rodriguez"
And yo no la atendi en esa visita
When hago click en esa visita
Then deberia poder ver el informe completo
# Para continuidad de atencion
# ============================================
# FILTROS Y NAVEGACION
# ============================================
Scenario: Filtrar historial por tipo de servicio
Given que estoy viendo el historial de "Luna"
When filtro por servicio "Vacunacion"
Then solo deberia ver visitas de vacunacion
Scenario: Filtrar por mis atenciones
Given que estoy viendo el historial de "Luna"
And Luna fue atendida por varios veterinarios
When marco "Solo mis atenciones"
Then solo deberia ver las visitas que yo realice
# ============================================
# PERMISOS
# ============================================
Rule: Veterinarios pueden ver historial de pacientes que atendieron
Scenario: Puedo ver historial de paciente que atendi
Given que yo atendi a "Luna" al menos una vez
When busco a Luna
Then deberia poder ver su historial completo
Scenario: Puedo ver historial de paciente con solicitud pendiente
Given que hay una solicitud pendiente para "Rocky"
And la solicitud esta en mi zona
When busco a Rocky
Then deberia poder ver su historial
# Para evaluar si acepto la solicitud

View File

@@ -0,0 +1,158 @@
# language: es
# Fuente: album/book/ops-templates/backoffice/01-gestion-solicitudes.md
# Drive: 05. ATC - Operaciones/Procedimientos
# Tests Backend: pytest tests/contracts/solicitudes/test_service_requests.py
# Tests Frontend: npx playwright test admin-requests.spec.ts
Característica: Gestion de solicitudes de servicio
Como administrador del backoffice
Quiero gestionar todas las solicitudes del sistema
Para asegurar que se atiendan correctamente
Antecedentes:
Dado que estoy logueado como administrador
Y estoy en la seccion "Solicitudes" del backoffice
# ============================================
# VER Y FILTRAR SOLICITUDES
# ============================================
Escenario: Ver listado de todas las solicitudes
Cuando cargo la pagina de solicitudes
Entonces deberia ver un listado paginado
Y cada solicitud deberia mostrar:
| campo |
| ID |
| Dueno |
| Mascota |
| Servicios |
| Estado |
| Fecha |
Esquema del escenario: Filtrar solicitudes por estado
Cuando filtro por estado "<estado>"
Entonces solo deberia ver solicitudes con estado "<estado>"
Ejemplos:
| estado |
| Pendiente |
| Coordinado |
| Pagado |
| Completado |
| Cancelado |
Escenario: Buscar solicitud especifica
Cuando busco por email "maria@ejemplo.com"
Entonces deberia ver solo solicitudes de ese dueno
Escenario: Filtrar por rango de fechas
Cuando filtro desde "01/01/2024" hasta "31/01/2024"
Entonces solo deberia ver solicitudes de enero 2024
# ============================================
# ASIGNAR VETERINARIO MANUALMENTE
# ============================================
Escenario: Asignar veterinario a solicitud pendiente
Dado que hay una solicitud pendiente en "Palermo"
Y no fue aceptada por ningun veterinario
Cuando abro el detalle de la solicitud
Y hago click en "Asignar veterinario"
Entonces deberia ver lista de veterinarios con cobertura en Palermo
Cuando selecciono "Dra. Garcia"
Y selecciono fecha "15 de enero" hora "10:00"
Y confirmo la asignacion
Entonces la solicitud deberia pasar a estado "Coordinado"
Y deberia estar asignada a Dra. Garcia
Y el dueno deberia recibir notificacion
Y el veterinario deberia recibir notificacion
Escenario: Ver disponibilidad de veterinarios antes de asignar
Dado que estoy asignando un veterinario
Cuando veo la lista de veterinarios disponibles
Entonces deberia ver para cada uno:
| informacion |
| Nombre |
| Disponibilidad |
| Visitas del dia |
| Distancia a destino |
# ============================================
# CAMBIAR ESTADO MANUALMENTE
# ============================================
Escenario: Cambiar estado de solicitud
Dado que hay una solicitud en estado "Coordinado"
Y el pago se proceso pero el webhook fallo
Cuando abro la solicitud
Y hago click en "Cambiar estado"
Y selecciono "Pagado"
Y ingreso motivo "Pago confirmado manualmente - ID MP: 12345"
Y confirmo el cambio
Entonces la solicitud deberia pasar a "Pagado"
Y el cambio deberia registrarse en el historial
Y deberia quedar mi usuario como responsable del cambio
Regla: Solo ciertos cambios de estado son validos
Escenario: No puedo volver a estado anterior
Dado que hay una solicitud en estado "Completado"
Cuando intento cambiar el estado a "Pagado"
Entonces deberia ver error "No se puede volver a un estado anterior"
Escenario: Puedo cancelar desde cualquier estado
Dado que hay una solicitud en estado "Coordinado"
Cuando cambio el estado a "Cancelado"
Y ingreso motivo de cancelacion
Entonces la solicitud deberia cancelarse
# ============================================
# VER DETALLE COMPLETO
# ============================================
Escenario: Ver historial de cambios de una solicitud
Dado que hay una solicitud con varios cambios de estado
Cuando abro el detalle de la solicitud
Y voy a la pestaña "Historial"
Entonces deberia ver todos los cambios con:
| campo |
| Fecha y hora |
| Estado anterior |
| Estado nuevo |
| Usuario |
| Motivo |
Escenario: Ver informacion de pago
Dado que hay una solicitud pagada
Cuando abro el detalle
Entonces deberia ver seccion "Pago" con:
| campo |
| Monto |
| Fecha de pago |
| Metodo |
| ID operacion MP |
| Estado del pago |
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Reasignar veterinario
Dado que hay una solicitud asignada a "Dr. Lopez"
Y Dr. Lopez no puede asistir
Cuando abro la solicitud
Y hago click en "Reasignar"
Y selecciono otro veterinario
Y confirmo
Entonces Dr. Lopez deberia ser notificado de la desasignacion
Y el nuevo vet deberia ser notificado
Y el dueno deberia ser notificado del cambio
Escenario: Solicitud sin veterinarios disponibles
Dado que hay una solicitud pendiente en una zona remota
Y no hay veterinarios con cobertura en esa zona
Cuando intento asignar veterinario
Entonces deberia ver mensaje "No hay veterinarios disponibles"
Y deberia poder expandir la busqueda a zonas cercanas

View File

@@ -0,0 +1,131 @@
# language: es
# Fuente: album/book/ops-templates/backoffice/02-gestion-usuarios.md
# Drive: 05. ATC - Operaciones/Procedimientos
# Tests Backend: pytest tests/contracts/mascotas/test_pet_owners.py
# Tests Frontend: npx playwright test admin-users.spec.ts
Característica: Gestion de usuarios
Como administrador del backoffice
Quiero gestionar duenos y veterinarios
Para mantener la base de usuarios actualizada
Antecedentes:
Dado que estoy logueado como administrador
Y estoy en el backoffice
# ============================================
# GESTION DE DUENOS
# ============================================
Escenario: Buscar dueno de mascota
Dado que estoy en la seccion "Duenos"
Cuando busco "maria@ejemplo.com"
Entonces deberia ver a Maria Garcia en los resultados
Y deberia ver sus mascotas listadas
Y deberia ver cantidad de turnos
Escenario: Ver perfil completo de dueno
Dado que encontre a "Maria Garcia"
Cuando hago click en su perfil
Entonces deberia ver:
| seccion | contenido |
| Datos personales | Nombre, email, telefono |
| Direcciones | Direcciones registradas |
| Mascotas | Lista de mascotas |
| Historial | Solicitudes anteriores |
| Pagos | Historial de pagos |
Escenario: Editar datos de dueno
Dado que estoy viendo el perfil de un dueno
Cuando hago click en "Editar"
Y cambio el telefono a "1155559999"
Y guardo los cambios
Entonces el telefono deberia actualizarse
Y deberia registrarse quien hizo el cambio
Esquema del escenario: Filtrar duenos por tipo
Dado que estoy en la lista de duenos
Cuando filtro por tipo "<tipo>"
Entonces solo deberia ver usuarios <tipo>
Ejemplos:
| tipo |
| Registrados |
| Invitados |
# ============================================
# GESTION DE VETERINARIOS
# ============================================
Escenario: Ver lista de veterinarios
Cuando voy a la seccion "Veterinarios"
Entonces deberia ver listado de todos los vets
Y cada vet deberia mostrar:
| campo |
| Nombre |
| Matricula |
| Zonas |
| Estado |
| Visitas mes |
Escenario: Agregar nuevo veterinario
Dado que estoy en la seccion "Veterinarios"
Cuando hago click en "Agregar veterinario"
Y completo los datos:
| campo | valor |
| Nombre | Dr. Juan Perez |
| Matricula | MV-12345 |
| Email | jperez@ejemplo.com |
| Telefono | 1155551234 |
| Especialidades | Clinica general |
Y selecciono zonas de cobertura:
| zona |
| Palermo |
| Recoleta |
Y genero credenciales de acceso
Y guardo
Entonces deberia crearse el veterinario
Y deberia poder loguearse con sus credenciales
Escenario: Desactivar veterinario sin citas pendientes
Dado que el veterinario "Dr. Lopez" no tiene citas pendientes
Cuando abro su perfil
Y hago click en "Desactivar"
Y confirmo la desactivacion
Entonces Dr. Lopez deberia estar inactivo
Y no deberia recibir nuevas solicitudes
Y no deberia poder loguearse
Escenario: Intentar desactivar veterinario con citas pendientes
Dado que el veterinario "Dra. Garcia" tiene 3 citas pendientes
Cuando intento desactivarla
Entonces deberia ver advertencia "Tiene 3 citas pendientes"
Y deberia ver opciones:
| opcion |
| Reasignar citas y desactivar |
| Cancelar |
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Fusionar cuentas duplicadas
Dado que hay dos cuentas para el mismo dueno:
| cuenta | email | mascotas |
| Cuenta1 | maria@gmail.com | Luna |
| Cuenta2 | maria@hotmail.com | Rocky |
Cuando selecciono ambas cuentas
Y hago click en "Fusionar"
Y elijo Cuenta1 como principal
Y confirmo
Entonces deberia existir solo Cuenta1
Y deberia tener ambas mascotas
Y el historial deberia combinarse
Escenario: Convertir invitado a registrado
Dado que hay un usuario invitado con email "invitado@test.com"
Cuando abro su perfil
Y hago click en "Enviar invitacion a registrarse"
Entonces deberia enviarse email con link de registro
Y el usuario podra crear contraseña y activar cuenta

View File

@@ -0,0 +1,131 @@
# language: es
# Fuente: album/book/ops-templates/backoffice/03-gestion-servicios.md
# Drive: 08. IT y Producto/Catalogo Servicios
# Tests Backend: pytest tests/contracts/productos/test_services.py
# Tests Frontend: npx playwright test admin-services.spec.ts
Característica: Gestion de servicios y precios
Como administrador
Quiero gestionar el catalogo de servicios
Para mantener la oferta actualizada
Antecedentes:
Dado que estoy logueado como administrador
Y estoy en la seccion "Servicios"
# ============================================
# VER CATALOGO DE SERVICIOS
# ============================================
Escenario: Ver listado de servicios
Cuando cargo la pagina de servicios
Entonces deberia ver todos los servicios organizados por categoria
Y cada servicio deberia mostrar:
| campo |
| Nombre |
| Categoria |
| Tipo mascota |
| Precio actual |
| Estado |
Esquema del escenario: Filtrar servicios
Cuando filtro por <filtro> "<valor>"
Entonces solo deberia ver servicios que coincidan
Ejemplos:
| filtro | valor |
| categoria | Vacunacion |
| tipo_mascota | Gato |
| estado | Activo |
# ============================================
# CREAR Y EDITAR SERVICIOS
# ============================================
Escenario: Agregar nuevo servicio
Cuando hago click en "Agregar servicio"
Y completo los datos:
| campo | valor |
| Nombre | Vacuna Quintuple Felina |
| Descripcion | Protege contra 5 enfermedades |
| Categoria | Vacunacion |
| Tipo mascota | Gato |
| Precio | 15000 |
| Duracion | 30 minutos |
Y guardo el servicio
Entonces el servicio deberia crearse
Y deberia aparecer en el turnero para gatos
Escenario: Editar servicio existente
Dado que existe el servicio "Consulta clinica"
Cuando abro el servicio
Y cambio la descripcion
Y guardo
Entonces la descripcion deberia actualizarse
Y las solicitudes existentes no deberian afectarse
Escenario: Desactivar servicio
Dado que existe el servicio "Servicio Antiguo"
Cuando abro el servicio
Y hago click en "Desactivar"
Y confirmo
Entonces el servicio no deberia aparecer en el turnero
Y las solicitudes existentes deberian mantenerse
# ============================================
# GESTION DE PRECIOS
# ============================================
Escenario: Actualizar precio de servicio
Dado que "Consulta clinica" tiene precio actual de 10000
Cuando abro el servicio
Y voy a la seccion "Precios"
Y hago click en "Agregar precio"
Y ingreso nuevo precio 12000
Y selecciono fecha de vigencia "01/02/2024"
Y guardo
Entonces deberia crearse el nuevo precio
Y el precio anterior deberia quedar en historial
Y el nuevo precio deberia aplicar desde la fecha indicada
Escenario: Ver historial de precios
Dado que un servicio tuvo varios cambios de precio
Cuando veo la seccion "Precios"
Entonces deberia ver historial con:
| campo |
| Precio |
| Fecha desde |
| Fecha hasta |
| Usuario |
Regla: El precio se congela al crear la solicitud
Escenario: Cambio de precio no afecta solicitudes existentes
Dado que hay una solicitud pendiente con "Consulta clinica" a 10000
Cuando cambio el precio de "Consulta clinica" a 12000
Entonces la solicitud deberia mantener el precio de 10000
# ============================================
# CONFIGURAR COMBOS Y DEPENDENCIAS
# ============================================
Escenario: Crear combo de servicios
Cuando voy a "Combos"
Y hago click en "Agregar combo"
Y configuro:
| campo | valor |
| Nombre | Plan Preventivo Felino |
| Servicios | Vacunacion, Desparasitacion, Antipulgas |
| Precio combo | 25000 |
| Descuento | 20% |
Y guardo
Entonces el combo deberia crearse
Y deberia aplicarse automaticamente cuando se seleccionen esos servicios
Escenario: Configurar servicio dependiente
Dado que "Vacunacion" requiere "Consulta clinica"
Cuando configuro la dependencia
Entonces al seleccionar "Vacunacion" en el turnero
Y "Consulta clinica" deberia agregarse automaticamente
Y no deberia poder quitarse

View File

@@ -0,0 +1,141 @@
# language: es
# Fuente: album/book/ops-templates/backoffice/04-reembolsos.md
# Drive: 07. Finanzas y contabilidad/Reembolsos
# Tests Backend: pytest tests/contracts/payments/test_refunds.py
# Tests Frontend: npx playwright test admin-refunds.spec.ts
Característica: Proceso de reembolso
Como administrador
Quiero procesar reembolsos de pagos
Para resolver cancelaciones y problemas
Antecedentes:
Dado que estoy logueado como administrador
Y estoy en el backoffice
# ============================================
# IDENTIFICAR PAGO A REEMBOLSAR
# ============================================
Escenario: Buscar solicitud pagada por ID de operacion
Cuando busco por ID de Mercado Pago "12345678"
Entonces deberia encontrar la solicitud asociada
Y deberia ver los detalles del pago
Escenario: Ver detalles de pago
Dado que encontre una solicitud pagada
Cuando veo la seccion de pago
Entonces deberia ver:
| campo |
| Monto pagado |
| Fecha de pago |
| Metodo de pago |
| ID operacion MP |
| Estado en MP |
# ============================================
# PROCESAR REEMBOLSO
# ============================================
Escenario: Reembolso total exitoso
Dado que hay una solicitud pagada por 15000
Y la visita fue cancelada
Cuando abro la solicitud
Y hago click en "Procesar reembolso"
Y selecciono "Reembolso total"
Y ingreso motivo "Cancelacion por indisponibilidad del veterinario"
Y confirmo el reembolso
Entonces deberia enviarse la solicitud de reembolso a Mercado Pago
Y deberia ver mensaje "Reembolso en proceso"
Y la solicitud deberia registrar el reembolso
Y el dueno deberia recibir email de confirmacion
Escenario: Reembolso parcial
Dado que hay una solicitud pagada por 20000
Y solo se realizo parte del servicio
Cuando proceso reembolso parcial por 10000
Y ingreso motivo "Servicio parcialmente completado"
Y confirmo
Entonces deberia procesarse reembolso por 10000
Y deberia quedar registro del monto reembolsado
Y deberia quedar registro del monto retenido
# ============================================
# TIEMPOS DE ACREDITACION
# ============================================
Esquema del escenario: Informar tiempo de acreditacion segun metodo
Dado que el pago original fue con "<metodo>"
Cuando proceso el reembolso
Entonces deberia informar al usuario:
"""
El reembolso se acreditara en <tiempo>
"""
Ejemplos:
| metodo | tiempo |
| Tarjeta credito | 1-2 resumenes de cuenta |
| Tarjeta debito | 5-10 dias habiles |
| Dinero en cuenta MP | forma inmediata |
| Transferencia | 5-10 dias habiles |
# ============================================
# VALIDACIONES Y ERRORES
# ============================================
Escenario: No puedo reembolsar mas del monto pagado
Dado que hay una solicitud pagada por 15000
Cuando intento reembolsar 20000
Entonces deberia ver error "El monto supera el pago original"
Escenario: Reembolso duplicado
Dado que ya procese un reembolso total para una solicitud
Cuando intento procesar otro reembolso
Entonces deberia ver error "Esta solicitud ya fue reembolsada"
Escenario: Pago fuera de plazo de reembolso
Dado que hay un pago de hace 200 dias
Cuando intento reembolsar
Entonces deberia ver advertencia "Fuera de plazo de reembolso de MP (180 dias)"
Y deberia sugerir "Contactar al dueno para solucion alternativa"
# ============================================
# SEGUIMIENTO
# ============================================
Escenario: Ver estado del reembolso
Dado que procese un reembolso hace 2 dias
Cuando veo el detalle del reembolso
Entonces deberia ver el estado actual en Mercado Pago
Y deberia ver historial de estados:
| estado | fecha |
| Solicitado | 01/01/2024 |
| En proceso | 01/01/2024 |
| Completado | 03/01/2024 |
Escenario: Reembolso rechazado por MP
Dado que Mercado Pago rechazo el reembolso
Cuando veo el estado
Entonces deberia ver "Reembolso rechazado"
Y deberia ver el motivo del rechazo
Y deberia poder contactar al dueno con alternativas
# ============================================
# REGISTRO Y AUDITORIA
# ============================================
Regla: Todos los reembolsos quedan registrados
Escenario: Ver historial de reembolsos
Cuando voy a "Reportes" -> "Reembolsos"
Entonces deberia ver lista de todos los reembolsos
Y cada uno deberia mostrar:
| campo |
| Solicitud |
| Monto original |
| Monto reembolso |
| Motivo |
| Procesado por |
| Fecha |
| Estado |

View File

@@ -0,0 +1,155 @@
# language: es
# Fuente: album/book/ops-templates/backoffice/05-reportes.md
# Drive: 07. Finanzas y contabilidad/Reportes
# Tests Backend: pytest tests/contracts/mascotas/test_stats.py
# Tests Frontend: npx playwright test admin-reports.spec.ts
Característica: Reportes y dashboard
Como administrador o gerente
Quiero ver metricas y generar reportes
Para tomar decisiones informadas
Antecedentes:
Dado que estoy logueado como administrador
Y estoy en el backoffice
# ============================================
# DASHBOARD
# ============================================
Escenario: Ver dashboard principal
Cuando accedo al dashboard
Entonces deberia ver metricas resumidas:
| metrica | periodo |
| Solicitudes nuevas | Hoy |
| Visitas completadas | Semana |
| Ingresos | Mes |
| Veterinarios activos | Actual |
| Tasa de conversion | Mes |
Escenario: Dashboard actualiza periodicamente
Dado que estoy en el dashboard
Cuando pasan 5 minutos
Entonces los datos deberian actualizarse
Y deberia ver indicador de ultima actualizacion
Escenario: Ver grafico de tendencia
Cuando veo el grafico de solicitudes
Entonces deberia ver la evolucion de los ultimos 30 dias
Y deberia poder comparar con periodo anterior
# ============================================
# REPORTE DE SOLICITUDES
# ============================================
Escenario: Generar reporte de solicitudes
Cuando voy a "Reportes" -> "Solicitudes"
Y selecciono periodo "Enero 2024"
Y hago click en "Generar"
Entonces deberia ver tabla con solicitudes del periodo
Y deberia ver totales por estado:
| estado | cantidad |
| Pendiente | X |
| Coordinado | X |
| Pagado | X |
| Completado | X |
| Cancelado | X |
Escenario: Filtrar reporte por multiples criterios
Cuando genero reporte con filtros:
| filtro | valor |
| Periodo | Enero 2024 |
| Zona | Palermo |
| Veterinario | Dra. Garcia |
| Estado | Completado |
Entonces deberia ver solo solicitudes que cumplan todos los criterios
# ============================================
# REPORTE DE INGRESOS
# ============================================
Escenario: Ver ingresos por periodo
Cuando voy a "Reportes" -> "Ingresos"
Y selecciono "Ultimo trimestre"
Entonces deberia ver:
| dato |
| Ingresos totales |
| Cantidad de pagos |
| Ticket promedio |
| Reembolsos realizados |
| Ingreso neto |
Escenario: Ingresos agrupados por veterinario
Cuando agrupo el reporte por "Veterinario"
Entonces deberia ver para cada vet:
| dato |
| Visitas completadas |
| Ingresos generados |
| Porcentaje del total |
Y deberian estar ordenados de mayor a menor
Escenario: Ingresos agrupados por servicio
Cuando agrupo el reporte por "Servicio"
Entonces deberia ver los servicios mas vendidos
Y deberia ver el ingreso de cada uno
Y deberia ver la cantidad de veces realizado
# ============================================
# REPORTE DE VETERINARIOS
# ============================================
Escenario: Ver performance de veterinarios
Cuando voy a "Reportes" -> "Veterinarios"
Entonces deberia ver para cada veterinario:
| metrica |
| Visitas completadas |
| Calificacion promedio |
| Tasa de aceptacion |
| Tasa de cancelacion |
| Ingresos generados |
Escenario: Identificar veterinarios con problemas
Dado que el reporte muestra la tasa de cancelacion
Cuando ordeno por "Tasa de cancelacion" descendente
Entonces deberia ver primero los vets con mas cancelaciones
Y deberia poder investigar los motivos
# ============================================
# EXPORTACION
# ============================================
Esquema del escenario: Exportar reporte en diferentes formatos
Dado que tengo un reporte generado
Cuando hago click en "Exportar"
Y selecciono formato "<formato>"
Entonces deberia descargarse el archivo en formato <formato>
Ejemplos:
| formato |
| CSV |
| Excel |
| PDF |
Escenario: Exportar reporte grande de forma asincrona
Dado que genere un reporte con mas de 10000 registros
Cuando hago click en "Exportar"
Entonces deberia ver mensaje "Generando exportacion..."
Y deberia recibir notificacion cuando este listo
Y deberia poder descargar desde el centro de notificaciones
# ============================================
# REPORTES PROGRAMADOS
# ============================================
Escenario: Programar reporte semanal
Cuando voy a "Reportes" -> "Programados"
Y creo nuevo reporte programado:
| campo | valor |
| Tipo | Ingresos |
| Frecuencia | Semanal |
| Dia | Lunes |
| Destinatarios | gerencia@ejemplo.com |
Y guardo
Entonces deberia enviarse el reporte cada lunes
Y los destinatarios deberian recibirlo por email

View File

@@ -0,0 +1,92 @@
# language: es
# Fuente: album/book/ops-templates/pet-owner/01-registro.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/common/test_users.py
# Tests Frontend: npx playwright test auth.spec.ts
Característica: Registro de usuario
Como visitante de la plataforma
Quiero poder crear una cuenta
Para gestionar mis mascotas y reservar turnos
# ============================================
# CAMINO FELIZ
# ============================================
Escenario: Registro exitoso con datos validos
Dado que estoy en la pagina de registro
Cuando ingreso email "nuevo@ejemplo.com"
Y ingreso contraseña "Password123"
Y confirmo contraseña "Password123"
Y acepto los terminos y condiciones
Y hago click en "Crear cuenta"
Entonces deberia ver mensaje "Te enviamos un email de verificacion"
Y deberia recibir email de verificacion
Escenario: Verificar email y activar cuenta
Dado que me registre con email "nuevo@ejemplo.com"
Y recibi el email de verificacion
Cuando hago click en el link de verificacion
Entonces mi cuenta deberia estar activa
Y deberia ser redirigido al dashboard
# ============================================
# VALIDACIONES
# ============================================
Esquema del escenario: Registro con datos invalidos
Dado que estoy en la pagina de registro
Cuando ingreso email "<email>"
Y ingreso contraseña "<password>"
Y confirmo contraseña "<confirmacion>"
Y hago click en "Crear cuenta"
Entonces deberia ver error "<mensaje_error>"
Ejemplos:
| email | password | confirmacion | mensaje_error |
| invalido | Password123 | Password123 | Email invalido |
| test@test.com | 123 | 123 | Contraseña muy corta |
| test@test.com | password | password | Debe contener al menos un numero |
| test@test.com | Password123 | Diferente123 | Las contraseñas no coinciden |
Escenario: Registro con email ya existente
Dado que existe un usuario con email "existente@ejemplo.com"
Y estoy en la pagina de registro
Cuando ingreso email "existente@ejemplo.com"
Y completo el resto del formulario correctamente
Y hago click en "Crear cuenta"
Entonces deberia ver error "Este email ya esta registrado"
Y deberia ver link "Recuperar contraseña"
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Registro linkea con cuenta invitado existente
# Usuario que reservo turno como invitado y ahora quiere registrarse
Dado que existe un usuario invitado con email "invitado@ejemplo.com"
Y ese usuario tiene una mascota "Luna" registrada
Y estoy en la pagina de registro
Cuando me registro con email "invitado@ejemplo.com"
Y verifico mi cuenta
Entonces deberia ver mi mascota "Luna" en el dashboard
Y deberia ver mis turnos anteriores
Escenario: Registro desde flujo de turnero
# Usuario empezo a reservar turno y decide crear cuenta
Dado que estoy en el paso final del turnero
Y ingrese mis datos de contacto
Cuando hago click en "Crear cuenta para guardar mis datos"
Entonces deberia ver formulario simplificado
Y mi email ya deberia estar pre-llenado
Y solo deberia ingresar contraseña
Escenario: Reenviar email de verificacion
Dado que me registre pero no verifique mi cuenta
Y estoy en la pagina de login
Cuando intento iniciar sesion
Entonces deberia ver "Tu cuenta no esta verificada"
Y deberia ver boton "Reenviar email"
Cuando hago click en "Reenviar email"
Entonces deberia recibir nuevo email de verificacion

View File

@@ -0,0 +1,141 @@
# language: es
# Fuente: album/book/ops-templates/pet-owner/02-reservar-turno.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/workflows/test_turnero_general.py
# Tests Frontend: npx playwright test turnero.spec.ts
# Relacionado: def/work_plan/10-flow-turnero.md
Característica: Reservar turno veterinario (Turnero)
Como dueno de mascota
Quiero reservar un turno veterinario a domicilio
Para que atiendan a mi mascota sin salir de casa
Antecedentes:
Dado que estoy en la pagina del turnero
# ============================================
# VERIFICACION DE COBERTURA
# ============================================
Escenario: Verificar cobertura en zona disponible
Cuando ingreso direccion "Av Santa Fe 1234, CABA"
Entonces deberia ver mensaje "Tenemos cobertura en tu zona"
Y deberia poder continuar al siguiente paso
Escenario: Zona sin cobertura
Cuando ingreso direccion "Calle Principal 100, Ushuaia"
Entonces deberia ver mensaje "Aun no tenemos cobertura en tu zona"
Y deberia ver formulario "Avisame cuando lleguen"
# ============================================
# FLUJO COMPLETO POR TIPO DE USUARIO
# ============================================
Esquema del escenario: Reservar turno como <tipo_usuario>
Dado que soy un usuario <tipo_usuario>
Y tengo cobertura en mi zona
Cuando completo los datos de mi mascota:
| campo | valor |
| nombre | Luna |
| tipo | Gato |
| edad | 2 años |
| castrada | Si |
Y selecciono servicios:
| servicio |
| Vacunacion |
Y selecciono fechas preferidas:
| fecha | franja |
| 2024-01-15 | Mañana |
| 2024-01-16 | Tarde |
Y completo datos de contacto con email "<email>"
Y envio la solicitud
Entonces deberia crearse una solicitud en estado "Pendiente"
Y el dueno deberia ser <estado_dueno>
Y deberia recibir email de confirmacion
Ejemplos:
| tipo_usuario | email | estado_dueno |
| invitado | nuevo@test.com | creado como invitado |
| registrado | user@test.com | mi cuenta existente |
| recurrente | conocido@test.com | identificado por email |
# ============================================
# SELECCION DE SERVICIOS
# ============================================
Escenario: Servicios filtrados por tipo de mascota
Dado que agregue una mascota tipo "Gato"
Cuando veo los servicios disponibles
Entonces deberia ver "Vacuna triple felina"
Y deberia ver "Vacuna antirabica"
Pero no deberia ver "Vacuna sextuple canina"
Escenario: Consulta clinica se agrega automaticamente con vacunacion
Dado que estoy seleccionando servicios
Cuando selecciono "Vacunacion"
Entonces "Consulta clinica" deberia agregarse automaticamente
Y deberia ver nota "Incluye revision general"
Y no deberia poder quitar "Consulta clinica"
Escenario: Servicios combo con descuento
Dado que estoy seleccionando servicios
Cuando agrego los siguientes servicios:
| servicio |
| Vacunacion |
| Desparasitacion |
| Antipulgas |
Entonces deberia ver "Plan preventivo completo"
Y el total deberia incluir descuento de combo
Escenario: Castracion no disponible para mascota castrada
Dado que mi mascota esta marcada como castrada
Cuando veo los servicios disponibles
Entonces no deberia ver "Castracion"
# ============================================
# DATOS DE CONTACTO Y CUENTA
# ============================================
Escenario: Pre-llenado de datos para usuario logueado
Dado que estoy logueado como "maria@ejemplo.com"
Y tengo registrada mascota "Firulais"
Cuando inicio el flujo de turnero
Entonces mi direccion deberia estar pre-llenada
Y deberia poder seleccionar "Firulais" de mis mascotas
Y mis datos de contacto ya deberian estar completos
Escenario: Detectar usuario existente por email
Dado que soy usuario invitado
Y existe una cuenta con email "existente@ejemplo.com"
Cuando ingreso email "existente@ejemplo.com" en datos de contacto
Entonces deberia ver "Ya tenes cuenta con este email"
Y deberia ver opciones:
| opcion |
| Iniciar sesion |
| Continuar como invitado |
# ============================================
# EDGE CASES
# ============================================
Escenario: Usuario abandona flujo a mitad
Dado que complete los datos de mascota
Y cerre el navegador sin enviar
Cuando vuelvo a la pagina del turnero
Entonces deberia poder recuperar mi progreso
# Nota: datos guardados en localStorage o session
Escenario: Multiples mascotas en una solicitud
Dado que quiero atender a 2 mascotas
Cuando agrego mascota "Luna" tipo "Gato"
Y agrego mascota "Rocky" tipo "Perro"
Y selecciono servicios para cada una
Entonces deberia crearse una solicitud con 2 mascotas
Y el precio deberia reflejar ambas
Escenario: Franja horaria especifica
Dado que solo puedo por la mañana
Cuando selecciono franja "Mañana (9-12hs)"
Entonces la solicitud deberia registrar esa preferencia
# Nota: Es preferencia, no garantia

View File

@@ -0,0 +1,154 @@
# language: es
# Fuente: album/book/ops-templates/pet-owner/03-gestion-mascotas.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_pets.py
# Tests Frontend: npx playwright test pets.spec.ts
Característica: Gestion de mascotas
Como dueno de mascota registrado
Quiero gestionar la informacion de mis mascotas
Para tenerla actualizada y acceder a su historial medico
Antecedentes:
Dado que estoy logueado como dueno de mascota
Y estoy en la seccion "Mis mascotas"
# ============================================
# AGREGAR MASCOTA
# ============================================
Escenario: Agregar mascota con datos minimos
Cuando hago click en "Agregar mascota"
Y completo el formulario:
| campo | valor |
| nombre | Luna |
| tipo | Gato |
Y hago click en "Guardar"
Entonces deberia ver "Luna" en mi lista de mascotas
Y Luna deberia aparecer disponible en el turnero
Escenario: Agregar mascota con datos completos
Cuando hago click en "Agregar mascota"
Y completo el formulario:
| campo | valor |
| nombre | Rocky |
| tipo | Perro |
| raza | Labrador |
| fecha_nacimiento | 2020-03-15 |
| peso | 25 |
| sexo | Macho |
| castrado | Si |
Y subo una foto de Rocky
Y hago click en "Guardar"
Entonces deberia ver "Rocky" con su foto en mi lista
Y deberia ver badge "Castrado"
Esquema del escenario: Validacion de datos de mascota
Cuando intento agregar mascota con <campo> igual a "<valor>"
Entonces deberia ver error "<mensaje>"
Ejemplos:
| campo | valor | mensaje |
| nombre | | El nombre es obligatorio |
| nombre | A | Nombre muy corto |
| tipo | | Selecciona el tipo |
| peso | -5 | El peso debe ser positivo |
| peso | 500 | Peso fuera de rango |
# ============================================
# EDITAR MASCOTA
# ============================================
Escenario: Editar peso de mascota
Dado que tengo una mascota "Luna" con peso 4kg
Cuando edito a Luna
Y cambio el peso a 5kg
Y guardo los cambios
Entonces Luna deberia mostrar peso "5 kg"
Escenario: Marcar mascota como castrada
Dado que tengo una mascota "Rocky" no castrado
Cuando edito a Rocky
Y marco "Esta castrado"
Y guardo los cambios
Entonces Rocky deberia mostrar badge "Castrado"
Y el servicio "Castracion" no deberia aparecer para Rocky en el turnero
Escenario: Actualizar foto de mascota
Dado que tengo una mascota "Luna" sin foto
Cuando edito a Luna
Y subo una nueva foto
Y guardo los cambios
Entonces deberia ver la foto de Luna en su tarjeta
# ============================================
# ELIMINAR MASCOTA
# ============================================
Escenario: Eliminar mascota sin historial
Dado que tengo una mascota "Nuevo" sin visitas
Cuando hago click en "Eliminar" para Nuevo
Y confirmo la eliminacion
Entonces Nuevo no deberia aparecer en mi lista
# Nota: Es soft delete
Escenario: Eliminar mascota con historial medico
Dado que tengo una mascota "Luna" con visitas anteriores
Cuando hago click en "Eliminar" para Luna
Entonces deberia ver advertencia "Luna tiene historial medico"
Y deberia ver "El historial se conservara pero no podras verlo"
Cuando confirmo la eliminacion
Entonces Luna no deberia aparecer en mi lista
Escenario: No puedo eliminar mascota con turno pendiente
Dado que tengo una mascota "Rocky" con turno pendiente
Cuando intento eliminar a Rocky
Entonces deberia ver error "Rocky tiene turnos pendientes"
Y deberia ver sugerencia "Cancela los turnos primero"
# ============================================
# VER HISTORIAL MEDICO
# ============================================
Escenario: Ver historial de visitas de mascota
Dado que tengo una mascota "Luna" con 3 visitas completadas
Cuando hago click en Luna
Y voy a la seccion "Historial"
Entonces deberia ver 3 visitas listadas
Y deberian estar ordenadas por fecha descendente
Escenario: Ver detalle de visita
Dado que tengo una mascota "Luna" con visitas
Cuando veo el historial de Luna
Y hago click en la primera visita
Entonces deberia ver:
| campo |
| Fecha |
| Veterinario |
| Diagnostico |
| Tratamiento |
| Medicamentos |
Escenario: Mascota sin historial
Dado que tengo una mascota "Nuevo" recien agregada
Cuando veo el perfil de Nuevo
Entonces la seccion "Historial" deberia estar vacia
Y deberia ver mensaje "Aun no hay visitas registradas"
Y deberia ver boton "Reservar primer turno"
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Mascota heredada de cuenta invitado
Dado que me registre con email "juan@test.com"
Y previamente reserve turno como invitado para "Firulais"
Cuando voy a "Mis mascotas"
Entonces deberia ver "Firulais" en mi lista
Y deberia ver su historial de visitas previas
Escenario: Razas filtradas por tipo
Cuando agrego una mascota tipo "Gato"
Entonces las razas disponibles deberian ser razas de gato
Y no deberia ver razas de perro como "Labrador"

View File

@@ -0,0 +1,141 @@
# language: es
# Fuente: album/book/ops-templates/pet-owner/04-pago-turno.md
# Drive: 07. Finanzas y contabilidad/Mercado Pago
# Tests Backend: pytest tests/contracts/payments/test_mercadopago.py
# Tests Frontend: npx playwright test payment.spec.ts
Característica: Pago de turno
Como dueno de mascota con turno coordinado
Quiero pagar mi turno online
Para confirmar la visita del veterinario
Antecedentes:
Dado que tengo un turno en estado "Coordinado"
Y el turno tiene asignado veterinario "Dra. Garcia"
Y la fecha asignada es "15 de enero a las 10:00"
# ============================================
# FLUJO DE PAGO EXITOSO
# ============================================
Escenario: Pagar turno con tarjeta de credito
Dado que estoy en el detalle de mi turno coordinado
Cuando hago click en "Pagar"
Entonces deberia ser redirigido a Mercado Pago
Y deberia ver el monto correcto
Cuando selecciono "Tarjeta de credito"
Y completo los datos de la tarjeta
Y confirmo el pago
Entonces deberia volver a la plataforma
Y deberia ver "Pago exitoso"
Y el turno deberia estar en estado "Pagado"
Escenario: Recibir confirmacion de pago
Dado que complete el pago exitosamente
Entonces deberia recibir email de confirmacion
Y el email deberia contener:
| campo |
| Fecha del turno |
| Direccion |
| Veterinario asignado |
| Monto pagado |
| Numero de operacion |
# ============================================
# METODOS DE PAGO
# ============================================
Esquema del escenario: Pagar con diferentes metodos
Dado que estoy en Mercado Pago
Cuando selecciono metodo "<metodo>"
Y completo el pago
Entonces el pago deberia ser <resultado>
Y el estado de acreditacion deberia ser "<acreditacion>"
Ejemplos:
| metodo | resultado | acreditacion |
| Tarjeta credito | exitoso | inmediata |
| Tarjeta debito | exitoso | inmediata |
| Dinero en cuenta | exitoso | inmediata |
| Transferencia | pendiente | 1-2 dias |
| Rapipago | pendiente | hasta 24hs |
# ============================================
# MANEJO DE ERRORES
# ============================================
Escenario: Pago rechazado por fondos insuficientes
Dado que estoy en Mercado Pago
Cuando intento pagar con tarjeta sin fondos
Entonces deberia ver error "Fondos insuficientes"
Y deberia poder reintentar con otra tarjeta
Y el turno deberia seguir en estado "Coordinado"
Escenario: Usuario cancela el pago
Dado que estoy en Mercado Pago
Cuando hago click en "Volver al sitio"
Entonces deberia volver a la plataforma
Y deberia ver mensaje "El pago fue cancelado"
Y deberia ver boton "Reintentar pago"
Y el turno deberia seguir en estado "Coordinado"
Escenario: Cierre de browser durante pago
Dado que estoy en Mercado Pago
Y cierro el navegador accidentalmente
Cuando vuelvo a la plataforma
Y voy a "Mis turnos"
Entonces deberia poder ver el estado real del pago
# Si se proceso: Pagado. Si no: Coordinado con opcion de pagar
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Link de pago expirado
Dado que recibi el link de pago hace mas de 24 horas
Cuando hago click en el link
Entonces deberia ver "Este link ha expirado"
Y deberia ver "Contacta a soporte para generar uno nuevo"
Escenario: Intento pagar turno ya pagado
Dado que mi turno ya esta en estado "Pagado"
Cuando accedo al link de pago
Entonces deberia ver "Este turno ya fue pagado"
Y deberia ver boton "Ver detalle del turno"
Escenario: Precio cambio desde la coordinacion
# Caso muy raro pero posible
Dado que el precio del servicio aumento desde que se coordino
Cuando voy a pagar
Entonces deberia ver el precio original acordado
# El precio se congela al momento de coordinacion
# ============================================
# INTEGRACION CON WEBHOOK
# ============================================
Regla: El estado del turno se actualiza via webhook de Mercado Pago
Escenario: Webhook confirma pago aprobado
Dado que el usuario completo el pago en Mercado Pago
Cuando Mercado Pago envia webhook con status "approved"
Entonces el turno deberia cambiar a estado "Pagado"
Y el veterinario deberia recibir notificacion
Y el usuario deberia recibir email de confirmacion
Escenario: Webhook informa pago pendiente
Dado que el usuario pago con transferencia bancaria
Cuando Mercado Pago envia webhook con status "pending"
Entonces el turno deberia quedarse en "Coordinado"
Y deberia registrarse el pago pendiente
Y el usuario deberia recibir email "Esperando acreditacion"
Escenario: Webhook falla pero pago se proceso
# Caso de error que requiere intervencion manual
Dado que el usuario pago exitosamente
Pero el webhook fallo por error de red
Entonces el turno seguira en "Coordinado"
Y el equipo de ops deberia recibir alerta
Y deberian poder actualizar manualmente

View File

@@ -0,0 +1,148 @@
# language: es
# Fuente: album/book/ops-templates/pet-owner/05-historial-medico.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_visits.py
# Tests Frontend: npx playwright test medical-history.spec.ts
Característica: Ver historial medico
Como dueno de mascota
Quiero ver el historial medico de mis mascotas
Para tener registro de sus visitas y tratamientos
Antecedentes:
Dado que estoy logueado como dueno de mascota
Y tengo una mascota "Luna" con visitas completadas
# ============================================
# ACCESO AL HISTORIAL
# ============================================
Escenario: Acceder al historial desde mascotas
Dado que estoy en "Mis mascotas"
Cuando hago click en "Luna"
Y voy a la seccion "Historial de visitas"
Entonces deberia ver lista de visitas de Luna
Escenario: Acceder al historial desde turnos
Dado que estoy en "Mis turnos"
Y filtro por "Completados"
Cuando hago click en un turno de Luna
Y hago click en "Ver informe medico"
Entonces deberia ver el informe de esa visita
# ============================================
# VISUALIZACION DE VISITAS
# ============================================
Escenario: Ver lista de visitas ordenadas
Dado que Luna tiene visitas en las siguientes fechas:
| fecha |
| 2024-01-15 |
| 2023-11-20 |
| 2023-06-10 |
Cuando veo el historial de Luna
Entonces deberia ver las visitas ordenadas de mas reciente a mas antigua
Y la primera deberia ser del "15 de enero 2024"
Escenario: Ver detalle completo de informe
Cuando hago click en una visita completada
Entonces deberia ver los siguientes datos:
| seccion | contenido |
| Informacion | Fecha, veterinario, servicios |
| Examen fisico | Peso, temperatura, FC, obs |
| Diagnostico | Descripcion del diagnostico |
| Tratamiento | Plan de tratamiento |
| Medicamentos | Lista con dosis y frecuencia |
| Estudios | Estudios solicitados y estado |
| Seguimiento | Recomendaciones, proximo control |
Escenario: Ver informe con medicamentos recetados
Dado que la visita incluye medicamentos:
"""
1. Amoxicilina 250mg - 1 comprimido cada 12hs por 7 dias
2. Meloxicam 1.5mg - 1 comprimido por dia por 3 dias
"""
Cuando veo el informe
Entonces deberia ver la lista de medicamentos
Y cada medicamento deberia mostrar:
| campo |
| Nombre |
| Dosis |
| Frecuencia |
| Duracion |
# ============================================
# DESCARGA Y COMPARTIR
# ============================================
Escenario: Descargar informe como PDF
Dado que estoy viendo un informe medico
Cuando hago click en "Descargar PDF"
Entonces deberia descargarse un archivo PDF
Y el PDF deberia contener toda la informacion del informe
Y deberia tener el logo de Amar Mascotas
Escenario: Imprimir informe
Dado que estoy viendo un informe medico
Cuando hago click en "Imprimir"
Entonces deberia abrirse el dialogo de impresion
Y el formato deberia ser optimizado para impresion
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Visita completada sin informe cargado
# El vet no cargo el informe todavia
Dado que Luna tiene una visita marcada como "Completada"
Pero el veterinario no cargo el informe aun
Cuando intento ver el informe
Entonces deberia ver mensaje "Informe pendiente"
Y deberia ver "El veterinario esta completando el informe"
Escenario: Informe con estudios pendientes
Dado que el informe solicita estudios de laboratorio
Y los resultados aun no estan disponibles
Cuando veo el informe
Entonces deberia ver seccion "Estudios solicitados"
Y deberia ver estado "Pendiente de resultados"
Y deberia ver nota "Se notificara cuando esten listos"
Escenario: Mascota con multiples visitas el mismo dia
# Raro pero posible: consulta de urgencia + seguimiento
Dado que Luna tuvo 2 visitas el 15 de enero
Cuando veo el historial
Entonces deberia ver ambas visitas listadas
Y deberian estar diferenciadas por hora
# ============================================
# PERMISOS Y PRIVACIDAD
# ============================================
Regla: Solo el dueno puede ver el historial de su mascota
Escenario: Dueno ve historial de su mascota
Dado que soy dueno de Luna
Cuando accedo al historial de Luna
Entonces deberia poder verlo completo
Escenario: No puedo ver historial de mascota ajena
Dado que existe mascota "Rocky" de otro dueno
Cuando intento acceder al historial de Rocky
Entonces deberia ver error "No tienes acceso"
# 403 Forbidden
# ============================================
# BUSQUEDA Y FILTROS
# ============================================
Escenario: Filtrar historial por tipo de servicio
Dado que Luna tiene visitas de vacunacion y consulta
Cuando filtro por "Vacunacion"
Entonces solo deberia ver visitas de vacunacion
Escenario: Buscar en historial por fecha
Cuando busco visitas del "2023"
Entonces solo deberia ver visitas del 2023
Y no deberia ver visitas del 2024

View File

@@ -0,0 +1,129 @@
# language: es
# Fuente: album/book/ops-templates/veterinarian/01-aceptar-solicitud.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/solicitudes/test_service_requests.py
# Tests Frontend: npx playwright test vet-requests.spec.ts
Característica: Aceptar o rechazar solicitudes de servicio
Como veterinario de la plataforma
Quiero revisar y responder a solicitudes en mi zona
Para gestionar mi agenda de visitas a domicilio
Antecedentes:
Dado que estoy logueado como veterinario
Y tengo cobertura en los barrios "Palermo" y "Recoleta"
Y estoy en el dashboard de veterinario
# ============================================
# VER SOLICITUDES PENDIENTES
# ============================================
Escenario: Ver lista de solicitudes en mi zona
Cuando veo la seccion "Solicitudes pendientes"
Entonces deberia ver solo solicitudes de "Palermo" y "Recoleta"
Y no deberia ver solicitudes de otros barrios
Escenario: Ver detalle de solicitud
Dado que hay una solicitud pendiente
Cuando hago click en la solicitud
Entonces deberia ver:
| seccion | contenido |
| Dueno | Nombre, telefono, direccion |
| Mascota | Nombre, tipo, edad, foto |
| Servicios | Lista de servicios solicitados |
| Fechas | Fechas preferidas por el dueno |
| Historial | Visitas anteriores si las hay |
# ============================================
# ACEPTAR SOLICITUD
# ============================================
Escenario: Aceptar solicitud con fecha disponible
Dado que hay una solicitud para el barrio "Palermo"
Y el dueno prefiere fechas:
| fecha | franja |
| 2024-01-15 | Mañana |
| 2024-01-16 | Tarde |
Y tengo disponibilidad el 15 de enero a las 10:00
Cuando hago click en "Aceptar"
Y selecciono fecha "15 de enero" hora "10:00"
Y confirmo la aceptacion
Entonces la solicitud deberia pasar a estado "Coordinado"
Y deberia quedar asignada a mi
Y el dueno deberia recibir notificacion con mis datos
Y la visita deberia aparecer en mi agenda
Escenario: Aceptar solicitud con datos de mascota que ya atendi
Dado que hay una solicitud para mascota "Luna"
Y yo atendi a "Luna" anteriormente
Cuando veo el detalle de la solicitud
Entonces deberia ver badge "Paciente recurrente"
Y deberia ver el historial de mis visitas anteriores a Luna
# ============================================
# RECHAZAR SOLICITUD
# ============================================
Esquema del escenario: Rechazar solicitud con motivo
Dado que hay una solicitud pendiente
Cuando hago click en "Rechazar"
Y selecciono motivo "<motivo>"
Y confirmo el rechazo
Entonces la solicitud deberia desaparecer de mi lista
Y deberia seguir visible para otros veterinarios
Ejemplos:
| motivo |
| No tengo disponibilidad |
| Fuera de mi zona |
| No realizo este servicio |
| Otro |
Escenario: Rechazar sin seleccionar motivo
Dado que hay una solicitud pendiente
Cuando hago click en "Rechazar"
Y confirmo sin seleccionar motivo
Entonces deberia poder rechazar igual
# El motivo es opcional
# ============================================
# RACE CONDITIONS
# ============================================
Regla: Solo un veterinario puede aceptar cada solicitud
Escenario: Otro vet acepta mientras estoy viendo
Dado que estoy viendo el detalle de una solicitud
Y otro veterinario acepta la misma solicitud
Cuando intento aceptarla
Entonces deberia ver error "Esta solicitud ya fue aceptada"
Y deberia ser redirigido al listado
Escenario: Solicitud desaparece cuando otro la acepta
Dado que estoy viendo el listado de solicitudes
Y otro veterinario acepta una solicitud
Entonces esa solicitud deberia desaparecer de mi lista
# Idealmente en tiempo real via websocket
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Solicitud urgente destacada
Dado que hay una solicitud marcada como "Urgente"
Cuando veo el listado de solicitudes
Entonces deberia ver la solicitud con indicador de urgencia
Y deberia aparecer primero en la lista
Escenario: Solicitud con multiples mascotas
Dado que hay una solicitud para 2 mascotas
Cuando veo el detalle
Entonces deberia ver informacion de ambas mascotas
Y deberia ver nota sobre tiempo estimado adicional
Escenario: No puedo aceptar con agenda completa
Dado que tengo mi agenda completa para las fechas de la solicitud
Cuando intento aceptar la solicitud
Entonces deberia ver advertencia "No tienes disponibilidad en las fechas preferidas"
Y deberia poder proponer una fecha alternativa

View File

@@ -0,0 +1,134 @@
# language: es
# Fuente: album/book/ops-templates/veterinarian/02-gestion-agenda.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_availability.py
# Tests Frontend: npx playwright test vet-schedule.spec.ts
Característica: Gestion de agenda veterinaria
Como veterinario
Quiero gestionar mi calendario y disponibilidad
Para organizar mis visitas a domicilio eficientemente
Antecedentes:
Dado que estoy logueado como veterinario
Y estoy en la seccion "Mi agenda"
# ============================================
# VER AGENDA
# ============================================
Escenario: Ver calendario semanal
Cuando veo mi agenda en vista semanal
Entonces deberia ver los 7 dias de la semana
Y deberia ver mis visitas programadas
Y cada visita deberia mostrar hora y nombre del paciente
Escenario: Ver visitas con codigo de color por estado
Dado que tengo visitas en diferentes estados
Cuando veo mi agenda
Entonces las visitas deberian mostrarse con colores:
| estado | color |
| Coordinado | amarillo |
| Pagado | verde |
| En progreso | azul |
| Completado | gris |
Escenario: Ver detalle de visita desde el calendario
Dado que tengo una visita programada para hoy
Cuando hago click en la visita
Entonces deberia ver popup con:
| campo | valor |
| Hora | 10:00 - 11:00 |
| Dueno | Maria Garcia |
| Direccion | Av Santa Fe 1234 |
| Mascota | Luna (Gato, 3 años) |
| Servicios | Vacunacion, Consulta |
| Estado pago | Pagado |
Y deberia ver boton "Ver detalle completo"
# ============================================
# CONFIGURAR DISPONIBILIDAD SEMANAL
# ============================================
Escenario: Configurar horario laboral
Cuando voy a "Configuracion" -> "Mi disponibilidad"
Y configuro mi horario:
| dia | trabajo | desde | hasta | pausa_desde | pausa_hasta |
| Lunes | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Martes | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Miercoles | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Jueves | Si | 09:00 | 18:00 | 13:00 | 14:00 |
| Viernes | Si | 09:00 | 15:00 | | |
| Sabado | No | | | | |
| Domingo | No | | | | |
Y guardo la configuracion
Entonces mi disponibilidad deberia actualizarse
Y no deberia recibir solicitudes fuera de ese horario
# ============================================
# BLOQUEAR DIAS ESPECIFICOS
# ============================================
Escenario: Marcar dia como no disponible
Dado que necesito el 20 de enero libre
Cuando hago click en el dia 20 de enero en el calendario
Y selecciono "Marcar como no disponible"
Y ingreso motivo "Vacaciones"
Y confirmo
Entonces el dia 20 deberia mostrarse como bloqueado
Y no deberia poder aceptar solicitudes para ese dia
Escenario: Bloquear rango de fechas
Dado que tomo vacaciones del 15 al 22 de enero
Cuando voy a "Agregar indisponibilidad"
Y selecciono fecha inicio "15 de enero"
Y selecciono fecha fin "22 de enero"
Y ingreso motivo "Vacaciones"
Y confirmo
Entonces todos esos dias deberian mostrarse como bloqueados
Escenario: Desbloquear dia
Dado que tengo el 20 de enero bloqueado
Y ya no necesito ese dia libre
Cuando hago click en el dia bloqueado
Y selecciono "Quitar bloqueo"
Entonces el dia deberia estar disponible nuevamente
# ============================================
# IMPACTO EN SOLICITUDES
# ============================================
Regla: Los bloqueos no afectan visitas ya aceptadas
Escenario: Bloquear dia con visita ya aceptada
Dado que tengo una visita aceptada para el 15 de enero
Cuando intento bloquear el 15 de enero
Entonces deberia ver advertencia "Tienes una visita programada ese dia"
Y deberia poder elegir:
| opcion |
| Cancelar el bloqueo |
| Bloquear y contactar al dueno |
Regla: Cambios de disponibilidad solo afectan solicitudes futuras
Escenario: Cambiar horario no afecta visitas existentes
Dado que tengo una visita a las 17:00 el lunes
Cuando cambio mi horario del lunes para terminar a las 16:00
Y guardo los cambios
Entonces la visita de las 17:00 deberia mantenerse
Y deberia ver advertencia sobre la inconsistencia
# ============================================
# NAVEGACION
# ============================================
Escenario: Navegar entre semanas
Cuando hago click en "Semana siguiente"
Entonces deberia ver el calendario de la proxima semana
Y deberia poder volver con "Semana anterior"
Escenario: Ir a fecha especifica
Cuando hago click en el selector de fecha
Y selecciono "15 de marzo"
Entonces deberia ver la semana que contiene el 15 de marzo

View File

@@ -0,0 +1,159 @@
# language: es
# Fuente: album/book/ops-templates/veterinarian/03-realizar-visita.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_visits.py
# Tests Frontend: npx playwright test vet-visit.spec.ts
Característica: Realizar visita y crear informe medico
Como veterinario
Quiero registrar la atencion medica de mis visitas
Para mantener el historial clinico del paciente
Antecedentes:
Dado que estoy logueado como veterinario
Y tengo una visita programada para hoy con mascota "Luna"
Y la visita esta en estado "Pagado"
# ============================================
# INICIAR VISITA
# ============================================
Escenario: Ver informacion antes de la visita
Cuando accedo al detalle de la visita
Entonces deberia ver la direccion para llegar
Y deberia ver el telefono del dueno para confirmar
Y deberia ver los servicios a realizar
Y deberia ver el historial previo de Luna
Escenario: Iniciar visita al llegar
Dado que llegue al domicilio
Cuando hago click en "Iniciar visita"
Entonces el estado deberia cambiar a "En progreso"
Y deberia registrarse la hora de inicio
Y deberia habilitarse el boton "Crear informe"
# ============================================
# CREAR INFORME MEDICO
# ============================================
Escenario: Completar informe con examen fisico
Dado que la visita esta en progreso
Cuando voy a "Crear informe"
Y completo el examen fisico:
| campo | valor |
| Peso | 4.5 kg |
| Temperatura | 38.5 °C |
| Frecuencia cardiaca | 120 lpm |
| Frecuencia resp | 25 rpm |
| Mucosas | Rosadas |
| Hidratacion | Normal |
Y guardo el informe
Entonces el examen fisico deberia guardarse
Escenario: Agregar diagnostico y tratamiento
Dado que complete el examen fisico
Cuando agrego el diagnostico:
"""
Otitis externa bilateral leve.
Paciente presenta prurito y secrecion ceruminosa.
Sin signos de infeccion secundaria.
"""
Y agrego el tratamiento:
"""
Limpieza de oidos con solucion fisiologica.
Aplicacion de gotas oticas antibioticas.
Control en 7 dias.
"""
Y guardo el informe
Entonces el diagnostico y tratamiento deberian guardarse
Escenario: Recetar medicamentos
Dado que estoy creando el informe
Cuando agrego medicamentos:
| nombre | dosis | frecuencia | duracion |
| Otomax gotas | 5 gotas | cada 12 horas | 7 dias |
| Meloxicam 1.5mg | 1 comp | cada 24 horas | 3 dias |
Y guardo el informe
Entonces los medicamentos deberian aparecer en el informe
Y el dueno podra verlos desde su cuenta
Escenario: Solicitar estudios complementarios
Dado que necesito estudios de laboratorio
Cuando agrego estudio:
| tipo | indicaciones |
| Hemograma completo | Ayuno de 8 horas |
| Perfil hepatico | Para control pre-quirurgico |
Y guardo el informe
Entonces los estudios deberian quedar registrados
Y deberian mostrarse como "Pendientes de resultado"
# ============================================
# COMPLETAR VISITA
# ============================================
Escenario: Completar visita con informe
Dado que el informe esta completo
Cuando hago click en "Completar visita"
Y confirmo la finalizacion
Entonces la visita deberia pasar a estado "Completado"
Y deberia generarse factura electronica (AFIP)
Y el dueno deberia recibir notificacion
Y el informe deberia ser visible para el dueno
Escenario: No puedo completar sin informe
Dado que la visita esta en progreso
Pero no cree ningun informe
Cuando intento completar la visita
Entonces deberia ver error "Debes crear el informe antes de completar"
# ============================================
# GUARDADO AUTOMATICO
# ============================================
Regla: El informe se guarda automaticamente
Escenario: Guardado automatico cada 30 segundos
Dado que estoy escribiendo el informe
Cuando pasan 30 segundos
Entonces deberia ver indicador "Guardado automaticamente"
Escenario: Recuperar informe despues de desconexion
Dado que estaba escribiendo el informe
Y perdi conexion a internet
Cuando recupero la conexion
Entonces deberia recuperar mi progreso
Y no deberia perder lo que escribi
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Visita con multiples mascotas
Dado que la visita incluye a "Luna" y "Rocky"
Cuando creo el informe
Entonces deberia crear un informe separado para cada mascota
Y ambos informes deberian estar asociados a la misma visita
Escenario: Cancelar visita in situ
Dado que llegue al domicilio
Pero el dueno no esta
Cuando marco la visita como "Cancelada in situ"
Y selecciono motivo "Dueno ausente"
Entonces la visita deberia marcarse como cancelada
Y deberia generarse cargo por visita fallida
# Segun politicas de la empresa
Escenario: Emergencia durante visita rutinaria
Dado que estoy en una consulta de vacunacion
Y detecto una condicion que requiere atencion urgente
Cuando agrego servicio "Consulta de urgencia"
Entonces deberia poder documentar ambos servicios
Y el precio deberia ajustarse
Escenario: Derivar a clinica
Dado que el paciente necesita atencion en clinica
Cuando marco "Derivacion a clinica"
Y especifico el motivo
Entonces deberia quedar registrada la derivacion
Y el dueno deberia recibir instrucciones

View File

@@ -0,0 +1,121 @@
# language: es
# Fuente: album/book/ops-templates/veterinarian/04-zonas-cobertura.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_veterinarians.py
# Tests Frontend: npx playwright test vet-coverage.spec.ts
Característica: Gestionar zonas de cobertura
Como veterinario
Quiero definir en que zonas atiendo
Para recibir solo solicitudes que puedo cubrir
Antecedentes:
Dado que estoy logueado como veterinario
Y estoy en la seccion "Mi cobertura"
# ============================================
# VER ZONAS ACTUALES
# ============================================
Escenario: Ver mapa con mis zonas de cobertura
Cuando cargo la pagina de cobertura
Entonces deberia ver un mapa de la ciudad
Y mis zonas cubiertas deberian estar resaltadas
Y deberia ver un listado de barrios seleccionados
Escenario: Ver estadisticas por zona
Dado que tengo cobertura en "Palermo" y "Recoleta"
Cuando veo el detalle de mis zonas
Entonces deberia ver para cada zona:
| metrica |
| Solicitudes este mes |
| Visitas completadas |
| Otros vets en la zona |
# ============================================
# AGREGAR ZONAS
# ============================================
Escenario: Agregar barrio desde el mapa
Dado que no tengo cobertura en "Belgrano"
Cuando hago click en "Belgrano" en el mapa
Y confirmo agregar la zona
Entonces "Belgrano" deberia aparecer en mi lista de zonas
Y deberia empezar a ver solicitudes de Belgrano
Escenario: Agregar barrio desde el buscador
Cuando busco "Nuñez" en el buscador de barrios
Y selecciono "Nuñez" de los resultados
Y confirmo agregar la zona
Entonces "Nuñez" deberia aparecer en mi lista
Escenario: Agregar multiples zonas a la vez
Cuando selecciono los barrios:
| barrio |
| Colegiales |
| Chacarita |
| Villa Crespo |
Y hago click en "Agregar seleccionados"
Entonces los 3 barrios deberian agregarse a mi cobertura
# ============================================
# QUITAR ZONAS
# ============================================
Escenario: Quitar zona de cobertura
Dado que tengo cobertura en "Recoleta"
Cuando hago click en "X" junto a "Recoleta"
Y confirmo quitar la zona
Entonces "Recoleta" no deberia estar en mi lista
Y no deberia ver nuevas solicitudes de Recoleta
Escenario: Quitar zona con solicitudes pendientes
Dado que tengo cobertura en "Palermo"
Y hay solicitudes pendientes en Palermo que no acepte
Cuando intento quitar "Palermo"
Entonces deberia ver advertencia "Hay X solicitudes pendientes en esta zona"
Y deberia poder confirmar de todas formas
# Las solicitudes pendientes siguen disponibles para otros vets
# ============================================
# IMPACTO EN SOLICITUDES
# ============================================
Regla: Los cambios de zona afectan solo solicitudes nuevas
Escenario: Agregar zona muestra solicitudes existentes
Dado que no tengo cobertura en "Belgrano"
Y hay 3 solicitudes pendientes en Belgrano
Cuando agrego "Belgrano" a mi cobertura
Entonces deberia ver las 3 solicitudes pendientes de Belgrano
Escenario: Quitar zona no afecta visitas aceptadas
Dado que tengo cobertura en "Recoleta"
Y tengo una visita aceptada en Recoleta para mañana
Cuando quito "Recoleta" de mi cobertura
Entonces la visita de mañana deberia mantenerse
Pero no deberia ver nuevas solicitudes de Recoleta
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Zona no disponible en el sistema
Dado que quiero cubrir "Barrio Nuevo"
Pero ese barrio no esta en el sistema
Cuando busco "Barrio Nuevo"
Entonces deberia ver mensaje "Zona no encontrada"
Y deberia ver opcion "Solicitar que agreguen esta zona"
Escenario: Zona con baja demanda
Dado que agrego una zona con poca actividad
Cuando pasan 30 dias sin solicitudes
Entonces deberia recibir sugerencia "Considera expandir tu cobertura"
Escenario: Zona saturada de veterinarios
Dado que quiero agregar "Palermo"
Y hay muchos veterinarios cubriendo Palermo
Cuando agrego la zona
Entonces deberia ver nota "Esta zona tiene alta cobertura de veterinarios"
# Informativo, no bloquea

View File

@@ -0,0 +1,157 @@
# language: es
# Fuente: album/book/ops-templates/veterinarian/05-historial-pacientes.md
# Drive: 08. IT y Producto/Funcionalidades Plataforma Veterinario
# Tests Backend: pytest tests/contracts/mascotas/test_vet_visits.py
# Tests Frontend: npx playwright test vet-history.spec.ts
Característica: Ver historial de pacientes
Como veterinario
Quiero acceder al historial medico de pacientes
Para tener contexto clinico en mis atenciones
Antecedentes:
Dado que estoy logueado como veterinario
Y estoy en la seccion "Historia clinica"
# ============================================
# BUSCAR PACIENTES
# ============================================
Esquema del escenario: Buscar paciente por diferentes criterios
Cuando busco por <criterio> con valor "<valor>"
Entonces deberia ver resultados que coincidan
Ejemplos:
| criterio | valor |
| nombre dueno | Maria Garcia |
| nombre mascota | Luna |
| telefono | 1155551234 |
| email | maria@ejemplo.com |
Escenario: Busqueda sin resultados
Cuando busco "ZZZZZ paciente inexistente"
Entonces deberia ver mensaje "No se encontraron resultados"
Y deberia ver sugerencia "Verifica la ortografia"
Escenario: Busqueda con multiples resultados
Cuando busco "Garcia"
Y hay varios duenos con apellido Garcia
Entonces deberia ver lista de coincidencias
Y cada resultado deberia mostrar:
| campo |
| Nombre completo |
| Email |
| Mascotas |
# ============================================
# VER FICHA DE MASCOTA
# ============================================
Escenario: Ver ficha completa de mascota
Dado que encontre a la mascota "Luna"
Cuando hago click en Luna
Entonces deberia ver la ficha con:
| seccion | contenido |
| Datos basicos | Nombre, tipo, raza, edad, peso |
| Foto | Foto de la mascota |
| Dueno | Nombre y contacto del dueno |
| Vacunacion | Estado de vacunas |
| Historial | Lista de visitas |
Escenario: Ver grafico de peso historico
Dado que estoy viendo la ficha de "Luna"
Y Luna tuvo multiples visitas con peso registrado
Cuando veo la seccion "Evolucion"
Entonces deberia ver grafico de peso en el tiempo
Y deberia poder detectar tendencias
# ============================================
# VER HISTORIAL DE VISITAS
# ============================================
Escenario: Ver listado de visitas
Dado que estoy viendo la ficha de "Luna"
Y Luna tiene 5 visitas completadas
Cuando veo la seccion "Historial de visitas"
Entonces deberia ver las 5 visitas listadas
Y deberian estar ordenadas de mas reciente a mas antigua
Y cada visita deberia mostrar:
| campo |
| Fecha |
| Veterinario |
| Servicios |
| Diagnostico |
Escenario: Ver informe de visita de otro veterinario
Dado que Luna fue atendida por "Dra. Rodriguez"
Y yo no la atendi en esa visita
Cuando hago click en esa visita
Entonces deberia poder ver el informe completo
# Para continuidad de atencion
Escenario: Ver informe detallado
Cuando hago click en una visita
Entonces deberia ver el informe con:
| seccion | contenido |
| Examen fisico | Peso, temperatura, signos vitales |
| Diagnostico | Descripcion del diagnostico |
| Tratamiento | Plan de tratamiento indicado |
| Medicamentos | Lista con dosis y duracion |
| Estudios | Estudios solicitados y resultados |
| Observaciones | Notas adicionales |
# ============================================
# FILTROS Y NAVEGACION
# ============================================
Escenario: Filtrar historial por tipo de servicio
Dado que estoy viendo el historial de "Luna"
Cuando filtro por servicio "Vacunacion"
Entonces solo deberia ver visitas de vacunacion
Escenario: Filtrar historial por fecha
Dado que estoy viendo el historial de "Luna"
Cuando filtro por año "2023"
Entonces solo deberia ver visitas del 2023
Escenario: Filtrar por mis atenciones
Dado que estoy viendo el historial de "Luna"
Y Luna fue atendida por varios veterinarios
Cuando marco "Solo mis atenciones"
Entonces solo deberia ver las visitas que yo realice
# ============================================
# PERMISOS
# ============================================
Regla: Veterinarios pueden ver historial de pacientes que atendieron
Escenario: Puedo ver historial de paciente que atendi
Dado que yo atendi a "Luna" al menos una vez
Cuando busco a Luna
Entonces deberia poder ver su historial completo
Escenario: Puedo ver historial de paciente con solicitud pendiente
Dado que hay una solicitud pendiente para "Rocky"
Y la solicitud esta en mi zona
Cuando busco a Rocky
Entonces deberia poder ver su historial
# Para evaluar si acepto la solicitud
# ============================================
# CASOS ESPECIALES
# ============================================
Escenario: Paciente sin historial previo
Dado que busco a "Nuevo" que nunca fue atendido
Cuando veo su ficha
Entonces el historial deberia estar vacio
Y deberia ver mensaje "Sin visitas registradas"
Escenario: Dueno con multiples mascotas
Dado que busque al dueno "Maria Garcia"
Y Maria tiene 3 mascotas
Cuando veo su perfil
Entonces deberia ver las 3 mascotas listadas
Y deberia poder navegar al historial de cada una

View File

@@ -0,0 +1,248 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gherkin Samples - Album</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f8fafc;
color: #1e293b;
line-height: 1.6;
}
.container { max-width: 1000px; margin: 0 auto; padding: 2rem 1rem; }
header { margin-bottom: 2rem; }
.breadcrumb { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; }
.breadcrumb a { color: #6366f1; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
h1 { font-size: 2rem; color: #6366f1; }
.subtitle { color: #64748b; margin-top: 0.5rem; }
.sample-badge {
display: inline-block;
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
margin-top: 0.5rem;
}
.lang-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.lang-tab {
padding: 0.5rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
background: white;
font-weight: 500;
color: #64748b;
}
.lang-tab.active {
border-color: #6366f1;
color: #6366f1;
background: #eef2ff;
}
.lang-section { display: none; }
.lang-section.active { display: block; }
.user-type {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.user-type h2 {
font-size: 1.25rem;
color: #6366f1;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-type h2::before {
content: '';
width: 8px;
height: 8px;
background: #818cf8;
border-radius: 50%;
}
.features-list { list-style: none; }
.features-list li {
border-bottom: 1px solid #e2e8f0;
}
.features-list li:last-child { border-bottom: none; }
.features-list a {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
text-decoration: none;
color: #1e293b;
}
.features-list a:hover { color: #6366f1; }
.features-list .name { font-weight: 500; }
.features-list .ext { color: #94a3b8; font-size: 0.85rem; }
.features-list .arrow { color: #94a3b8; }
.info-box {
background: #eef2ff;
border: 1px solid #c7d2fe;
border-radius: 8px;
padding: 1rem;
margin-top: 2rem;
font-size: 0.9rem;
}
.info-box h3 { font-size: 0.9rem; color: #6366f1; margin-bottom: 0.5rem; }
.keywords {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.keyword {
background: white;
border: 1px solid #c7d2fe;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.8rem;
}
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
font-size: 0.85rem;
}
footer a { color: #6366f1; text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="breadcrumb"><a href="/">Album</a> / Gherkin Samples</div>
<h1>Gherkin Samples</h1>
<p class="subtitle">BDD feature files - Given/When/Then specifications derived from ops templates</p>
<span class="sample-badge">Sample Data</span>
</header>
<div class="lang-tabs">
<button class="lang-tab active" onclick="showLang('es')">Espanol (Dado/Cuando/Entonces)</button>
<button class="lang-tab" onclick="showLang('en')">English (Given/When/Then)</button>
</div>
<!-- Spanish -->
<div id="lang-es" class="lang-section active">
<div class="user-type">
<h2>Pet Owner</h2>
<ul class="features-list">
<li><a href="/book/gherkin/es/pet-owner/01-registro.feature"><span class="name">01. Registro de Usuario</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/pet-owner/02-reservar-turno.feature"><span class="name">02. Reservar Turno</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/pet-owner/03-gestion-mascotas.feature"><span class="name">03. Gestion de Mascotas</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/pet-owner/04-pago-turno.feature"><span class="name">04. Pago de Turno</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/pet-owner/05-historial-medico.feature"><span class="name">05. Historial Medico</span><span class="ext">.feature</span></a></li>
</ul>
</div>
<div class="user-type">
<h2>Veterinarian</h2>
<ul class="features-list">
<li><a href="/book/gherkin/es/veterinarian/01-aceptar-solicitud.feature"><span class="name">01. Aceptar/Rechazar Solicitud</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/veterinarian/02-gestion-agenda.feature"><span class="name">02. Gestion de Agenda</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/veterinarian/03-realizar-visita.feature"><span class="name">03. Realizar Visita</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/veterinarian/04-zonas-cobertura.feature"><span class="name">04. Zonas de Cobertura</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/veterinarian/05-historial-pacientes.feature"><span class="name">05. Historial de Pacientes</span><span class="ext">.feature</span></a></li>
</ul>
</div>
<div class="user-type">
<h2>Backoffice</h2>
<ul class="features-list">
<li><a href="/book/gherkin/es/backoffice/01-gestion-solicitudes.feature"><span class="name">01. Gestion de Solicitudes</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/backoffice/02-gestion-usuarios.feature"><span class="name">02. Gestion de Usuarios</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/backoffice/03-gestion-servicios.feature"><span class="name">03. Gestion de Servicios</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/backoffice/04-reembolsos.feature"><span class="name">04. Reembolsos</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/es/backoffice/05-reportes.feature"><span class="name">05. Reportes</span><span class="ext">.feature</span></a></li>
</ul>
</div>
<div class="info-box">
<h3>Spanish Keywords</h3>
<div class="keywords">
<span class="keyword">Caracteristica</span>
<span class="keyword">Escenario</span>
<span class="keyword">Dado</span>
<span class="keyword">Cuando</span>
<span class="keyword">Entonces</span>
<span class="keyword">Y</span>
<span class="keyword">Pero</span>
</div>
</div>
</div>
<!-- English -->
<div id="lang-en" class="lang-section">
<div class="user-type">
<h2>Pet Owner</h2>
<ul class="features-list">
<li><a href="/book/gherkin/en/pet-owner/01-registro.feature"><span class="name">01. User Registration</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/pet-owner/02-reservar-turno.feature"><span class="name">02. Book Appointment</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/pet-owner/03-gestion-mascotas.feature"><span class="name">03. Pet Management</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/pet-owner/04-pago-turno.feature"><span class="name">04. Payment</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/pet-owner/05-historial-medico.feature"><span class="name">05. Medical History</span><span class="ext">.feature</span></a></li>
</ul>
</div>
<div class="user-type">
<h2>Veterinarian</h2>
<ul class="features-list">
<li><a href="/book/gherkin/en/veterinarian/01-aceptar-solicitud.feature"><span class="name">01. Accept/Reject Request</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/veterinarian/02-gestion-agenda.feature"><span class="name">02. Schedule Management</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/veterinarian/03-realizar-visita.feature"><span class="name">03. Conduct Visit</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/veterinarian/04-zonas-cobertura.feature"><span class="name">04. Coverage Zones</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/veterinarian/05-historial-pacientes.feature"><span class="name">05. Patient History</span><span class="ext">.feature</span></a></li>
</ul>
</div>
<div class="user-type">
<h2>Backoffice</h2>
<ul class="features-list">
<li><a href="/book/gherkin/en/backoffice/01-gestion-solicitudes.feature"><span class="name">01. Request Management</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/backoffice/02-gestion-usuarios.feature"><span class="name">02. User Management</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/backoffice/03-gestion-servicios.feature"><span class="name">03. Service Management</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/backoffice/04-reembolsos.feature"><span class="name">04. Refunds</span><span class="ext">.feature</span></a></li>
<li><a href="/book/gherkin/en/backoffice/05-reportes.feature"><span class="name">05. Reports</span><span class="ext">.feature</span></a></li>
</ul>
</div>
<div class="info-box">
<h3>English Keywords</h3>
<div class="keywords">
<span class="keyword">Feature</span>
<span class="keyword">Scenario</span>
<span class="keyword">Given</span>
<span class="keyword">When</span>
<span class="keyword">Then</span>
<span class="keyword">And</span>
<span class="keyword">But</span>
</div>
</div>
</div>
<footer>
<a href="/">&larr; Album</a> |
<a href="/book/ops-templates/">Source Templates</a> |
<a href="/book/feature-flow/">Feature Flow Pipeline</a>
</footer>
</div>
<script>
function showLang(lang) {
document.querySelectorAll('.lang-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.lang-tab').forEach(t => t.classList.remove('active'));
document.getElementById('lang-' + lang).classList.add('active');
event.target.classList.add('active');
}
</script>
</body>
</html>

160
atlas/index.html Normal file
View File

@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Album · Pawprint</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='%2315803d'%3E%3Cpath d='M4 8 C4 8 12 6 24 10 L24 42 C12 38 4 40 4 40 Z' opacity='0.3'/%3E%3Cpath d='M44 8 C44 8 36 6 24 10 L24 42 C36 38 44 40 44 40 Z' opacity='0.5'/%3E%3Cpath d='M4 8 C4 8 12 6 24 10 M44 8 C44 8 36 6 24 10' fill='none' stroke='%2315803d' stroke-width='2'/%3E%3Cpath d='M4 40 C4 40 12 38 24 42 M44 40 C44 40 36 38 24 42' fill='none' stroke='%2315803d' stroke-width='2'/%3E%3Cline x1='24' y1='10' x2='24' y2='42' stroke='%2315803d' stroke-width='2'/%3E%3C/svg%3E">
<style>
* { box-sizing: border-box; }
html { background: #0a0a0a; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 960px;
margin: 0 auto;
padding: 2rem 1rem;
line-height: 1.6;
color: #e5e5e5;
background: #163528;
}
header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.logo { width: 64px; height: 64px; color: white; }
h1 { font-size: 2.5rem; margin: 0; color: white; }
.tagline {
color: rgba(255,255,255,0.85);
margin-bottom: 2rem;
border-bottom: 1px solid rgba(255,255,255,0.3);
padding-bottom: 2rem;
}
section {
background: #000000;
padding: 1.5rem;
margin: 1.5rem 0;
border-radius: 12px;
}
section h2 {
margin: 0 0 1rem 0;
font-size: 1.2rem;
color: #86efac;
}
.composition {
background: #000000;
border: 2px solid #3e915d;
padding: 1rem;
border-radius: 12px;
}
.composition h3 { margin: 0 0 0.75rem 0; font-size: 1.1rem; color: #86efac; }
.composition > p { margin: 0 0 1rem 0; font-size: 0.9rem; color: #a3a3a3; }
.components {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
}
.component {
background: #0a0a0a;
border: 1px solid #6b665e;
padding: 0.75rem;
border-radius: 8px;
}
.component h4 { margin: 0 0 0.25rem 0; font-size: 0.95rem; color: #86efac; }
.component p { margin: 0; font-size: 0.85rem; color: #a3a3a3; }
.books {
list-style: none;
padding: 0;
margin: 0;
}
.books li {
padding: 0.75rem 0;
border-bottom: 1px solid #6b665e;
display: flex;
justify-content: space-between;
align-items: center;
}
.books li:last-child { border-bottom: none; }
.books a { color: #86efac; text-decoration: none; font-weight: 500; }
.books a:hover { text-decoration: underline; }
.status {
display: none;
}
footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(255,255,255,0.3);
font-size: 0.85rem;
color: rgba(255,255,255,0.7);
}
footer a { color: white; }
footer .disabled { opacity: 0.5; }
</style>
</head>
<body>
<header style="position:relative;">
<!-- Open book -->
<svg class="logo" viewBox="0 0 48 48" fill="currentColor">
<path d="M4 8 C4 8 12 6 24 10 L24 42 C12 38 4 40 4 40 Z" opacity="0.3"/>
<path d="M44 8 C44 8 36 6 24 10 L24 42 C36 38 44 40 44 40 Z" opacity="0.5"/>
<path d="M4 8 C4 8 12 6 24 10 M44 8 C44 8 36 6 24 10" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M4 40 C4 40 12 38 24 42 M44 40 C44 40 36 38 24 42" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="24" y1="10" x2="24" y2="42" stroke="currentColor" stroke-width="2"/>
</svg>
<h1>Album</h1>
{% if pawprint_url %}<a href="{{ pawprint_url }}" style="position:absolute;right:0;top:50%;transform:translateY(-50%);color:rgba(255,255,255,0.7);font-size:0.85rem;">← Pawprint</a>{% endif %}
</header>
<p class="tagline">conocimiento y toma de decisiones <!-- Actionable documentation --></p>
<section>
<div class="composition">
<h3>Books</h3>
<div class="components">
<div class="component"><h4>Template</h4></div>
<div class="component"><h4>Larder</h4></div>
</div>
</div>
</section>
<section>
<h2>Books</h2>
<ul class="books">
{% for book in books %}
<li>
<a href="/book/{{ book.slug }}/">{{ book.title }}</a>
<span class="status {% if book.status == 'ready' %}ready{% elif book.status == 'building' %}building{% endif %}">{{ book.status | capitalize }}</span>
</li>
{% else %}
<li><span style="color:#86efac;font-weight:500;">--</span><span class="status">Pending</span></li>
{% endfor %}
</ul>
</section>
<section>
<h2>Templates</h2>
<ul class="books">
{% for template in templates %}
<li><a href="/template/{{ template.slug }}/">{{ template.title }}</a><span class="status {% if template.status == 'ready' %}ready{% elif template.status == 'building' %}building{% endif %}">{{ template.status | capitalize }}</span></li>
{% else %}
<li><span style="color:#86efac;font-weight:500;">--</span><span class="status">Pending</span></li>
{% endfor %}
</ul>
</section>
<section>
<h2>Larders</h2>
<ul class="books">
{% for larder in larders %}
<li><a href="/book/feature-form-samples/larder/">{{ larder.title }}</a><span class="status {% if larder.status == 'ready' %}ready{% elif larder.status == 'building' %}building{% endif %}">{{ larder.status | capitalize }}</span></li>
{% else %}
<li><span style="color:#86efac;font-weight:500;">--</span><span class="status">Pending</span></li>
{% endfor %}
</ul>
</section>
<footer>
{% if pawprint_url %}<a href="{{ pawprint_url }}">← Pawprint</a>{% else %}<span class="disabled">← Pawprint</span>{% endif %}
</footer>
</body>
</html>

369
atlas/main.py Normal file
View File

@@ -0,0 +1,369 @@
"""
Album - Documentation system.
"""
import os
import httpx
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI(title="Album", version="0.1.0")
BASE_DIR = Path(__file__).parent.resolve()
BOOK_DIR = BASE_DIR / "book"
STATIC_DIR = BASE_DIR / "static"
# Create static directory if it doesn't exist
STATIC_DIR.mkdir(exist_ok=True)
templates = Jinja2Templates(directory=str(BASE_DIR))
# Serve static files
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# Pawprint URL for data fetching
PAWPRINT_URL = os.getenv("PAWPRINT_URL", "http://localhost:12000")
def get_data():
"""Fetch data from pawprint hub."""
try:
resp = httpx.get(f"{PAWPRINT_URL}/api/data/album", timeout=5.0)
if resp.status_code == 200:
return resp.json()
except Exception as e:
print(f"Failed to fetch data from pawprint: {e}")
return {"templates": [], "larders": [], "books": []}
@app.get("/health")
def health():
return {"status": "ok", "service": "album"}
@app.get("/")
def index(request: Request):
data = get_data()
return templates.TemplateResponse("index.html", {
"request": request,
"pawprint_url": os.getenv("PAWPRINT_EXTERNAL_URL", PAWPRINT_URL),
**data,
})
@app.get("/api/data")
def api_data():
"""API endpoint for frontend data (proxied from pawprint)."""
return get_data()
# --- Book: Feature Flow (HTML presentations) ---
@app.get("/book/feature-flow/", response_class=HTMLResponse)
@app.get("/book/feature-flow", response_class=HTMLResponse)
def feature_flow_index():
"""Redirect to English presentation by default"""
return """<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>Feature Flow</title>
<style>
body { font-family: system-ui; background: #0f172a; color: #e2e8f0; min-height: 100vh;
display: flex; flex-direction: column; align-items: center; justify-content: center; }
h1 { font-size: 2rem; margin-bottom: 2rem; }
.links { display: flex; gap: 1rem; }
a { background: #334155; color: #e2e8f0; padding: 1rem 2rem; border-radius: 8px;
text-decoration: none; font-size: 1.1rem; }
a:hover { background: #475569; }
</style></head>
<body>
<h1>Feature Flow - Standardization Pipeline</h1>
<div class="links">
<a href="/book/feature-flow/en">English</a>
<a href="/book/feature-flow/es">Español</a>
</div>
</body></html>"""
@app.get("/book/feature-flow/en", response_class=HTMLResponse)
def feature_flow_en():
html_file = BOOK_DIR / "feature-flow" / "index-en.html"
return HTMLResponse(html_file.read_text())
@app.get("/book/feature-flow/es", response_class=HTMLResponse)
def feature_flow_es():
html_file = BOOK_DIR / "feature-flow" / "index-es.html"
return HTMLResponse(html_file.read_text())
# --- Book: Feature Form Samples (templated book) ---
@app.get("/book/feature-form-samples/", response_class=HTMLResponse)
@app.get("/book/feature-form-samples", response_class=HTMLResponse)
def feature_form_samples_index(request: Request):
"""Templated book landing page"""
data = get_data()
book = next((b for b in data.get("books", []) if b["slug"] == "feature-form-samples"), None)
if not book:
return HTMLResponse("<h1>Book not found</h1>", status_code=404)
return templates.TemplateResponse("book-template.html", {
"request": request,
"book": book,
})
@app.get("/book/feature-form-samples/template/", response_class=HTMLResponse)
@app.get("/book/feature-form-samples/template", response_class=HTMLResponse)
def feature_form_samples_template():
"""View the template - styled like actual feature forms"""
html = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feature Form Template · Album</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f8fafc;
color: #1e293b;
line-height: 1.6;
}
.container { max-width: 800px; margin: 0 auto; padding: 2rem 1rem; }
header { margin-bottom: 1.5rem; }
.breadcrumb { font-size: 0.9rem; color: #64748b; margin-bottom: 0.5rem; }
.breadcrumb a { color: #15803d; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
h1 { font-size: 1.5rem; color: #15803d; }
.meta { display: flex; gap: 0.5rem; margin-top: 0.5rem; font-size: 0.8rem; }
.meta span { background: #f1f5f9; padding: 0.2rem 0.5rem; border-radius: 4px; color: #64748b; }
.meta .template { background: #dbeafe; color: #1d4ed8; }
.form-card {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
overflow: hidden;
}
.form-header {
background: linear-gradient(135deg, #15803d, #22c55e);
color: white;
padding: 1rem 1.5rem;
}
.form-header h2 { font-size: 1.1rem; font-weight: 600; }
.form-body { padding: 1.5rem; }
.field { margin-bottom: 1.25rem; }
.field:last-child { margin-bottom: 0; }
.field-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin-bottom: 0.35rem;
}
.field-value {
background: #f8fafc;
border: 2px dashed #cbd5e1;
border-radius: 6px;
padding: 0.75rem;
font-size: 0.95rem;
color: #94a3b8;
font-style: italic;
min-height: 2.5rem;
}
.field-value.multiline { min-height: 4rem; }
.field-steps .field-value { padding-left: 0.5rem; }
.field-steps ol { list-style: none; padding-left: 0; margin: 0; }
.field-steps li {
position: relative;
padding-left: 2rem;
margin-bottom: 0.5rem;
}
.field-steps li::before {
content: attr(data-step);
position: absolute;
left: 0;
width: 1.5rem;
height: 1.5rem;
background: #cbd5e1;
color: white;
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.field-problems .field-value { background: #fef2f2; border-color: #fecaca; }
.field-special .field-value { background: #fffbeb; border-color: #fde68a; }
.field-technical .field-value { background: #f0fdf4; border-color: #bbf7d0; font-family: monospace; font-size: 0.85rem; }
footer {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
font-size: 0.85rem;
}
footer a { color: #15803d; text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="breadcrumb">
<a href="/">Album</a> / <a href="/book/feature-form-samples/">Feature Form Samples</a> / Template
</div>
<h1>Feature Form Template</h1>
<div class="meta">
<span class="template">Template</span>
</div>
</header>
<div class="form-card">
<div class="form-header">
<h2>[Nombre del Flujo]</h2>
</div>
<div class="form-body">
<div class="field">
<label class="field-label">Tipo de Usuario</label>
<div class="field-value">[Dueno de mascota / Veterinario / Admin]</div>
</div>
<div class="field">
<label class="field-label">Punto de Entrada</label>
<div class="field-value">[Que pagina/boton/link]</div>
</div>
<div class="field">
<label class="field-label">Objetivo del Usuario</label>
<div class="field-value">[Objetivo en una oracion]</div>
</div>
<div class="field field-steps">
<label class="field-label">Pasos</label>
<div class="field-value multiline">
<ol>
<li data-step="1">[Primera cosa que hace el usuario]</li>
<li data-step="2">[Segunda cosa que hace el usuario]</li>
<li data-step="3">[etc.]</li>
</ol>
</div>
</div>
<div class="field">
<label class="field-label">Resultado Esperado</label>
<div class="field-value">[Resultado esperado cuando todo funciona]</div>
</div>
<div class="field field-problems">
<label class="field-label">Problemas Comunes</label>
<div class="field-value multiline">[Problema 1]<br>[Problema 2]</div>
</div>
<div class="field field-special">
<label class="field-label">Casos Especiales</label>
<div class="field-value multiline">[Caso especial 1]<br>[Caso especial 2]</div>
</div>
<div class="field">
<label class="field-label">Flujos Relacionados</label>
<div class="field-value">[Otros flujos que se conectan con este]</div>
</div>
<div class="field field-technical">
<label class="field-label">Notas Tecnicas</label>
<div class="field-value">[Notas para el equipo de desarrollo]</div>
</div>
</div>
</div>
<footer>
<a href="/book/feature-form-samples/">&larr; Feature Form Samples</a>
</footer>
</div>
</body>
</html>"""
return HTMLResponse(html)
@app.get("/book/feature-form-samples/larder/", response_class=HTMLResponse)
@app.get("/book/feature-form-samples/larder", response_class=HTMLResponse)
def feature_form_samples_larder():
"""Browse the larder (actual data)"""
html_file = BOOK_DIR / "feature-form-samples" / "index.html"
if html_file.exists():
return HTMLResponse(html_file.read_text())
return HTMLResponse("<h1>Larder index not found</h1>", status_code=404)
@app.get("/book/feature-form-samples/larder/{user_type}/{filename}", response_class=HTMLResponse)
def feature_form_samples_detail(request: Request, user_type: str, filename: str):
"""View a specific feature form"""
# Look in the larder subfolder (feature-form)
larder_dir = BOOK_DIR / "feature-form-samples" / "feature-form"
file_path = larder_dir / user_type / filename
if not file_path.exists():
return HTMLResponse("<h1>Not found</h1>", status_code=404)
content = file_path.read_text()
detail_file = BOOK_DIR / "feature-form-samples" / "detail.html"
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), {
"request": request,
"user_type": user_type,
"filename": filename,
"content": content,
})
# --- Book: Gherkin Samples ---
@app.get("/book/gherkin-samples/", response_class=HTMLResponse)
@app.get("/book/gherkin-samples", response_class=HTMLResponse)
@app.get("/book/gherkin/", response_class=HTMLResponse) # Alias
@app.get("/book/gherkin", response_class=HTMLResponse) # Alias
def gherkin_samples_index():
"""Browse gherkin samples"""
html_file = BOOK_DIR / "gherkin-samples" / "index.html"
return HTMLResponse(html_file.read_text())
@app.get("/book/gherkin-samples/{lang}/{user_type}/{filename}", response_class=HTMLResponse)
@app.get("/book/gherkin/{lang}/{user_type}/{filename}", response_class=HTMLResponse) # Alias
def gherkin_samples_detail(request: Request, lang: str, user_type: str, filename: str):
"""View a specific gherkin file"""
file_path = BOOK_DIR / "gherkin-samples" / lang / user_type / filename
if not file_path.exists() or not file_path.suffix == ".feature":
return HTMLResponse("<h1>Not found</h1>", status_code=404)
content = file_path.read_text()
detail_file = BOOK_DIR / "gherkin-samples" / "detail.html"
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), {
"request": request,
"lang": lang,
"user_type": user_type,
"filename": filename,
"content": content,
})
# --- Book: Architecture Model (static site) ---
app.mount("/book/arch-model", StaticFiles(directory=str(BOOK_DIR / "arch-model"), html=True), name="arch-model")
# --- Book: Drive Index ---
@app.get("/book/drive-index/", response_class=HTMLResponse)
@app.get("/book/drive-index", response_class=HTMLResponse)
def drive_index():
"""Browse drive index"""
html_file = BOOK_DIR / "drive-index" / "index.html"
return HTMLResponse(html_file.read_text())
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=int(os.getenv("PORT", "12002")),
reload=os.getenv("DEV", "").lower() in ("1", "true"),
)

4
atlas/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
fastapi>=0.104.0
uvicorn>=0.24.0
jinja2>=3.1.0
httpx>=0.25.0

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}

View File

@@ -0,0 +1 @@
!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var i=n.querySelector(".line-numbers-rows");if(i){var r=parseInt(n.getAttribute("data-start"),10)||1,s=r+(i.children.length-1);t<r&&(t=r),t>s&&(t=s);var l=t-r;return i.children[l]}}},resize:function(e){r([e])},assumeViewportIndependence:!0},i=void 0;window.addEventListener("resize",(function(){t.assumeViewportIndependence&&i===window.innerWidth||(i=window.innerWidth,r(Array.prototype.slice.call(document.querySelectorAll("pre.line-numbers"))))})),Prism.hooks.add("complete",(function(t){if(t.code){var i=t.element,s=i.parentNode;if(s&&/pre/i.test(s.nodeName)&&!i.querySelector(".line-numbers-rows")&&Prism.util.isActive(i,e)){i.classList.remove(e),s.classList.add(e);var l,o=t.code.match(n),a=o?o.length+1:1,u=new Array(a+1).join("<span></span>");(l=document.createElement("span")).setAttribute("aria-hidden","true"),l.className="line-numbers-rows",l.innerHTML=u,s.hasAttribute("data-start")&&(s.style.counterReset="linenumber "+(parseInt(s.getAttribute("data-start"),10)-1)),t.element.appendChild(l),r([s]),Prism.hooks.run("line-numbers",t)}}})),Prism.hooks.add("line-numbers",(function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}))}function r(e){if(0!=(e=e.filter((function(e){var n,t=(n=e,n?window.getComputedStyle?getComputedStyle(n):n.currentStyle||null:null)["white-space"];return"pre-wrap"===t||"pre-line"===t}))).length){var t=e.map((function(e){var t=e.querySelector("code"),i=e.querySelector(".line-numbers-rows");if(t&&i){var r=e.querySelector(".line-numbers-sizer"),s=t.textContent.split(n);r||((r=document.createElement("span")).className="line-numbers-sizer",t.appendChild(r)),r.innerHTML="0",r.style.display="block";var l=r.getBoundingClientRect().height;return r.innerHTML="",{element:e,lines:s,lineHeights:[],oneLinerHeight:l,sizer:r}}})).filter(Boolean);t.forEach((function(e){var n=e.sizer,t=e.lines,i=e.lineHeights,r=e.oneLinerHeight;i[t.length-1]=void 0,t.forEach((function(e,t){if(e&&e.length>1){var s=n.appendChild(document.createElement("span"));s.style.display="block",s.textContent=e}else i[t]=r}))})),t.forEach((function(e){for(var n=e.sizer,t=e.lineHeights,i=0,r=0;r<t.length;r++)void 0===t[r]&&(t[r]=n.children[i++].getBoundingClientRect().height)})),t.forEach((function(e){var n=e.sizer,t=e.element.querySelector(".line-numbers-rows");n.style.display="none",n.innerHTML="",e.lineHeights.forEach((function(e,n){t.children[n].style.height=e+"px"}))}))}}}();

View File

@@ -0,0 +1 @@
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}

1
atlas/static/prism/prism.min.js vendored Normal file

File diff suppressed because one or more lines are too long