01 · CONTRATO DE RED
Qué debe enviar el frontend
Todas las rutas salvo GET /subscription-types requieren autenticación. El webhook lo invoca Stripe, nunca el navegador.
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/jsonEl usuario debe ser admin o super_admin y el edificio debe tener la feature subscription_self_service.
02 · FLUJO COMPLETO
Endpoint a endpoint
Las filas marcadas como condicionales solo se ejecutan cuando el caso lo requiere.
/buildings/{buildingUuid}?include=subscriptionConfirma acceso, rol y suscripción actual.Auth/buildings/{buildingUuid}/featuresComprueba subscription_self_service.Auth/subscription-typesCarga el catálogo público de planes.Público/companies?include=buildingsLocaliza la compañía fiscal del edificio.Auth/companies/{companyUuid}/billing-profileValida el perfil fiscal antes del pago.Auth/companies/{companyUuid}/billing-profileCompleta y sincroniza datos fiscales con Odoo.Condicional/subscriptionsCrea o actualiza la suscripción y prepara el pago.Authstripe.confirmPayment()Confirma el PaymentIntent en Stripe.js.Condicional/stripe/webhookStripe notifica el resultado a Winter.Stripe/subscriptions/{subscriptionUuid}Consulta el estado final hasta que se estabilice.Auth03 · COMPAÑÍA FISCAL
Resolverla antes del plan de pago
Los planes gratuitos omiten por completo esta sección.
Revisar y completar
GET /companies/{uuid}/billing-profile
PATCH /companies/{uuid}/billing-profile
Enlazar durante el alta
Enviar company_uuid en POST /subscriptions.
Crear y asociar
POST /companies sincroniza también el partner con Odoo.
Campos mínimos para cobrar
POST /companies
{
"building_uuid": "BUILDING_UUID",
"name": "Hotel Example SL",
"country": "ES",
"vat_number": "B12345678",
"email_invoice": "facturas@example.com",
"address": "Calle Example 1",
"city": "Palma",
"zip": "07001"
}04 · ALTA Y CHECKOUT
Una sola entrada para crear o actualizar
POST /subscriptions
{
"building_uuid": "BUILDING_UUID",
"company_uuid": "COMPANY_UUID",
"subscription_type_uuid": "PLAN_UUID",
"auto_renew": true,
"trial_days": 0
}Activo inmediatamente
No hay Stripe, Payment Element ni webhook que esperar.
Confirmación pendiente
La respuesta incluye payment_intent.client_secret.
Checkout no requerido
zero_amount, already_active o no_immediate_payment_required.
{
"data": {
"uuid": "SUBSCRIPTION_UUID",
"type": "paid",
"status": "inactive"
},
"payment_intent": {
"client_secret": "pi_xxx_secret_xxx",
"amount": 4800,
"currency": "eur"
},
"checkout": {
"required": true,
"status": "pending_confirmation",
"reason": "initial_payment"
}
}Guardar inmediatamente data.uuid y el client_secret. No repetir el POST a ciegas si se pierde la respuesta.
05 · DIAGRAMA DE PETICIONES
Del edificio al estado operativo
Ver código Mermaid
flowchart TD
START["Frontend entra en el edificio"] --> ACCESS["GET building + features"]
ACCESS --> ALLOWED{"¿admin y self-service?"}
ALLOWED -- "No" --> STOP["403 · detener flujo"]
ALLOWED -- "Sí" --> PLANS["GET /subscription-types"]
PLANS --> MODE{"Plan elegido"}
MODE -- "Free" --> FREE["POST /subscriptions"]
FREE --> ACTIVE["Suscripción activa"]
MODE -- "Paid" --> COMPANY{"¿Compañía fiscal?"}
COMPANY -- "No existe" --> CREATE["POST /companies"]
COMPANY -- "Existe" --> PROFILE["GET/PATCH billing-profile"]
CREATE --> SUBSCRIBE["POST /subscriptions"]
PROFILE --> SUBSCRIBE
SUBSCRIBE --> CHECKOUT{"checkout.required"}
CHECKOUT -- "false" --> POLL["GET /subscriptions/{uuid}"]
CHECKOUT -- "true" --> STRIPE["stripe.confirmPayment()"]
STRIPE --> WEBHOOK["Stripe → POST /stripe/webhook"]
WEBHOOK --> SYNC["Winter + Odoo"]
SYNC --> POLL
POLL --> STATUS{"Estado"}
STATUS -- "pending" --> POLL
STATUS -- "failed" --> RETRY["Corregir y reintentar"]
RETRY --> STRIPE
STATUS -- "active" --> DONE["Operativa · consultar facturas"]06 · ESTADOS Y POLLING
Stripe confirma; el webhook activa
const result = await stripe.confirmPayment({
elements,
redirect: "if_required",
});Después de Stripe, consultar cada 1–2 segundos durante un tiempo acotado:
GET /subscriptions/{subscriptionUuid}
?fields=stripe_subscription_status,stripe_payment_method_id,sync_state,billing_metadatastatus=active y Stripe en succeeded, active, trialing, paid o not_required.
status=inactive y payment_required; seguir esperando o confirmar.
payment_failed; corregir el pago y reintentar el mismo PaymentIntent.
Las facturas son posteriores y no deben bloquear la pantalla de éxito:
GET /subscriptions/{subscriptionUuid}/invoice
GET /subscriptions/{subscriptionUuid}/invoices?limit=2007 · CAMBIOS Y CANCELACIÓN
Ciclo de vida de una suscripción existente
PATCH /subscriptions/{uuid}
{
"subscription_type_uuid": "NEW_PLAN_UUID",
"auto_renew": true
}POST /subscriptions/{uuid}/cancel
{
"cancel_at_period_end": true
}Un plan paid activo no puede pasar directamente a free. Hay que cancelar, esperar al fin del periodo —o cancelar inmediatamente— y después asignar el plan gratuito.
08 · MATRIZ DE ERRORES
Qué debe hacer la interfaz
UUID incorrecto o no se pudo resolver un plan válido.
Falta autenticación o el token no es válido.
El usuario no es admin/super_admin o falta subscription_self_service.
Edificio, compañía o suscripción inexistente o fuera del perímetro del usuario.
Perfil fiscal incompleto, compañía incompatible, validación o downgrade no permitido.
Stripe no está configurado para planes de pago.
Fallo interno o de una integración aguas abajo.
09 · LÍMITES ACTUALES
Decisiones que el frontend debe conocer
- No hay Checkout Session: el contrato actual es PaymentIntent + Stripe Elements.
- El client_secret no se recupera por GET: conservarlo en memoria durante el checkout.
- No hay idempotency key del frontend: no reintentar POST /subscriptions automáticamente tras un timeout ambiguo.
- PATCH de planes no canónicos: puede generar un client_secret interno sin exponerlo; no ofrecer ese cambio en autoservicio hasta corregirlo.
- Bootstrap de features: con enforcement estricto, el edificio necesita plan inicial u override para poder autosuscribirse.