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_capturableen metadata del pago - ✅ API
2024-12-18.acacia- Lectura del campolatest_charge(renombrado desdecharges) 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
- Crear cuenta en Stripe
- Activar Stripe Connect
- 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
- Ir a Stripe Dashboard → Webhooks
- Añadir endpoint:
https://api.haorp.es/api/payments/webhooks/stripe - Seleccionar eventos:
payment_intent.succeededpayment_intent.payment_failedpayment_intent.canceledpayment_intent.amount_capturable_updatedcharge.refundedcharge.dispute.createdcharge.dispute.updatedcharge.dispute.closedtransfer.createdtransfer.failedpayout.paidpayout.failedaccount.updated
- 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_chargedirectamente. - 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_chargeen vez decharges.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 eseur.
Flujo de Pagos
1. Selección de Experto
Cuando una empresa selecciona un experto:
- Se crea automáticamente el
PaymentIntentdel 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
helden Floutic - Se guarda
held_atystripe_charge_id - Notificación: Se envía notificación
payment_heldal 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_paymental experto para el nuevo hito. - El ciclo continúa hasta completar todos los hitos
4. Estados del Pago
pending- PaymentIntent creado, esperando autorizaciónheld- Pago autorizado, en depósitoreleased- 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:
- Verificar estado:
GET /api/stripe-connect/status - Generar enlace:
POST /api/stripe-connect/account-link - Completar onboarding en Stripe
- Verificar estado nuevamente
Actualización de Cuenta
Si la cuenta Connect necesita actualización:
- Generar enlace de actualización:
POST /api/stripe-connect/account-update-link - Completar actualización en Stripe
- 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 transferencia | Validación | Error si no está activa |
|---|---|---|
initiate_payment | stripe_connect_status == "active" | STRIPE_CONNECT_NOT_ACTIVE |
release_and_charge_next (_handle_approval) | stripe_connect_status == "active" | STRIPE_CONNECT_NOT_ACTIVE |
retry_failed_transfer | stripe_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 PaymentIntentPOST /api/payments/sync-status- Sincronizar estado con StripePOST /api/payments/release- Liberar pago retenidoPOST /api/payments/release-and-charge-next- Liberar y crear siguientePOST /api/payments/refund- Crear reembolsoGET /api/payments/project/{project_id}- Pagos de proyectoGET /api/payments/history- Historial de pagos
Stripe Connect
GET /api/stripe-connect/status- Estado de cuenta ConnectPOST /api/stripe-connect/account-link- Generar enlace de onboardingPOST /api/stripe-connect/account-update-link- Generar enlace de actualización
Webhooks
Stripe envía webhooks a:
POST /api/payments/webhooks/stripe
Eventos procesados:
| Evento | Descripción | Comportamiento |
|---|---|---|
payment_intent.succeeded | Pago completado | Marca pago como held (escrow activo), actualiza hito a pending |
payment_intent.payment_failed | Pago fallido | Marca pago como failed, hito permanece en awaiting_payment |
payment_intent.canceled | Pago cancelado | Marca pago como failed, mantiene hito en awaiting_payment |
payment_intent.amount_capturable_updated | Monto capturable actualizado | Almacena amount_capturable en metadata del pago |
charge.refunded | Cargo reembolsado | Marca pago como refunded, envía notificación y email a experto y empresa |
transfer.created | Transferencia creada | Actualiza payout_status a transferred, marca pago como released |
transfer.failed | Transferencia fallida | Revierte 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.paid | Payout completado | Busca Profile por stripe_connect_account_id, envía notificación EXPERT_PAYOUT_PAID al experto |
payout.failed | Payout fallido | Envía notificaciones EXPERT_PAYOUT_FAILED al experto y al admin |
charge.dispute.created | Disputa creada | Crea registro Dispute con stripe_dispute_id, marca pago como disputed |
charge.dispute.updated | Disputa actualizada | Sincroniza estado de Stripe a local (under_review → in_review, needs_response → open) |
charge.dispute.closed | Disputa cerrada | Si lost → pago reembolsado. Si won → pago permanece disputed (admin decide) |
account.updated | Cuenta actualizada | Sincroniza 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?
- Cuando se aprueba un hito, se captura el pago retenido en escrow.
- Se calcula la comisión de Floutic según el nivel del experto (
expert_amount = amount − commission_amount). - Validaciones previas: se exige
stripe_connect_status == "active",has_complete_fiscal_data()(DAC7) yexpert_amount > 0. Si alguna falla, la transferencia se rechaza y el pago quedareleasedpara intervención. - Se intenta transferir el monto neto (
expert_amount) víastripe.Transfera la cuenta Connect del experto, usando una clave de idempotenciatransfer_{payment.id}_{timestamp}(el sufijo de timestamp evita que Stripe cachee un error y envenene los reintentos siguientes dentro de las ~24h de ventana). - 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. - Cuando la transferencia se procesa, se registra el
stripe_transfer_iden el modelo Payment.
Handlers de Transferencias
transfer.created: Se ejecuta cuando Stripe confirma la transferencia. Actualizapayout_statusatransferredy marca el estado del pago comoreleased.transfer.failed: Se ejecuta cuando la transferencia falla asíncronamente. Revierte el hito (approved → in_progress) y la última entrega (approved → submitted), recalculaexpert_amount(que el revert nuleó) desdepayment.amount+CommissionService, establecepayout_statusafailedy limpia elstripe_transfer_idpara permitir un reintento. Si la aprobación había creado un hito siguiente enawaiting_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
| Estado | Descripción |
|---|---|
pending | Transferencia pendiente (balance insuficiente, scheduler activo) |
transferred | Transferencia completada exitosamente (dinero en saldo Connect) |
failed | Transferencia 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 elstripe_transfer_idoriginal. - 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_metadatapara 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?
- Stripe agrupa automáticamente las transferencias completadas en el saldo Connect.
- Según la configuración de payout del experto (diario, semanal, mensual), Stripe inicia un payout al banco.
- Floutic recibe un webhook de
payout.paidopayout.failed.
Handlers de Payouts
payout.paid: Se ejecuta cuando el payout se completa exitosamente. Busca el Profile del experto porstripe_connect_account_idy envía una notificaciónEXPERT_PAYOUT_PAIDal experto.payout.failed: Se ejecuta cuando el payout falla. Envía notificacionesEXPERT_PAYOUT_FAILEDtanto al experto como al administrador para intervención manual.
Nuevos tipos de notificación
| Tipo | Destinatario | Descripción |
|---|---|---|
EXPERT_PAYOUT_PAID | Experto | Notificación de payout completado |
EXPERT_PAYOUT_FAILED | Experto + Admin | Alerta 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?
- El titular de la tarjeta contacta a su banco y disputa el cargo.
- Stripe notifica a Floutic vía webhook
charge.dispute.created. - Se crea un registro
Disputeen la base de datos con elstripe_dispute_id. - El estado del pago asociado cambia a
disputed. - Durante el proceso, Stripe envía actualizaciones vía
charge.dispute.updated. - Finalmente, Stripe notifica el resultado vía
charge.dispute.closed.
Handlers de Disputas
-
charge.dispute.created: Crea un registroDisputevinculado al Payment, almacena elstripe_dispute_idy marca elpayment.status = "disputed". -
charge.dispute.updated: Sincroniza el estado de la disputa de Stripe al estado local:under_review→in_reviewneeds_response→open
-
charge.dispute.closed: Resuelve la disputa según el resultado:- Perdida (
lost): El pago se marca comorefunded(el dinero vuelve al cliente). - Ganada (
won): El pago permanece en estadodisputedpara que el administrador decida si liberar al experto (release_to_expert) o reembolsar a la empresa (refund_company).
- Perdida (
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 disputareason: Motivo de la disputa reportado por Stripeevidence_details: Detalles de evidencia requerida
Migraciones de base de datos
Las siguientes migraciones de Alembic están asociadas a las disputas:
e5f6a7b8c9d0— Campostripe_dispute_iden 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
- Verificar que el webhook esté configurado correctamente
- Revisar logs de Stripe Dashboard
- Verificar
STRIPE_WEBHOOK_SECRETen variables de entorno - Usar
POST /api/payments/sync-statuspara sincronización manual
El experto no recibe pagos
- Verificar que tenga cuenta Connect configurada
- Verificar estado con
GET /api/stripe-connect/status - Completar onboarding si es necesario
- Verificar que
charges_enabledypayouts_enabledseantrue
Error en el onboarding
- Verificar
STRIPE_CONNECT_RETURN_URLySTRIPE_CONNECT_REFRESH_URL - Verificar que las URLs sean HTTPS
- Regenerar enlace si es necesario
- 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:
- El webhook
charge.dispute.createdcrea automáticamente un registroDisputeen la base de datos. - El pago asociado se marca como
disputedautomáticamente. - Se almacena el
stripe_dispute_idpara seguimiento. - A medida que Stripe envía actualizaciones, el estado se sincroniza automáticamente.
- 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
- El webhook
transfer.failedmarca elpayout_statuscomofailed. - Se revierte el hito (
approved → in_progress) y la entrega (approved → submitted) para que la empresa pueda reaprobar. - Se recalcula
expert_amount(nuleado por el revert) desdepayment.amount+CommissionService. - Se limpia el
stripe_transfer_iddel pago para permitir un reintento. - Se envía una notificación legible al administrador (y una alerta si quedó un hito siguiente huérfano).
- El administrador puede reintentar la transferencia manualmente desde el panel vía
POST /api/payments/retry-transfer/{payment_id}, que acepta estadosheld+failedyreleased+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
- El webhook
payout.failedenvía notificaciones al experto y al administrador. - El administrado debe verificar la cuenta bancaria del experto en Stripe.
- El experto puede actualizar su información bancaria desde su dashboard de Stripe Connect.
- 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:
- Stripe Implementation Review - Auditoría completa
- Stripe Setup - Guía de configuración paso a paso
- API Endpoints - Pagos - Documentación de endpoints