payment-subscriptions
Implementa sistema completo de pagos recurrentes con Flow.cl (Chile) y Stripe (internacional). Incluye suscripciones mensuales, webhooks, firma HMAC, manejo de errores y múltiples estrategias de fallback. Basado en implementación real probada en producción.
When & Why to Use This Skill
This Claude skill provides a production-ready, end-to-end implementation of a recurring payment system for Django applications. It specializes in integrating Flow.cl for the Chilean market and Stripe for international subscriptions, featuring secure HMAC SHA256 signing, comprehensive webhook handling, and advanced fallback strategies to ensure high reliability and transaction integrity.
Use Cases
- SaaS Platforms: Launching a subscription-based service that requires seamless payment processing in Chilean Pesos (CLP) via Flow.cl and international currencies via Stripe.
- Custom Payment Integration: Implementing a robust payment gateway logic that bypasses buggy official SDKs by using a custom-built, production-tested implementation for Flow.cl.
- Automated Subscription Management: Synchronizing local database records with payment providers using secure webhooks to handle successful payments, failures, and cancellations automatically.
- Reliable Financial Operations: Deploying a payment system with built-in redundancy and fallback mechanisms to maintain service continuity even when third-party APIs experience intermittent issues.
| name | payment-subscriptions |
|---|---|
| description | Implementa sistema completo de pagos recurrentes con Flow.cl (Chile) y Stripe (internacional). Incluye suscripciones mensuales, webhooks, firma HMAC, manejo de errores y múltiples estrategias de fallback. Basado en implementación real probada en producción. |
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
Ingresa a https://www.flow.cl/app/web/planes.php
Crea planes mensuales con nombres exactos:
- "5000 Mensual" → $5.000 CLP/mes
- "10000 Mensual" → $10.000 CLP/mes
- etc.
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.