Docs StockMind
07
SRD Backend — StockMind
APIs, workers, infraestructura, integraciones externas y contratos de interfaz
Versión
v1.0
Fecha
Mayo 2026
Audiencia
Backend Engineering · Arquitectura
Estado
DRAFT
STOCKMIND
Inteligencia estadística para inventarios que no se quiebran.
Módulo #1 — Cheryx Suite

[CONSEJO — Doc 07] Contrarian: El error más costoso en SaaS B2B early-stage es over-engineering el backend antes de tener 10 clientes. Una API REST bien documentada con Postgres y Celery resuelve los primeros 200 clientes. Kafka, microservicios y event sourcing son problemas de mes 60, no de mes 1. First Principles: El motor estadístico es el corazón del producto, no la API. La API es el camino hacia el motor. Todo lo que complique el motor (latencia, dependencias externas, I/O bloqueante) daña el producto más que cualquier bug de CRUD. Expansionist: El schema de datos debe soportar multi-tenant y multi-país desde el día 1, aunque en MVP solo opere un tenant y un país. Agregar estas dimensiones después es una migración cara. Outsider: Los contadores y gerentes RRHH de PyMEs CR usan Alegra. Cualquier fricción en la integración Alegra mata la adopción más que cualquier bug del motor. La integración con el ERP del cliente es, funcionalmente, parte del producto. Executor: El motor estadístico debe ser un worker Celery completamente asíncrono. Nunca bloquear una request HTTP esperando que statsforecast termine. Una forecast run de 5,000 SKUs tarda minutos — el usuario no puede esperar en una pantalla de carga. Security Auditor: El acceso a datos de un tenant nunca puede filtrar datos de otro. Row-Level Security (RLS) en PostgreSQL como segunda línea de defensa detrás del ORM. Si el ORM filtra mal, el RLS corta. Dos capas. Chairman: La arquitectura backend es una decisión de largo plazo. Cambiar el schema de datos en producción con 50 clientes es tres veces más caro que hacerlo bien ahora. Invertir el tiempo en el modelo de datos correcto desde el día 1.


1. Stack técnico backend

Componente Tecnología Versión Propósito
Runtime Python 3.12 Backend language
Framework FastAPI 0.115+ API REST + async I/O
ORM SQLAlchemy 2.0 (async) Acceso a base de datos
Validación Pydantic v2 Schemas de request/response
Workers Celery 5.x Procesamiento asíncrono del motor
Broker/cache Redis 7.x Celery broker + cache de resultados
Base de datos PostgreSQL 16 Persistencia principal
Migraciones Alembic latest Migraciones de schema versionadas
Auth python-jose latest JWT signing/verification
Email Resend SDK latest Emails transaccionales (magic link)
HTTP client httpx latest Llamadas a APIs externas (Alegra, ONVO Pay)
Testing pytest + pytest-asyncio latest Tests unitarios e integración
Monitoreo Sentry SDK latest Error tracking
Logging structlog latest Logs estructurados (JSON)

Deployment: Coolify sobre Hostinger KVM2 (2 vCPU, 8 GB RAM). Contenedores Docker gestionados por Coolify. PostgreSQL y Redis corren en el mismo VPS hasta ~50 clientes activos.


2. Arquitectura general

┌─────────────────────────────────────────────────────────────┐
│                     CLIENTE (Next.js)                        │
└─────────────────────────────┬───────────────────────────────┘
                              │ HTTPS / JSON
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                  FastAPI Application                         │
│  ┌──────────────┐  ┌───────────────┐  ┌──────────────────┐  │
│  │  Auth Router  │  │  Core Router  │  │  Webhook Router  │  │
│  │  /auth/*      │  │  /api/v1/*    │  │  /webhooks/*     │  │
│  └──────────────┘  └───────────────┘  └──────────────────┘  │
│                              │                               │
│  ┌───────────────────────────▼──────────────────────────┐   │
│  │              Services (Business Logic)                 │   │
│  │  InventoryService · OrderService · ForecastService     │   │
│  │  TenantService · BillingService · IntegrationService   │   │
│  └───────────────────────────┬──────────────────────────┘   │
│                              │                               │
│  ┌───────────────────────────▼──────────────────────────┐   │
│  │          SQLAlchemy async (Repository layer)          │   │
│  └───────────────────────────┬──────────────────────────┘   │
└─────────────────────────────┬───────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              │                               │
              ▼                               ▼
┌─────────────────────┐         ┌─────────────────────────────┐
│   PostgreSQL 16     │         │   Redis 7                   │
│   Schema: sm_*      │         │   Celery broker + cache     │
└─────────────────────┘         └──────────┬──────────────────┘
                                           │
                                           ▼
                              ┌─────────────────────────────┐
                              │   Celery Workers             │
                              │   ┌─────────────────────┐   │
                              │   │  forecast_worker     │   │
                              │   │  (statsforecast)     │   │
                              │   └─────────────────────┘   │
                              │   ┌─────────────────────┐   │
                              │   │  email_worker        │   │
                              │   │  (Resend)            │   │
                              │   └─────────────────────┘   │
                              │   ┌─────────────────────┐   │
                              │   │  integration_worker  │   │
                              │   │  (Alegra sync)       │   │
                              │   └─────────────────────┘   │
                              └─────────────────────────────┘

3. API REST — contratos principales

3.1 Convenciones generales

3.2 Autenticación

POST /auth/magic-link
Body: {"email": "string"}
Response: {"message": "Magic link sent"}

GET /auth/verify?token=<token>
Response: {
  "access_token": "jwt_string",
  "token_type": "bearer",
  "expires_in": 2592000  // 30 días en segundos
}

POST /auth/logout
Headers: Authorization: Bearer <token>
Response: {"message": "Logged out"}

El JWT contiene: user_id, tenant_id, role, exp. El tenant_id es el campo crítico para el aislamiento de datos.

3.3 SKUs / Inventario

GET /inventory/skus
Params: page, page_size, sort_by, sort_order,
        status (critical|low|ok|overstock|nodata),
        abc_class (A|B|C),
        search (string, fuzzy match en código y descripción)
Response: PaginatedResponse[SkuListItem]

GET /inventory/skus/{sku_id}
Response: SkuDetail (incluye forecast + last_order + explanation_html)

POST /inventory/skus
Body: SkuCreate
Response: SkuDetail

PUT /inventory/skus/{sku_id}
Body: SkuUpdate (campos parciales)
Response: SkuDetail

DELETE /inventory/skus/{sku_id}
Response: {"message": "SKU archived"}  // soft delete, nunca hard delete

GET /inventory/summary
Response: {
  "total_skus": int,
  "by_status": {"critical": int, "low": int, "ok": int, "overstock": int, "nodata": int},
  "by_abc": {"A": int, "B": int, "C": int},
  "alerts": [AlertItem]  // máximo 5 alertas más urgentes
}

POST /inventory/import/csv
Body: multipart/form-data (archivo CSV + tenant_id implícito del JWT)
Response: {"job_id": "uuid", "status": "queued", "estimated_minutes": int}

GET /inventory/import/{job_id}/status
Response: {"status": "queued|processing|completed|failed", "progress": 0.0-1.0, "errors": [...]}

3.4 Forecast

GET /forecast/skus/{sku_id}
Response: {
  "sku_id": "uuid",
  "method_used": "CrostonTSB",
  "method_reason": "intermittent demand (ADI=2.4, CV²=0.38)",
  "forecast": [{"date": "2026-06-01", "value": 45.2, "ci_lower": 28.1, "ci_upper": 62.3}],
  "history": [{"date": "2026-04-01", "value": 38}],
  "reorder_point": 200,
  "safety_stock": 45,
  "eoq": 350,
  "explanation_html": "<p>El motor seleccionó...</p>",
  "last_run_at": "2026-05-15T08:00:00Z",
  "drift_status": "ok|alert|critical"
}

POST /forecast/run
Body: {"sku_ids": ["uuid", ...] | null}  // null = todos los SKUs del tenant
Response: {"job_id": "uuid", "status": "queued"}

GET /forecast/run/{job_id}/status
Response: {"status": "queued|running|completed|failed", "progress": 0.0-1.0, "skus_processed": int}

3.5 Órdenes de Compra

GET /orders
Params: status (pending|approved|received|rejected), page, page_size
Response: PaginatedResponse[OrderListItem]

GET /orders/{order_id}
Response: OrderDetail

POST /orders/approve/{order_id}
Body: {"quantity_override": int | null}  // null = usar cantidad sugerida
Response: OrderDetail (status="approved")

POST /orders/reject/{order_id}
Body: {"reason": "string"}
Response: OrderDetail (status="rejected")

POST /orders/receive/{order_id}
Body: {"quantity_received": int, "received_date": "date", "notes": "string"}
Response: OrderDetail (status="received")

POST /orders/bulk-approve
Body: {"order_ids": ["uuid", ...]}
Response: {"approved": int, "failed": [{"order_id": "uuid", "reason": "string"}]}

POST /orders
Body: OrderCreate (creación manual, sin sugerencia del motor)
Response: OrderDetail

3.6 Proveedores

GET /suppliers
Response: PaginatedResponse[SupplierListItem]

GET /suppliers/{supplier_id}
Response: SupplierDetail (incluye SKUs asociados + historial de lead time)

POST /suppliers
Body: SupplierCreate
Response: SupplierDetail

PUT /suppliers/{supplier_id}
Body: SupplierUpdate
Response: SupplierDetail

3.7 Integraciones

GET /integrations/status
Response: {"alegra": {"connected": bool, "last_sync": "datetime"}, ...}

POST /integrations/alegra/connect
Body: {"api_key": "string", "api_email": "string"}
Response: {"status": "connected", "company_name": "string"}

POST /integrations/alegra/sync
Response: {"job_id": "uuid"}

DELETE /integrations/alegra/disconnect
Response: {"message": "Disconnected"}

3.8 Facturación y suscripción

GET /billing/subscription
Response: {"plan": "starter|growth|pro|enterprise", "status": "active|trial|past_due", "next_payment": "date"}

POST /billing/checkout
Body: {"plan": "string", "payment_method": "onvo_pay"}
Response: {"checkout_url": "string"}  // redirect a ONVO Pay

GET /billing/invoices
Response: PaginatedResponse[InvoiceListItem]

4. Workers Celery

4.1 forecast_worker — Motor estadístico

Tarea: tasks.forecast.run_forecast_pipeline

Trigger: - Automático: diario a las 3am CR (cron Celery Beat) - Manual: vía POST /forecast/run desde el frontend

Proceso: 1. Lee SKUs activos del tenant desde PostgreSQL 2. Llama al pipeline de 11 pasos (ver Doc 09 Motor Core) 3. Escribe resultados (forecast, policy parameters, explanation) en tabla sm_forecast_results 4. Genera sugerencias de OC (sm_order_suggestions) para SKUs en estado crítico/bajo 5. Publica notificaciones de alerta en Redis → API las lee en tiempo real vía polling

Configuración de colas:

CELERY_ROUTES = {
    'tasks.forecast.run_forecast_pipeline': {'queue': 'forecast'},
    'tasks.email.*': {'queue': 'email'},
    'tasks.integration.*': {'queue': 'integration'},
}
CELERY_WORKER_CONCURRENCY = {
    'forecast': 2,    # CPU-bound, no más de 2 en KVM2
    'email': 4,       # I/O-bound, puede ser más
    'integration': 2, # I/O-bound con rate limiting de Alegra
}

Timeout: forecast task timeout = 4h (para KVM2 con 100k SKUs). Si supera 90 min, emitir alerta interna.

4.2 email_worker — Emails transaccionales

Tareas: - tasks.email.send_magic_link — enviado en <5s del request - tasks.email.send_order_alert — alerta de OC urgente - tasks.email.send_weekly_summary — resumen semanal (V1.1)

Provider: Resend API. Retry automático 3 veces con backoff exponencial (1s, 4s, 16s).

4.3 integration_worker — Sync con sistemas externos

Tareas: - tasks.integration.sync_alegra — sincronización bidireccional con Alegra - Pull: productos, movimientos de inventario desde Alegra → StockMind - Push: ajustes de stock aprobados desde StockMind → Alegra (V1.1) - Rate limit Alegra: 120 requests/min → worker respeta con asyncio.sleep

Retry policy: 5 intentos, backoff exponencial, DLQ (dead letter queue) en Redis para fallos persistentes.


5. Multi-tenancy y aislamiento de datos

5.1 Estrategia: schema shared, tenant_id en cada tabla

Todas las tablas de negocio incluyen tenant_id UUID NOT NULL. No hay schemas separados por tenant (demasiado overhead de gestión en fase early).

Capa 1 — ORM: todos los queries incluyen WHERE tenant_id = :tenant_id automáticamente vía un TenantFilterMixin en SQLAlchemy.

Capa 2 — PostgreSQL RLS:

ALTER TABLE sm_skus ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON sm_skus
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

El app.tenant_id se establece al inicio de cada transacción desde el JWT. Si el ORM falla en filtrar, el RLS lo bloquea.

Capa 3 — Tests: tests de integración que verifican explícitamente que tenant A no puede leer datos de tenant B.

5.2 Extracción del tenant_id

async def get_current_tenant(token: str = Depends(oauth2_scheme)) -> UUID:
    payload = verify_jwt(token)
    return UUID(payload["tenant_id"])

El tenant_id nunca viene del request body — siempre del JWT. Cualquier tenant_id en el body se ignora.


6. Integración con Alegra

Alegra es el ERP más usado en CR/LATAM PyME. La integración es crítica para la adopción.

6.1 Flujo de autenticación Alegra

Alegra usa API Key + Email (no OAuth2). El usuario ingresa sus credenciales en /configuracion/integraciones. Las credenciales se almacenan cifradas (AES-256 con llave gestionada por Coolify Secrets) en la tabla sm_integration_credentials.

6.2 Datos que se sincronizan

Desde Alegra → StockMind (pull, cada 6h): - Productos (id, nombre, código, unidad de medida, precio costo, precio venta) - Movimientos de inventario (entradas, salidas, ajustes con fecha y cantidad) - Proveedores (nombre, contacto)

Desde StockMind → Alegra (push, cuando se aprueba una OC en StockMind): - Ajuste de inventario (cuando se marca OC como "recibida") — V1.1

6.3 Manejo de conflictos

Si un producto existe en Alegra con el mismo código que en StockMind pero con diferente nombre → StockMind gana (el inventario en StockMind es la fuente de verdad para el motor). El usuario recibe notificación del conflicto.


7. Integración ONVO Pay (facturación)

ONVO Pay es la pasarela de pagos primaria para CR. Tilopay como backup.

7.1 Flujo de pago (suscripción mensual)

1. Usuario elige plan en /configuracion/facturacion
2. StockMind llama ONVO Pay API: crear checkout session
3. Redirect usuario a ONVO Pay hosted checkout
4. ONVO Pay procesa el pago (tarjeta CR, SINPE, etc.)
5. Webhook ONVO Pay → POST /webhooks/onvo-pay/payment-success
6. StockMind activa/extiende suscripción en sm_subscriptions
7. Email de confirmación al usuario (vía Resend)

7.2 Webhook de ONVO Pay

El endpoint /webhooks/onvo-pay/payment-success verifica la firma HMAC del webhook antes de procesar. Cualquier request sin firma válida retorna 401 sin procesar.

def verify_onvo_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

7.3 Renovación automática

ONVO Pay gestiona la renovación mensual vía tokens guardados. StockMind recibe el webhook de éxito/fallo y actualiza el estado de la suscripción. Si el pago falla → estado past_due, acceso reducido (solo lectura), email de alerta.


8. Configuración de infraestructura (Coolify + Hostinger KVM2)

8.1 Servicios en el VPS

Servicio Puerto interno Expose externo Notas
FastAPI (Uvicorn) 8000 No (Nginx proxy) 4 workers Uvicorn
Celery forecast worker No 2 procesos
Celery email/integration worker No 4 procesos
Celery Beat (scheduler) No 1 proceso
Redis 6379 No Solo acceso interno
PostgreSQL 5432 No Solo acceso interno
Nginx (Coolify) 80/443 Reverse proxy + TLS

8.2 Variables de entorno requeridas

# Database
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/stockmind

# Redis
REDIS_URL=redis://localhost:6379/0

# JWT
JWT_SECRET_KEY=<256-bit random>
JWT_ALGORITHM=HS256
JWT_EXPIRE_DAYS=30

# Email
RESEND_API_KEY=<key>
FROM_EMAIL=noreply@stockmind.io

# Payments
ONVO_PAY_API_KEY=<key>
ONVO_PAY_WEBHOOK_SECRET=<secret>
TILOPAY_API_KEY=<key>  # backup

# Integrations
ALEGRA_ENCRYPTION_KEY=<32-byte key for AES-256>

# Monitoring
SENTRY_DSN=<dsn>

# App
ENVIRONMENT=production|staging|development
ALLOWED_ORIGINS=https://app.stockmind.io

Todas las variables se gestionan en Coolify Secrets (nunca en el repo).

8.3 Escalabilidad en Hostinger

Umbral Acción
>50 clientes activos Mover PostgreSQL a Hostinger Cloud DB o RDS (separar del VPS)
Forecast batch >90 min Mover forecast worker a VPS dedicado (KVM4 o instancia separada)
>200 clientes Upgrade VPS a KVM4 (4 vCPU, 16 GB) o evaluar multi-instancia
>500 clientes Revisar arquitectura completa con ops engineer dedicado

9. Logging y monitoreo

9.1 Logs estructurados (structlog)

Todos los logs en formato JSON para facilitar búsqueda en Coolify Logs:

{
  "timestamp": "2026-05-15T10:00:00Z",
  "level": "info",
  "event": "forecast_completed",
  "tenant_id": "uuid",
  "skus_processed": 847,
  "duration_seconds": 142.3,
  "methods_selected": {"CrostonTSB": 312, "SES": 198, ...}
}

No loguear nunca: contraseñas, API keys, tokens JWT, datos personales de clientes, ni cantidades de inventario específicas de un cliente (riesgo Ley 8968).

9.2 Sentry error tracking

9.3 Health checks

GET /health
Response: {"status": "ok", "db": "ok", "redis": "ok", "workers": {"forecast": "ok", "email": "ok"}}

GET /health/ready (liveness para Coolify)
Response: 200 OK si el servicio puede aceptar tráfico

10. Testing strategy

10.1 Cobertura objetivo

Capa Cobertura objetivo
Motor estadístico (pipeline completo) ≥95%
Lógica de negocio (services) ≥80%
API endpoints (integration tests) ≥75%
Workers Celery ≥70%
Utilidades y helpers ≥90%

10.2 Tipos de tests

Unit tests (pytest): cada función del motor de forma aislada. Fixtures con series temporales sintéticas conocidas (resultado esperado calculado a mano).

Integration tests (pytest + TestClient de FastAPI): cada endpoint con base de datos real (PostgreSQL en modo transacción, rollback después de cada test). Jamás mockear la base de datos para tests de lógica de negocio — riesgo documentado en wiki.

E2E tests (pytest + httpx): flujos completos (login → import → forecast run → approve order). Corren en staging antes de cada deploy a producción.

Property-based tests (Hypothesis): para el motor estadístico, verificar invariantes (ej: "el stock de seguridad nunca es negativo con inputs válidos", "el EOQ siempre es mayor al MOQ si el MOQ < demanda anual").


Ver también: Doc 06 (SRD Frontend — contratos de API que este backend expone) · Doc 08 (Modelo de Datos — schema SQL detallado) · Doc 09 (Motor Core — pipeline estadístico que este backend ejecuta) · Doc 10 (Seguridad — autenticación, cifrado, RLS) · wiki/04-stack-tecnico.md