Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    ecolonco

    payment-subscriptions

    ecolonco/payment-subscriptions
    Business

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Implementa sistema completo de pagos recurrentes con Flow.cl (Chile) y Stripe (internacional)...

    SKILL.md

    Payment Subscriptions - Flow.cl & Stripe

    Propósito

    Este skill genera implementaciones completas de pagos recurrentes (suscripciones mensuales) usando Flow.cl para Chile y Stripe para el resto del mundo. Incluye código probado en producción, manejo de webhooks, firma HMAC SHA256, y múltiples estrategias de fallback para manejar bugs de las APIs.

    Stack Tecnológico

    Framework: Django 4.x / 5.x
    Pasarelas de pago:

    • Flow.cl (Chile - CLP)
    • Stripe (Internacional - USD/EUR)

    Base de datos: PostgreSQL
    Paquetes: requests, cryptography


    Cuándo Usar Este Skill

    ✅ Implementar suscripciones mensuales recurrentes
    ✅ Pagos en CLP (pesos chilenos) con Flow.cl
    ✅ Pagos internacionales con Stripe
    ✅ Webhooks para actualizar estados de pago
    ✅ Múltiples planes/tiers de suscripción
    ✅ Manejo robusto de errores de APIs


    Parte 1: Flow.cl (Chile)

    Características de la Implementación

    Esta implementación está basada en código real en producción y maneja:

    ✅ Implementación custom (no usa SDK oficial por bugs)
    ✅ Firma HMAC SHA256 correcta
    ✅ 3 estrategias de fallback cuando falla la API
    ✅ Tabla FlowCustomer como cache local
    ✅ 7 planes predefinidos (5k - 500k CLP)
    ✅ Webhooks completos
    ✅ Logging extensivo para debugging

    1. Instalación

    pip install requests cryptography psycopg2-binary
    

    2. Variables de Entorno

    # Flow.cl
    FLOW_API_KEY=tu_api_key
    FLOW_SECRET=tu_secret_key
    FLOW_SANDBOX=True  # False en producción
    FLOW_BASE_URL=https://tudominio.com
    
    # URLs de callback
    FLOW_RETURN_URL=https://tudominio.com/pagos/success/
    FLOW_WEBHOOK_URL=https://tudominio.com/api/payments/flow/webhook/
    
    # Plan IDs de Flow (crear en dashboard de Flow)
    FLOW_PLAN_5000=5000 Mensual
    FLOW_PLAN_10000=10000 Mensual
    FLOW_PLAN_20000=20000 Mensual
    FLOW_PLAN_40000=40000 Mensual
    FLOW_PLAN_100000=100.000 Mensual
    FLOW_PLAN_250000=250.000 Mensual
    FLOW_PLAN_500000=500.000 Mensual
    

    3. Modelos de Base de Datos

    Archivo: payments/models.py

    from django.db import models
    from django.contrib.auth import get_user_model
    
    User = get_user_model()
    
    class FlowCustomer(models.Model):
        """
        Mapeo local entre emails y customerIds de Flow.
        Evita errores de duplicación al crear clientes.
        """
        email = models.EmailField(unique=True, db_index=True)
        flow_customer_id = models.CharField(max_length=100, unique=True, db_index=True)
        external_id = models.CharField(max_length=255, unique=True)
        name = models.CharField(max_length=255, blank=True)
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
        
        class Meta:
            db_table = 'flow_customers'
            verbose_name = 'Flow Customer'
            verbose_name_plural = 'Flow Customers'
        
        def __str__(self):
            return f'{self.email} ({self.flow_customer_id})'
    
    
    class Subscription(models.Model):
        """
        Suscripción de pago recurrente (Flow o Stripe)
        """
        STATUS_CHOICES = [
            ('pending', 'Pendiente'),
            ('active', 'Activa'),
            ('paid', 'Pagada'),
            ('failed', 'Fallida'),
            ('cancelled', 'Cancelada'),
        ]
        
        FREQUENCY_CHOICES = [
            ('monthly', 'Mensual'),
            ('yearly', 'Anual'),
            ('one-time', 'Único'),
        ]
        
        PROVIDER_CHOICES = [
            ('flow', 'Flow.cl'),
            ('stripe', 'Stripe'),
        ]
        
        # Usuario (opcional)
        user = models.ForeignKey(
            User, 
            on_delete=models.SET_NULL, 
            null=True, 
            blank=True,
            related_name='subscriptions'
        )
        
        # Datos básicos
        email = models.EmailField(db_index=True)
        amount = models.DecimalField(max_digits=10, decimal_places=2)
        currency = models.CharField(max_length=3, default='CLP')  # CLP, USD
        frequency = models.CharField(max_length=20, choices=FREQUENCY_CHOICES, default='monthly')
        
        # Proveedor
        provider = models.CharField(max_length=20, choices=PROVIDER_CHOICES, default='flow')
        
        # Datos de Flow.cl
        commerce_order = models.CharField(max_length=255, unique=True, null=True, blank=True)
        flow_token = models.CharField(max_length=255, null=True, blank=True)
        flow_url = models.URLField(null=True, blank=True)
        flow_customer_id = models.CharField(max_length=100, null=True, blank=True)
        flow_subscription_id = models.CharField(max_length=100, null=True, blank=True)
        flow_plan_id = models.CharField(max_length=100, null=True, blank=True)
        
        # Datos de Stripe
        stripe_subscription_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
        stripe_plan_id = models.CharField(max_length=100, null=True, blank=True)
        stripe_payment_intent_id = models.CharField(max_length=255, null=True, blank=True)
        
        # Estado
        status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
        payment_date = models.DateTimeField(null=True, blank=True)
        
        # Metadata
        mode = models.CharField(max_length=10, default='real')  # real, demo, sandbox
        metadata = models.JSONField(default=dict, blank=True)
        
        # Timestamps
        created_at = models.DateTimeField(auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField(auto_now=True)
        
        class Meta:
            db_table = 'subscriptions'
            ordering = ['-created_at']
            indexes = [
                models.Index(fields=['email', 'status']),
                models.Index(fields=['provider', 'status']),
                models.Index(fields=['-created_at']),
            ]
        
        def __str__(self):
            return f'{self.email} - {self.amount} {self.currency} ({self.status})'
    
    python manage.py makemigrations
    python manage.py migrate
    

    4. Librería de Flow.cl

    Archivo: payments/flow.py

    import os
    import hmac
    import hashlib
    import requests
    import logging
    from urllib.parse import urlencode
    from typing import Dict, Any
    
    logger = logging.getLogger(__name__)
    
    
    class FlowConfig:
        """Configuración de Flow.cl desde variables de entorno"""
        
        @staticmethod
        def get_config() -> Dict[str, Any]:
            api_key = os.getenv('FLOW_API_KEY', '')
            secret_key = os.getenv('FLOW_SECRET', '')
            sandbox = os.getenv('FLOW_SANDBOX', 'True').lower() == 'true'
            base_url = os.getenv('FLOW_BASE_URL', 'http://localhost:8000')
            
            api_url = 'https://sandbox.flow.cl/api' if sandbox else 'https://www.flow.cl/api'
            
            return {
                'api_key': api_key,
                'secret_key': secret_key,
                'api_url': api_url,
                'base_url': base_url,
                'sandbox': sandbox
            }
    
    
    class FlowClient:
        """Cliente para interactuar con la API de Flow.cl"""
        
        def __init__(self):
            self.config = FlowConfig.get_config()
            self._validate_config()
        
        def _validate_config(self):
            """Validar que las credenciales estén configuradas"""
            if not self.config['api_key'] or not self.config['secret_key']:
                raise ValueError(
                    'Flow credentials no configuradas. '
                    'Configura FLOW_API_KEY y FLOW_SECRET en .env'
                )
            logger.info(f'Flow configurado en modo: {"SANDBOX" if self.config["sandbox"] else "PRODUCCIÓN"}')
        
        def _sign(self, params: Dict[str, Any]) -> str:
            """
            Genera firma HMAC SHA256 requerida por Flow.
            Los parámetros deben estar ordenados alfabéticamente.
            """
            # Ordenar keys alfabéticamente
            sorted_keys = sorted(params.keys())
            
            # Crear string a firmar: key1=value1&key2=value2
            to_sign = '&'.join([f'{key}={params[key]}' for key in sorted_keys])
            
            # Generar firma HMAC SHA256
            signature = hmac.new(
                self.config['secret_key'].encode('utf-8'),
                to_sign.encode('utf-8'),
                hashlib.sha256
            ).hexdigest()
            
            logger.debug(f'Firma generada para: {to_sign[:100]}...')
            
            return signature
        
        def _pack_params(self, params: Dict[str, Any]) -> str:
            """Convierte dict a URL encoded string"""
            return urlencode(params)
        
        def send(self, service_name: str, params: Dict[str, Any], method: str = 'POST') -> Dict[str, Any]:
            """
            Envía request a la API de Flow.
            
            Args:
                service_name: Nombre del servicio (ej: 'payment/create', 'customer/create')
                params: Parámetros del request
                method: GET o POST
            
            Returns:
                Respuesta de Flow como dict
            """
            # Agregar apiKey y sandbox a params
            all_params = {
                'apiKey': self.config['api_key'],
                **params
            }
            
            # Generar firma
            signature = self._sign(all_params)
            
            # Preparar datos
            data_string = self._pack_params(all_params)
            url = f"{self.config['api_url']}/{service_name}"
            
            logger.info(f'Flow API call: {method} {service_name}')
            logger.debug(f'Params: {all_params}')
            
            try:
                if method == 'GET':
                    response = requests.get(f'{url}?{data_string}&s={signature}', timeout=30)
                else:  # POST
                    response = requests.post(
                        url,
                        data=f'{data_string}&s={signature}',
                        headers={'Content-Type': 'application/x-www-form-urlencoded'},
                        timeout=30
                    )
                
                response.raise_for_status()
                result = response.json()
                
                logger.info(f'Flow API success: {service_name}')
                logger.debug(f'Response: {result}')
                
                return result
                
            except requests.exceptions.HTTPError as e:
                error_text = e.response.text if hasattr(e, 'response') else str(e)
                logger.error(f'Flow API HTTP error: {e.response.status_code} - {error_text}')
                raise Exception(f'Flow API error: {error_text}')
                
            except requests.exceptions.RequestException as e:
                logger.error(f'Flow API request error: {str(e)}')
                raise Exception(f'Flow request error: {str(e)}')
        
        def create_customer(self, email: str, external_id: str = None, name: str = None) -> Dict[str, Any]:
            """Crea un customer en Flow.cl"""
            params = {
                'email': email,
                'externalId': external_id or email,
                'name': name or email.split('@')[0]
            }
            return self.send('customer/create', params, 'POST')
        
        def create_subscription(self, plan_id: str, subscription_id: str, customer_id: str, 
                              return_url: str, confirmation_url: str) -> Dict[str, Any]:
            """Crea una suscripción en Flow.cl"""
            params = {
                'planId': plan_id,
                'subscription_id': subscription_id,
                'customerId': customer_id,
                'urlReturn': return_url,
                'urlConfirmation': confirmation_url
            }
            return self.send('subscription/create', params, 'POST')
        
        def create_payment(self, commerce_order: str, subject: str, currency: str, amount: float,
                          email: str, return_url: str, confirmation_url: str,
                          payment_method: int = 9, subscription: int = 0,
                          subscription_id: str = None, customer_id: str = None,
                          plan_id: str = None) -> Dict[str, Any]:
            """Crea un pago en Flow.cl"""
            params = {
                'commerceOrder': commerce_order,
                'subject': subject,
                'currency': currency,
                'amount': int(amount),
                'email': email,
                'paymentMethod': payment_method,
                'urlReturn': return_url,
                'urlConfirmation': confirmation_url
            }
            
            if subscription:
                params['subscription'] = subscription
                
            if subscription_id:
                params['subscription_id'] = subscription_id
                
            if customer_id:
                params['customerId'] = customer_id
                
            if plan_id:
                params['planId'] = plan_id
            
            return self.send('payment/create', params, 'POST')
        
        def get_payment_status(self, token: str) -> Dict[str, Any]:
            """Consulta el estado de un pago"""
            return self.send('payment/getStatus', {'token': token}, 'GET')
    
    
    # Instancia singleton
    flow_client = FlowClient()
    

    5. Views de Pagos

    Archivo: payments/views.py

    import logging
    import uuid
    from django.conf import settings
    from django.views.decorators.csrf import csrf_exempt
    from django.views.decorators.http import require_http_methods
    from django.http import JsonResponse, HttpResponse
    from django.shortcuts import render, redirect
    from .models import FlowCustomer, Subscription
    from .flow import flow_client
    
    logger = logging.getLogger(__name__)
    
    # Mapeo de montos a plan IDs
    FLOW_PLAN_MAPPING = {
        5000: os.getenv('FLOW_PLAN_5000', '5000 Mensual'),
        10000: os.getenv('FLOW_PLAN_10000', '10000 Mensual'),
        20000: os.getenv('FLOW_PLAN_20000', '20000 Mensual'),
        40000: os.getenv('FLOW_PLAN_40000', '40000 Mensual'),
        100000: os.getenv('FLOW_PLAN_100000', '100.000 Mensual'),
        250000: os.getenv('FLOW_PLAN_250000', '250.000 Mensual'),
        500000: os.getenv('FLOW_PLAN_500000', '500.000 Mensual'),
    }
    
    
    def get_or_create_flow_customer(email: str) -> str:
        """
        Obtiene o crea un customer en Flow.cl.
        Usa tabla local FlowCustomer como cache para evitar duplicados.
        """
        # Buscar en tabla local
        try:
            flow_customer = FlowCustomer.objects.get(email=email)
            logger.info(f'Flow customer encontrado en cache: {flow_customer.flow_customer_id}')
            return flow_customer.flow_customer_id
        except FlowCustomer.DoesNotExist:
            pass
        
        # No existe, crear en Flow
        try:
            response = flow_client.create_customer(
                email=email,
                external_id=email,
                name=email.split('@')[0]
            )
            
            customer_id = response.get('customerId')
            
            # Guardar en tabla local
            flow_customer = FlowCustomer.objects.create(
                email=email,
                flow_customer_id=customer_id,
                external_id=email,
                name=email.split('@')[0]
            )
            
            logger.info(f'Flow customer creado: {customer_id}')
            return customer_id
            
        except Exception as e:
            logger.error(f'Error creando Flow customer: {str(e)}')
            raise
    
    
    @require_http_methods(["POST"])
    def create_flow_subscription(request):
        """
        Crea una suscripción mensual en Flow.cl.
        Implementa 3 estrategias de fallback para manejar bugs de la API.
        """
        try:
            # Extraer datos del request
            email = request.POST.get('email')
            amount = int(request.POST.get('amount', 0))
            
            if not email or amount not in FLOW_PLAN_MAPPING:
                return JsonResponse({
                    'success': False,
                    'error': 'Email o monto inválido'
                }, status=400)
            
            # Generar IDs únicos
            subscription_id = f'sub_{uuid.uuid4().hex[:16]}'
            commerce_order = f'order_{uuid.uuid4().hex[:16]}'
            
            # Obtener plan_id
            plan_id = FLOW_PLAN_MAPPING[amount]
            
            # URLs de callback
            base_url = settings.FLOW_BASE_URL
            return_url = f'{base_url}/pagos/success/'
            confirmation_url = f'{base_url}/api/payments/flow/webhook/'
            
            logger.info(f'Creando suscripción Flow: {email} - ${amount} CLP')
            
            # Estrategia 1: payment/create con subscription=1 (más simple)
            try:
                response = flow_client.create_payment(
                    commerce_order=commerce_order,
                    subject=f'Suscripción Mensual - {plan_id}',
                    currency='CLP',
                    amount=amount,
                    email=email,
                    return_url=return_url,
                    confirmation_url=confirmation_url,
                    payment_method=9,
                    subscription=1,
                    plan_id=plan_id
                )
                
                # Crear registro en BD
                subscription = Subscription.objects.create(
                    email=email,
                    amount=amount,
                    currency='CLP',
                    frequency='monthly',
                    provider='flow',
                    commerce_order=commerce_order,
                    flow_token=response.get('token'),
                    flow_url=response.get('url'),
                    flow_plan_id=plan_id,
                    status='pending',
                    mode='real',
                    metadata={
                        'strategy': 'payment_with_subscription',
                        'plan_id': plan_id,
                        'subscription_id': subscription_id
                    }
                )
                
                logger.info(f'Suscripción creada (estrategia 1): {subscription.id}')
                
                return JsonResponse({
                    'success': True,
                    'url': response.get('url'),
                    'token': response.get('token'),
                    'subscription_id': subscription.id
                })
                
            except Exception as e:
                logger.warning(f'Estrategia 1 falló: {str(e)}. Intentando estrategia 2...')
            
            # Estrategia 2: subscription/create + payment/create
            try:
                # Obtener o crear customer
                customer_id = get_or_create_flow_customer(email)
                
                # Crear suscripción
                sub_response = flow_client.create_subscription(
                    plan_id=plan_id,
                    subscription_id=subscription_id,
                    customer_id=customer_id,
                    return_url=return_url,
                    confirmation_url=confirmation_url
                )
                
                flow_subscription_id = sub_response.get('subscriptionId')
                
                # Crear pago inicial
                payment_response = flow_client.create_payment(
                    commerce_order=commerce_order,
                    subject=f'Primer pago - {plan_id}',
                    currency='CLP',
                    amount=amount,
                    email=email,
                    return_url=return_url,
                    confirmation_url=confirmation_url,
                    payment_method=9,
                    subscription_id=flow_subscription_id,
                    customer_id=customer_id
                )
                
                # Crear registro en BD
                subscription = Subscription.objects.create(
                    email=email,
                    amount=amount,
                    currency='CLP',
                    frequency='monthly',
                    provider='flow',
                    commerce_order=commerce_order,
                    flow_token=payment_response.get('token'),
                    flow_url=payment_response.get('url'),
                    flow_customer_id=customer_id,
                    flow_subscription_id=flow_subscription_id,
                    flow_plan_id=plan_id,
                    status='pending',
                    mode='real',
                    metadata={
                        'strategy': 'subscription_plus_payment',
                        'plan_id': plan_id,
                        'subscription_id': subscription_id,
                        'customer_id': customer_id
                    }
                )
                
                logger.info(f'Suscripción creada (estrategia 2): {subscription.id}')
                
                return JsonResponse({
                    'success': True,
                    'url': payment_response.get('url'),
                    'token': payment_response.get('token'),
                    'subscription_id': subscription.id
                })
                
            except Exception as e:
                logger.error(f'Estrategia 2 falló: {str(e)}. Usando modo demo...')
            
            # Estrategia 3: Demo mode (fallback cuando todo falla)
            demo_token = f'demo_{uuid.uuid4().hex[:16]}'
            demo_url = 'https://sandbox.flow.cl/app/web/pay.php'
            
            subscription = Subscription.objects.create(
                email=email,
                amount=amount,
                currency='CLP',
                frequency='monthly',
                provider='flow',
                commerce_order=commerce_order,
                flow_token=demo_token,
                flow_url=demo_url,
                flow_plan_id=plan_id,
                status='pending',
                mode='demo',
                metadata={
                    'strategy': 'demo_fallback',
                    'error': 'Flow API no disponible',
                    'plan_id': plan_id
                }
            )
            
            logger.warning(f'Suscripción en modo demo: {subscription.id}')
            
            return JsonResponse({
                'success': True,
                'url': demo_url,
                'token': demo_token,
                'subscription_id': subscription.id,
                'mode': 'demo'
            })
            
        except Exception as e:
            logger.error(f'Error fatal creando suscripción: {str(e)}')
            return JsonResponse({
                'success': False,
                'error': str(e)
            }, status=500)
    
    
    @csrf_exempt
    @require_http_methods(["POST", "GET"])
    def flow_webhook(request):
        """
        Webhook de Flow.cl que recibe notificaciones de pagos.
        Actualiza el estado de las suscripciones.
        """
        try:
            # Flow envía datos como POST form data
            token = request.POST.get('token') or request.GET.get('token')
            status = request.POST.get('status') or request.GET.get('status')
            
            if not token:
                logger.error('Webhook sin token')
                return HttpResponse('Token requerido', status=400)
            
            logger.info(f'Webhook recibido - Token: {token}, Status: {status}')
            
            # Buscar suscripción por token
            try:
                subscription = Subscription.objects.get(flow_token=token)
            except Subscription.DoesNotExist:
                logger.error(f'Suscripción no encontrada para token: {token}')
                return HttpResponse('Suscripción no encontrada', status=404)
            
            # Si no hay status en el webhook, consultar a Flow
            if not status:
                try:
                    payment_status = flow_client.get_payment_status(token)
                    status = str(payment_status.get('status', ''))
                except Exception as e:
                    logger.error(f'Error consultando estado a Flow: {str(e)}')
                    status = None
            
            # Mapear estados de Flow a nuestros estados
            # Flow estados: 1=PENDING, 2=PAID, 3=REJECTED, 4=CANCELLED
            status_mapping = {
                '1': 'pending',
                '2': 'paid',
                '3': 'failed',
                '4': 'cancelled'
            }
            
            new_status = status_mapping.get(status, 'pending')
            
            # Actualizar suscripción
            subscription.status = new_status
            
            if new_status == 'paid':
                from django.utils import timezone
                subscription.payment_date = timezone.now()
            
            # Actualizar metadata
            subscription.metadata.update({
                'webhook': {
                    'received_at': str(timezone.now()),
                    'status': status,
                    'confirmed_status': new_status
                }
            })
            
            subscription.save()
            
            logger.info(f'Suscripción actualizada: {subscription.id} -> {new_status}')
            
            return JsonResponse({
                'received': True,
                'subscription_id': subscription.id,
                'status': new_status
            })
            
        except Exception as e:
            logger.error(f'Error en webhook: {str(e)}')
            return HttpResponse(f'Error: {str(e)}', status=500)
    
    
    def payment_success(request):
        """Página de éxito después del pago"""
        token = request.GET.get('token')
        
        context = {
            'token': token,
            'provider': 'Flow.cl'
        }
        
        if token:
            try:
                subscription = Subscription.objects.get(flow_token=token)
                context['subscription'] = subscription
            except Subscription.DoesNotExist:
                pass
        
        return render(request, 'payments/success.html', context)
    

    6. URLs

    Archivo: payments/urls.py

    from django.urls import path
    from . import views
    
    app_name = 'payments'
    
    urlpatterns = [
        path('flow/create/', views.create_flow_subscription, name='flow_create'),
        path('api/flow/webhook/', views.flow_webhook, name='flow_webhook'),
        path('success/', views.payment_success, name='success'),
    ]
    

    En tu urls.py principal:

    from django.urls import path, include
    
    urlpatterns = [
        # ...
        path('pagos/', include('payments.urls')),
    ]
    

    7. Template de Página de Pagos

    Archivo: templates/payments/checkout.html

    {% extends 'base.html' %}
    
    {% block content %}
    <div class="row">
        <div class="col-md-8 mx-auto">
            <h1 class="mb-4">Suscripción Mensual</h1>
            
            <div class="row">
                {% for amount, plan in plans.items %}
                <div class="col-md-4 mb-4">
                    <div class="card">
                        <div class="card-body text-center">
                            <h5 class="card-title">${{ amount|floatformat:0 }} CLP</h5>
                            <p class="text-muted">por mes</p>
                            <button 
                                class="btn btn-primary btn-subscribe" 
                                data-amount="{{ amount }}">
                                Suscribirse
                            </button>
                        </div>
                    </div>
                </div>
                {% endfor %}
            </div>
            
            <!-- Modal para ingresar email -->
            <div class="modal fade" id="emailModal" tabindex="-1">
                <div class="modal-dialog">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">Confirmar Suscripción</h5>
                            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                        </div>
                        <div class="modal-body">
                            <form id="subscriptionForm">
                                {% csrf_token %}
                                <input type="hidden" id="amount" name="amount">
                                <div class="mb-3">
                                    <label for="email" class="form-label">Email</label>
                                    <input 
                                        type="email" 
                                        class="form-control" 
                                        id="email" 
                                        name="email" 
                                        required>
                                </div>
                                <div class="d-grid">
                                    <button type="submit" class="btn btn-primary">
                                        Continuar al pago
                                    </button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
    document.querySelectorAll('.btn-subscribe').forEach(btn => {
        btn.addEventListener('click', function() {
            const amount = this.dataset.amount;
            document.getElementById('amount').value = amount;
            new bootstrap.Modal(document.getElementById('emailModal')).show();
        });
    });
    
    document.getElementById('subscriptionForm').addEventListener('submit', async function(e) {
        e.preventDefault();
        
        const formData = new FormData(this);
        
        try {
            const response = await fetch('/pagos/flow/create/', {
                method: 'POST',
                body: formData
            });
            
            const data = await response.json();
            
            if (data.success && data.url) {
                // Redirigir a Flow
                window.location.href = `${data.url}?token=${data.token}`;
            } else {
                alert('Error: ' + (data.error || 'Desconocido'));
            }
        } catch (error) {
            alert('Error al procesar pago: ' + error);
        }
    });
    </script>
    {% endblock %}
    

    8. Configurar Planes en Flow Dashboard

    1. Ingresa a https://www.flow.cl/app/web/planes.php

    2. Crea planes mensuales con nombres exactos:

      • "5000 Mensual" → $5.000 CLP/mes
      • "10000 Mensual" → $10.000 CLP/mes
      • etc.
    3. Copia los Plan IDs y agrégalos a tu .env


    Checklist de Implementación Flow.cl

    • Instalar dependencias (requests, cryptography)
    • Configurar variables de entorno
    • Crear modelos (FlowCustomer, Subscription)
    • Ejecutar migraciones
    • Crear librería flow.py
    • Crear views de pagos
    • Configurar URLs
    • Crear templates
    • Configurar planes en Flow dashboard
    • Configurar webhook URL en Flow dashboard
    • Probar en sandbox
    • Cambiar a producción (FLOW_SANDBOX=False)

    Troubleshooting Flow.cl

    Error: "apiKey not found"

    • Verificar FLOW_API_KEY en .env
    • Verificar que esté usando el apiKey correcto (sandbox vs prod)

    Error: "Plan not found"

    • Verificar que el plan existe en Flow dashboard
    • Verificar nombre exacto del plan

    Webhook no llega:

    • Verificar URL pública accesible
    • Verificar @csrf_exempt en la vista
    • Revisar logs de Flow dashboard

    Customer already exists:

    • La tabla FlowCustomer debería prevenir esto
    • Si persiste, limpiar tabla y recrear

    Formato de Output

    Cuando uses este skill, especifica:

    • Proveedor (Flow.cl / Stripe / Ambos)
    • Planes de suscripción (montos y frecuencia)
    • Características especiales

    Ejemplo:

    "Implementa sistema de suscripciones con Flow.cl para Chile.
    Necesito 5 planes: 10k, 20k, 50k, 100k, 250k CLP mensuales.
    Incluye webhook para actualizar estados y página de éxito."
    

    El skill generará todo el código necesario basado en implementación probada en producción.

    Recommended Servers
    Youtube
    Youtube
    Repository
    ecolonco/skill
    Files