Guía de integración · Frontend

Suscripción de un edificio a un plan

Secuencia exacta de peticiones entre frontend, Winter CMS, Stripe y Odoo, incluyendo planes gratuitos, checkout de pago, estados asíncronos y recuperación de errores.

01No hay Checkout Session

Stripe Elements confirma un client_secret.

02Fiscalidad antes del pago

Winter y Odoo son la fuente canónica.

03Activación asíncrona

El webhook decide el estado definitivo.

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/json
PRECONDICIÓN

El 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.

#VerboEndpointPropósitoUso
1GET/buildings/{buildingUuid}?include=subscriptionConfirma acceso, rol y suscripción actual.Auth
2GET/buildings/{buildingUuid}/featuresComprueba subscription_self_service.Auth
3GET/subscription-typesCarga el catálogo público de planes.Público
4GET/companies?include=buildingsLocaliza la compañía fiscal del edificio.Auth
5GET/companies/{companyUuid}/billing-profileValida el perfil fiscal antes del pago.Auth
6PATCH/companies/{companyUuid}/billing-profileCompleta y sincroniza datos fiscales con Odoo.Condicional
7POST/subscriptionsCrea o actualiza la suscripción y prepara el pago.Auth
8SDKstripe.confirmPayment()Confirma el PaymentIntent en Stripe.js.Condicional
9POST/stripe/webhookStripe notifica el resultado a Winter.Stripe
10GET/subscriptions/{subscriptionUuid}Consulta el estado final hasta que se estabilice.Auth

03 · COMPAÑÍA FISCAL

Resolverla antes del plan de pago

Los planes gratuitos omiten por completo esta sección.

A · YA ASOCIADA

Revisar y completar

GET /companies/{uuid}/billing-profile

PATCH /companies/{uuid}/billing-profile

B · EXISTE OTRA ACCESIBLE

Enlazar durante el alta

Enviar company_uuid en POST /subscriptions.

C · NO EXISTE

Crear y asociar

POST /companies sincroniza también el partner con Odoo.

Campos mínimos para cobrar

namecountryvat_numberemail_invoice o email
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
}
PLAN GRATUITO

Activo inmediatamente

No hay Stripe, Payment Element ni webhook que esperar.

PAGO INMEDIATO

Confirmación pendiente

La respuesta incluye payment_intent.client_secret.

SIN COBRO AHORA

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"
  }
}
IMPORTANTE

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_metadata
Operativa

status=active y Stripe en succeeded, active, trialing, paid o not_required.

Pendiente

status=inactive y payment_required; seguir esperando o confirmar.

Fallida

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=20

07 · CAMBIOS Y CANCELACIÓN

Ciclo de vida de una suscripción existente

CAMBIAR PLAN

PATCH /subscriptions/{uuid}

{
  "subscription_type_uuid": "NEW_PLAN_UUID",
  "auto_renew": true
}
CANCELAR

POST /subscriptions/{uuid}/cancel

{
  "cancel_at_period_end": true
}
DOWNGRADE A FREE

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

400

UUID incorrecto o no se pudo resolver un plan válido.

401

Falta autenticación o el token no es válido.

403

El usuario no es admin/super_admin o falta subscription_self_service.

404

Edificio, compañía o suscripción inexistente o fuera del perímetro del usuario.

422

Perfil fiscal incompleto, compañía incompatible, validación o downgrade no permitido.

503

Stripe no está configurado para planes de pago.

500

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.