Payments (Gestión Financiera)
El módulo de Payments gestiona toda la capa financiera de Yastubo. No solo procesa pagos unitarios y suscripciones, sino que también orquestra la distribución de comisiones a los revendedores (Vendedores) a través de Stripe Connect.
Key Features
Section titled “Key Features”- Pagos Unitarios y Suscripciones: Soporte para cobros de pólizas anuales (un solo pago) o mensuales (recurrente).
- Stripe Connect (Custom/Express): Onboarding automático para vendedores, permitiendo depósitos directos de comisiones.
- Gestión de Comisiones: Cálculo automático de “Application Fees” que la plataforma retiene antes de enviar el saldo al vendedor.
- Webhook Handler: Sistema robusto de escucha de eventos de Stripe para actualizar el estado de las pólizas y suscripciones en tiempo real.
- Reintentos de Cobro Inteligentes: Motor de reintentos con límite de 2 intentos y notificación automática al cliente vía email + WhatsApp al alcanzar el límite.
Deep Dive Técnico
Section titled “Deep Dive Técnico”Flujo de Suscripción
Section titled “Flujo de Suscripción”Cuando un cliente opta por un plan mensual:
- Customer Creation: El sistema verifica si el usuario ya existe en Stripe; si no, lo crea.
- Payment Method: Se vincula el método de pago proporcionado.
- Subscription Link: Se asocia el
price_iddel plan de Yastubo con la suscripción en Stripe. - Commission Splitting: Si el vendedor tiene Connect activo, se aplica el
application_fee_percentdefinido en elCompany.
Gestión de Errores y Reintentos
Section titled “Gestión de Errores y Reintentos”Si un cobro recurrente falla vía webhook de Stripe:
- Stripe notifica mediante
invoice.payment_failed. - Yastubo cambia el estado de la póliza a
IN_ARREARS(En mora). - Se activa el flujo de notificaciones para recordar el pago.
- Si el pago se regulariza, la póliza vuelve automáticamente a
ACTIVE.
El sistema también soporta reintentos manuales desde el panel de administración:
Motor de Reintentos (MAX_PAYMENT_ATTEMPTS = 2)
Section titled “Motor de Reintentos (MAX_PAYMENT_ATTEMPTS = 2)”| Intento | Comportamiento |
|---|---|
| 1er reintento | Crea un nuevo PaymentIntent en Stripe. La transacción vuelve a PENDING. |
| 2do reintento | Último intento permitido. Mismo flujo que el primero. |
| Límite alcanzado | El sistema bloquea el reintento y envía notificación automática al cliente por email y WhatsApp para que actualice su método de pago. |
MAX_PAYMENT_ATTEMPTS = 2
async def retry_payment(db, stripe, transaction_id, retried_by): # Si se alcanzó el límite: notificar y bloquear if transaction.attempt_count >= MAX_PAYMENT_ATTEMPTS: await notifications.on_payment_failed(policy, client, transaction.attempt_count) raise HTTPException(422, "Maximum retry attempts reached. Client notified.")
# Crear nuevo PaymentIntent y reintentar pi = await stripe.create_payment_intent(...) transaction.attempt_count += 1 transaction.status = "PENDING"Recordatorios de Pago y Vencimiento
Section titled “Recordatorios de Pago y Vencimiento”Para pólizas que no utilizan suscripciones recurrentes, el sistema ejecuta tareas diarias para:
- Recordatorios de Mora: Avisar a clientes con pólizas en
IN_ARREARS. - Aviso de Vencimiento: Notificar 3 días antes de que la vigencia de la póliza termine (
end_date).
Ejemplo Práctico: Creación de Suscripción
Section titled “Ejemplo Práctico: Creación de Suscripción”El siguiente código muestra cómo se orquestra una suscripción con división de comisiones:
async def create_subscription( db: AsyncSession, stripe: StripeClient, data: CreateSubscriptionRequest, issued_by: uuid.UUID,) -> Subscription: """ Crea una suscripción en Stripe vinculada a una póliza. Gestiona la división de comisiones si hay un revendedor activo. """ policy = await emission_service.get_policy(db, data.policy_id) company = await get_company(db, policy.company_id)
# Configuración de comisiones para Stripe Connect connect_account_id = None app_fee_percent = None
if company.is_reseller and company.stripe_connect_id: connect_account_id = company.stripe_connect_id app_fee_percent = float(company.commission_rate) # Lo que se queda la plataforma
# 1. Obtener o crear el cliente en Stripe customer_id = await get_or_create_customer(stripe, policy.client)
# 2. Iniciar la suscripción en Stripe stripe_sub = await stripe.create_subscription( customer_id=customer_id, price_id=policy.plan.stripe_price_id, payment_method_id=data.stripe_payment_method_id, connect_account_id=connect_account_id, application_fee_percent=app_fee_percent, )
# 3. Persistir la información localmente sub = Subscription( policy_id=policy.id, stripe_subscription_id=stripe_sub["id"], status=stripe_sub["status"].upper() ) db.add(sub) await db.commit() return subDiagrama de Proceso
Section titled “Diagrama de Proceso”
Endpoints Principales
Section titled “Endpoints Principales”| Método | Ruta | Rol requerido | Descripción |
|---|---|---|---|
POST | /api/v1/payments/intent | ADMIN, VENDEDOR | Crea un PaymentIntent (pago único). |
POST | /api/v1/payments/subscription | ADMIN, VENDEDOR | Inicia una suscripción recurrente mensual. |
POST | /api/v1/payments/subscription/cancel | ADMIN | Cancela una suscripción activa. |
POST | /api/v1/payments/manual | ADMIN | Registra un pago manual (fuera de Stripe). |
GET | /api/v1/payments/transactions | ADMIN, VENDEDOR | Lista transacciones, filtrable por póliza y estado. |
POST | /api/v1/payments/transactions/{id}/retry | ADMIN, VENDEDOR | Reintenta un cobro fallido (máx. 2 intentos). |
POST | /api/v1/payments/connect/onboarding | Autenticado | Inicia el flujo de registro de vendedor en Stripe Connect. |
GET | /api/v1/payments/reseller/dashboard | ADMIN, VENDEDOR | Dashboard de comisiones del revendedor. |
POST | /api/v1/payments/webhook | — | Endpoint para notificaciones de Stripe (firma verificada). |
Ejemplo: Reintentar un cobro fallido
Section titled “Ejemplo: Reintentar un cobro fallido”POST /api/v1/payments/transactions/{transaction_id}/retryAuthorization: Bearer <admin_token>Respuesta exitosa (intento 1 o 2):
{ "transaction_id": "uuid", "attempt_count": 2, "status": "PENDING", "message": "Retry attempt 2 of 2 initiated.", "client_secret": "pi_xxx_secret_yyy"}Respuesta al alcanzar el límite (HTTP 422):
{ "detail": "Maximum retry attempts (2) reached. A payment update notification has been sent to the client."}