Saltar al contenido principal

Integración Stripe

Integración completa con Stripe Connect para pagos Escrow seguros en Floutic.

Características

  • Pagos Escrow seguros - El dinero se mantiene en depósito hasta la aprobación
  • Stripe Connect - Transferencias automáticas a expertos
  • PaymentIntent inicial - Se crea al seleccionar experto
  • Sincronización automática - Estados pending → held → released
  • Liberación en cascada - Payout automatizado al aprobar hito
  • Gestión de comisiones - Comisiones automáticas para Floutic
  • Onboarding Express - Configuración rápida de cuentas Connect
  • Webhooks - Sincronización bidireccional con Stripe
  • Reembolsos automáticos - Notificación a experto y empresa al reembolsar
  • Seguimiento de transferencias - Transfer → Connect balance con reintento automático
  • Seguimiento de payouts - Connect balance → cuenta bancaria con notificaciones
  • Gestión de disputas - Chargebacks con sincronización automática de estados
  • Metadata de captura - Almacenamiento de amount_capturable en metadata del pago
  • API 2024-12-18.acacia - Lectura del campo latest_charge (renombrado desde charges) en todos los puntos de lectura del PaymentIntent
  • Validaciones de seguridad - Titularidad, importe, Connect activo y DAC7 verificados antes de transferir
  • Reversión de transferencias - Las transferencias liberadas se revierten antes de un reembolso

Configuración

1. Crear Cuenta en Stripe

  1. Crear cuenta en Stripe
  2. Activar Stripe Connect
  3. Configurar cuenta Express para expertos

2. Obtener API Keys

Modo Test:

  • STRIPE_SECRET_KEY=sk_test_...
  • STRIPE_PUBLISHABLE_KEY=pk_test_...
  • STRIPE_WEBHOOK_SECRET=whsec_...

Modo Live:

  • STRIPE_SECRET_KEY=sk_live_...
  • STRIPE_PUBLISHABLE_KEY=pk_live_...
  • STRIPE_WEBHOOK_SECRET=whsec_...

3. Configurar Webhooks

  1. Ir a Stripe Dashboard → Webhooks
  2. Añadir endpoint: https://api.haorp.es/api/payments/webhooks/stripe
  3. Seleccionar eventos:
    • payment_intent.succeeded
    • payment_intent.payment_failed
    • payment_intent.canceled
    • payment_intent.amount_capturable_updated
    • charge.refunded
    • charge.dispute.created
    • charge.dispute.updated
    • charge.dispute.closed
    • transfer.created
    • transfer.failed
    • payout.paid
    • payout.failed
    • account.updated
  4. Copiar el Signing Secret (whsec_...)

4. Variables de Entorno

# Stripe API Keys
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Stripe Connect
STRIPE_CONNECT_RETURN_URL=https://app.floutic.com/dashboard/experto
STRIPE_CONNECT_REFRESH_URL=https://app.floutic.com/dashboard/experto?retryStripeOnboarding=true
STRIPE_CONNECT_ACCOUNT_COUNTRY=ES

# Frontend
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
VITE_STRIPE_DASHBOARD_URL_BASE=https://dashboard.stripe.com/test/payments/

5. Versión de API y latest_charge

La integración usa la versión de API 2024-12-18.acacia. En esta versión Stripe renombró el campo charges (lista expandida) por latest_charge (string con el ID del cargo).

  • Todos los puntos de lectura del PaymentIntent usan latest_charge directamente.
  • El helper get_attr(payment_intent, "latest_charge") se usa para tolerar versiones anteriores sin romper.
  • Al actualizar la versión de API en el dashboard de Stripe, verifica que los mocks de tests y los fixtures usen latest_charge en vez de charges.data[0].
  • Moneda: todas las llamadas a Stripe normalizan la moneda a minúsculas (currency.lower()), ya que la API exige códigos ISO 4217 en minúsculas (ej. eur). El valor por defecto es eur.

Flujo de Pagos

1. Selección de Experto

Cuando una empresa selecciona un experto:

  • Se crea automáticamente el PaymentIntent del primer hito
  • Estado inicial: requires_payment_method
  • El pago se muestra en la ficha del proyecto

2. Autorización del Pago

La empresa autoriza el pago:

  • El frontend sincroniza el PaymentIntent (/api/payments/sync-status)
  • Estado cambia a requires_capture
  • El pago se marca como held en Floutic
  • Se guarda held_at y stripe_charge_id
  • Notificación: Se envía notificación payment_held al experto confirmando que los fondos están seguros.

3. Aprobación de Hito

Cuando la empresa aprueba un hito:

  • Se captura el pago retenido (/api/payments/release-and-charge-next)
  • Se transfiere el neto al experto vía Stripe Connect
  • Se genera automáticamente el PaymentIntent del siguiente hito
  • Notificación: Se envía notificación milestone_awaiting_payment al experto para el nuevo hito.
  • El ciclo continúa hasta completar todos los hitos

4. Estados del Pago

  • pending - PaymentIntent creado, esperando autorización
  • held - Pago autorizado, en depósito
  • released - Pago capturado por la plataforma (transferencia al experto puede ser diferida)
  • refunded - Pago reembolsado

Stripe Connect

Onboarding de Expertos

Los expertos deben completar el onboarding de Stripe Connect para recibir pagos:

  1. Verificar estado: GET /api/stripe-connect/status
  2. Generar enlace: POST /api/stripe-connect/account-link
  3. Completar onboarding en Stripe
  4. Verificar estado nuevamente

Actualización de Cuenta

Si la cuenta Connect necesita actualización:

  1. Generar enlace de actualización: POST /api/stripe-connect/account-update-link
  2. Completar actualización en Stripe
  3. Verificar estado nuevamente

Estado de Connect requerido para transferencias

Toda transferencia a un experto exige que su cuenta Connect esté activa. El campo stripe_connect_status debe ser "active":

Ruta de transferenciaValidaciónError si no está activa
initiate_paymentstripe_connect_status == "active"STRIPE_CONNECT_NOT_ACTIVE
release_and_charge_next (_handle_approval)stripe_connect_status == "active"STRIPE_CONNECT_NOT_ACTIVE
retry_failed_transferstripe_connect_status == "active"STRIPE_CONNECT_NOT_ACTIVE

:::warning Cuentas restringidas Si Stripe restringe o pausa una cuenta Connect (KYC pendiente, información desactualizada), las transferencias se rechazan en lugar de enviarse a una cuenta donde los fondos quedarían bloqueados. El experto debe completar la actualización desde POST /api/stripe-connect/account-update-link. :::

Endpoints de API

Pagos

  • POST /api/payments/initiate - Crear PaymentIntent
  • POST /api/payments/sync-status - Sincronizar estado con Stripe
  • POST /api/payments/release - Liberar pago retenido
  • POST /api/payments/release-and-charge-next - Liberar y crear siguiente
  • POST /api/payments/refund - Crear reembolso
  • GET /api/payments/project/{project_id} - Pagos de proyecto
  • GET /api/payments/history - Historial de pagos

Stripe Connect

  • GET /api/stripe-connect/status - Estado de cuenta Connect
  • POST /api/stripe-connect/account-link - Generar enlace de onboarding
  • POST /api/stripe-connect/account-update-link - Generar enlace de actualización

Webhooks

Stripe envía webhooks a:

  • POST /api/payments/webhooks/stripe

Eventos procesados:

EventoDescripciónComportamiento
payment_intent.succeededPago completadoMarca pago como held (escrow activo), actualiza hito a pending
payment_intent.payment_failedPago fallidoMarca pago como failed, hito permanece en awaiting_payment
payment_intent.canceledPago canceladoMarca pago como failed, mantiene hito en awaiting_payment
payment_intent.amount_capturable_updatedMonto capturable actualizadoAlmacena amount_capturable en metadata del pago
charge.refundedCargo reembolsadoMarca pago como refunded, envía notificación y email a experto y empresa
transfer.createdTransferencia creadaActualiza payout_status a transferred, marca pago como released
transfer.failedTransferencia fallidaRevierte hito (approved→in_progress) y entrega (approved→submitted), establece payout_status a failed, limpia stripe_transfer_id, recalcula expert_amount y alerta a admins sobre hitos siguientes huérfanos
payout.paidPayout completadoBusca Profile por stripe_connect_account_id, envía notificación EXPERT_PAYOUT_PAID al experto
payout.failedPayout fallidoEnvía notificaciones EXPERT_PAYOUT_FAILED al experto y al admin
charge.dispute.createdDisputa creadaCrea registro Dispute con stripe_dispute_id, marca pago como disputed
charge.dispute.updatedDisputa actualizadaSincroniza estado de Stripe a local (under_reviewin_review, needs_responseopen)
charge.dispute.closedDisputa cerradaSi lost → pago reembolsado. Si won → pago permanece disputed (admin decide)
account.updatedCuenta actualizadaSincroniza estado de cuenta Connect del experto

Flujo de Transferencias

Las transferencias mueven dinero desde la cuenta de la plataforma (Floutic) hacia el saldo de Stripe Connect del experto.

¿Cómo funcionan?

  1. Cuando se aprueba un hito, se captura el pago retenido en escrow.
  2. Se calcula la comisión de Floutic según el nivel del experto (expert_amount = amount − commission_amount).
  3. Validaciones previas: se exige stripe_connect_status == "active", has_complete_fiscal_data() (DAC7) y expert_amount > 0. Si alguna falla, la transferencia se rechaza y el pago queda released para intervención.
  4. Se intenta transferir el monto neto (expert_amount) vía stripe.Transfer a la cuenta Connect del experto, usando una clave de idempotencia transfer_{payment.id}_{timestamp} (el sufijo de timestamp evita que Stripe cachee un error y envenene los reintentos siguientes dentro de las ~24h de ventana).
  5. Si el balance de la plataforma es insuficiente (fondos pendientes de settlement en Stripe LIVE): la transferencia se difiere (payout_status=pending) y un scheduler diario reintenta automáticamente.
  6. Cuando la transferencia se procesa, se registra el stripe_transfer_id en el modelo Payment.

Handlers de Transferencias

  • transfer.created: Se ejecuta cuando Stripe confirma la transferencia. Actualiza payout_status a transferred y marca el estado del pago como released.
  • transfer.failed: Se ejecuta cuando la transferencia falla asíncronamente. Revierte el hito (approved → in_progress) y la última entrega (approved → submitted), recalcula expert_amount (que el revert nuleó) desde payment.amount + CommissionService, establece payout_status a failed y limpia el stripe_transfer_id para permitir un reintento. Si la aprobación había creado un hito siguiente en awaiting_payment, alerta al admin con su título para que lo resuelva. Envía una notificación legible (no el código crudo de Stripe).

Ciclo de vida de payout_status en el modelo Payment

EstadoDescripción
pendingTransferencia pendiente (balance insuficiente, scheduler activo)
transferredTransferencia completada exitosamente (dinero en saldo Connect)
failedTransferencia fallida tras reintentos, requiere intervención

[!NOTE] Las transferencias mueven dinero de la plataforma al saldo Connect del experto. No confundir con los payouts, que mueven dinero del saldo Connect a la cuenta bancaria del experto. En Stripe LIVE, los fondos capturados tardan ~5 días hábiles en setearse; si el balance es insuficiente al momento de la aprobación, la transferencia se difiere y el scheduler reintenta diariamente.

Reversión de transferencias al reembolsar

Cuando se reembolsa un pago que ya estaba liberado y transferido al experto, el sistema revierte la transferencia antes de emitir el reembolso. Esto evita que la plataforma absorba el dinero del experto.

  • Se invoca stripe_service.create_transfer_reversal() sobre el stripe_transfer_id original.
  • La reversión es proporcional si el reembolso es parcial (reversal = expert_amount × (refund_amount / payment_amount)), o total si el reembolso es completo.
  • Si la reversión falla, el reembolso no se procesa (se prefiere bloquear antes que generar una pérdida financiera).
  • El ID y el monto de la reversión se guardan en payment_metadata para auditoría.

Flujo de Payouts (Connect → Banco)

Los payouts mueven dinero desde el saldo de Stripe Connect del experto hacia su cuenta bancaria registrada.

[!IMPORTANT] Los payouts son a nivel de cuenta (account-level), NO por cada Payment. Un solo payout puede agregar múltiples transferencias. El sistema encuentra el Profile del experto mediante su stripe_connect_account_id.

¿Cómo funcionan?

  1. Stripe agrupa automáticamente las transferencias completadas en el saldo Connect.
  2. Según la configuración de payout del experto (diario, semanal, mensual), Stripe inicia un payout al banco.
  3. Floutic recibe un webhook de payout.paid o payout.failed.

Handlers de Payouts

  • payout.paid: Se ejecuta cuando el payout se completa exitosamente. Busca el Profile del experto por stripe_connect_account_id y envía una notificación EXPERT_PAYOUT_PAID al experto.
  • payout.failed: Se ejecuta cuando el payout falla. Envía notificaciones EXPERT_PAYOUT_FAILED tanto al experto como al administrador para intervención manual.

Nuevos tipos de notificación

TipoDestinatarioDescripción
EXPERT_PAYOUT_PAIDExpertoNotificación de payout completado
EXPERT_PAYOUT_FAILEDExperto + AdminAlerta de payout fallido

Flujo de Disputas (Chargebacks)

Las disputas ocurren cuando un titular de tarjeta disputa un cargo directamente a través de su banco (chargeback).

¿Cómo funcionan las disputas?

  1. El titular de la tarjeta contacta a su banco y disputa el cargo.
  2. Stripe notifica a Floutic vía webhook charge.dispute.created.
  3. Se crea un registro Dispute en la base de datos con el stripe_dispute_id.
  4. El estado del pago asociado cambia a disputed.
  5. Durante el proceso, Stripe envía actualizaciones vía charge.dispute.updated.
  6. Finalmente, Stripe notifica el resultado vía charge.dispute.closed.

Handlers de Disputas

  • charge.dispute.created: Crea un registro Dispute vinculado al Payment, almacena el stripe_dispute_id y marca el payment.status = "disputed".

  • charge.dispute.updated: Sincroniza el estado de la disputa de Stripe al estado local:

    • under_reviewin_review
    • needs_responseopen
  • charge.dispute.closed: Resuelve la disputa según el resultado:

    • Perdida (lost): El pago se marca como refunded (el dinero vuelve al cliente).
    • Ganada (won): El pago permanece en estado disputed para que el administrador decida si liberar al experto (release_to_expert) o reembolsar a la empresa (refund_company).

Modelo Dispute

El modelo Dispute se vincula al modelo Payment mediante una clave foránea (payment_id) e incluye:

  • stripe_dispute_id: Identificador único de la disputa en Stripe (String 100, nullable, unique, indexed)
  • status: Estado actual de la disputa
  • reason: Motivo de la disputa reportado por Stripe
  • evidence_details: Detalles de evidencia requerida

Migraciones de base de datos

Las siguientes migraciones de Alembic están asociadas a las disputas:

  • e5f6a7b8c9d0 — Campo stripe_dispute_id en modelo Dispute

Comisiones

Floutic cobra una comisión automática en cada transferencia:

  • Configurable desde el panel de administración
  • Se calcula automáticamente en cada payout
  • Se resta del monto total antes de transferir al experto

Seguridad

  • Validación de firmas - Todos los webhooks se validan con HMAC
  • HTTPS obligatorio - Todas las comunicaciones son seguras
  • Tokens seguros - Los PaymentIntents usan tokens únicos
  • Validación de permisos - Solo empresa/admin pueden ver enlaces a Stripe

Troubleshooting

El pago no se sincroniza

  1. Verificar que el webhook esté configurado correctamente
  2. Revisar logs de Stripe Dashboard
  3. Verificar STRIPE_WEBHOOK_SECRET en variables de entorno
  4. Usar POST /api/payments/sync-status para sincronización manual

El experto no recibe pagos

  1. Verificar que tenga cuenta Connect configurada
  2. Verificar estado con GET /api/stripe-connect/status
  3. Completar onboarding si es necesario
  4. Verificar que charges_enabled y payouts_enabled sean true

Error en el onboarding

  1. Verificar STRIPE_CONNECT_RETURN_URL y STRIPE_CONNECT_REFRESH_URL
  2. Verificar que las URLs sean HTTPS
  3. Regenerar enlace si es necesario
  4. Verificar que el país esté configurado (STRIPE_CONNECT_ACCOUNT_COUNTRY)

Recibí una disputa

[!WARNING] Las disputas son sensibles al tiempo. Stripe da un plazo limitado para responder.

Cuando se recibe una disputa:

  1. El webhook charge.dispute.created crea automáticamente un registro Dispute en la base de datos.
  2. El pago asociado se marca como disputed automáticamente.
  3. Se almacena el stripe_dispute_id para seguimiento.
  4. A medida que Stripe envía actualizaciones, el estado se sincroniza automáticamente.
  5. Al cerrarse la disputa, el sistema actúa según el resultado:
    • Si se pierde: el pago se reembolsa automáticamente.
    • Si se gana: el administrador debe decidir manualmente la resolución.

Transferencia fallida

  1. El webhook transfer.failed marca el payout_status como failed.
  2. Se revierte el hito (approved → in_progress) y la entrega (approved → submitted) para que la empresa pueda reaprobar.
  3. Se recalcula expert_amount (nuleado por el revert) desde payment.amount + CommissionService.
  4. Se limpia el stripe_transfer_id del pago para permitir un reintento.
  5. Se envía una notificación legible al administrador (y una alerta si quedó un hito siguiente huérfano).
  6. El administrador puede reintentar la transferencia manualmente desde el panel vía POST /api/payments/retry-transfer/{payment_id}, que acepta estados held+failed y released+pending.

[!TIP] Las transferencias fallidas suelen deberse a problemas con la cuenta Connect del experto (verificar estado con GET /api/stripe-connect/status).

Payout fallido

  1. El webhook payout.failed envía notificaciones al experto y al administrador.
  2. El administrado debe verificar la cuenta bancaria del experto en Stripe.
  3. El experto puede actualizar su información bancaria desde su dashboard de Stripe Connect.
  4. Una vez corregido, Stripe reintenta automáticamente el payout según su programación.

[!NOTE] Los payouts fallidos requieren intervención manual. Verificar que los datos bancarios del experto estén correctos.

Documentación Completa

Para documentación detallada, consulta:

Referencias