Prompts para Testear Pagos con Stripe e IA: Webhooks y Suscripciones
10 prompts para testear la integración de Stripe con IA: webhooks, idempotencia, suscripciones, reembolsos y los casos que nadie testea hasta que fallan.
Tabla de contenidos
Voy a contar algo que pasa cada semana en algún proyecto de Stripe. El pago funciona. El webhook funciona. El cliente entra en la app y ve su suscripción activa. A las tres de la mañana, un email automático de un proveedor actualiza su modelo de IA, se reinician tres contenedores y tu webhook se procesa por segunda vez con el mismo event.id.
Tu código suma el pago otra vez. O cancela la suscripción de un cliente que sí pagó.
El problema no es Stripe. Stripe está bien documentado y su API es de las más sólidas que existen. El problema es que testear pagos da pereza, porque en cuanto lo automatizas te mete miedo gastar dinero real. Así que casi nadie lo automatiza, y luego se descubren los bugs en producción, que es el peor sitio posible para descubrir que tu lógica de reembolso estaba mal.
La salida es el entorno de test de Stripe: tarjetas de test que no mueven dinero, stripe trigger para disparar eventos concretos, clocks de test para simular el paso de un año en un minuto, y la posibilidad de escribir tests deterministas que corren en CI sin que nadie configure nada.
Este artículo te da los 10 prompts para construir esa suite. Y si antes de esto integraste Stripe a pelo, la guía de webhooks con Next.js te da el handler base sobre el que trabajar.
Seis cosas que hay que tener claras antes de testear pagos
1. La idempotencia no es opcional, es la base
Stripe entrega cada evento al menos una vez. No exactamente una vez. Nunca asumas que un evento llega una sola vez, ni que llega en orden. La idempotencia con event.id es la diferencia entre un glitch y un incidente con clientes enfadados.
2. Nunca invoques nada externo antes de responder el 2xx
Stripe espera tu respuesta y, si tarda más de 20 segundos, reintenta. Si dentro de ese handler llamas a un modelo de IA, mandas un email o escribes en tu cola, tienes dos opciones: hacerlo después de responder, o hacerlo dentro de una transacción con la idempotencia ya registrada. Lo contrario son llamadas duplicadas de pago.
3. El estado local es una caché, no la fuente de verdad
Stripe es la fuente de verdad del estado del pago. Tu base de datos guarda una copia para no llamar a la API en cada request. Si divergen, gana Stripe, y tu webhook es lo que la corrige. Si diseñas al revés, cada desincronización es un problema de datos.
4. La prueba de la idempotencia es enviar el mismo evento dos veces
No la simules con un mock. Usa stripe trigger con el mismo evento, o reenvía el payload firmado desde un test. Si tu idempotencia solo funciona en el test unitario con un mock, no funciona.
5. El modo test tiene tarjetas que fallan a propósito
4000000000000002 siempre se rechaza. 4000000000009995 falla la verificación de saldo. 4000000000000069 caduca. No es un detalle: es la forma de testear los caminos de error sin inventar mocks.
6. Usa clocks de test para lo que tarda
Renovaciones, expiraciones de prueba gratuita, retry de un cobro fallido: los test clocks de Stripe ejecutan meses de vida de suscripción en segundos. Sin ellos, un test de expiración tarda un mes o no se hace.
1. Montar el harness de test con Stripe CLI
Prompt:
Quiero una suite de tests para mi integración de Stripe:
STACK: [Next.js 15 route handlers / Express / NestJS]
BASE DE DATOS: [Postgres con Prisma / Drizzle]
TESTS: [Vitest + Playwright]
STRIPE: [stripe v18 / stripe-node, API version 2026-XX]
Genera:
1. package.json scripts: stripe:listen (forward a localhost), stripe:trigger,
test:payments, test:payments:watch, seed:test.
2. docker-compose.yml para Postgres de test, con migraciones aplicadas
automáticamente al arrancar.
3. Un fixture helper que cree una sesión de Stripe Connect de test con una
cuenta completa (customer, price, product, subscription) usando el
cliente de test. Debe ser idempotente: si los IDs están en variables de
entorno, los reutiliza en vez de crear duplicados.
4. El script de seed que crea un catálogo de precios: free trial, mensual,
anual, plan por uso. Y al menos 4 casos de prueba de metered billing.
5. Un beforeEach que resetea el estado entre tests: limpia las tablas
locales, no toca Stripe (los objetos de test se reutilizan, borrarlos es
más lento y menos fiable).
6. El helper para firmar payloads manualmente en tests unitarios, con
el STRIPE_WEBHOOK_SECRET de test, para poder probar la verificación de
firma sin pasar por Stripe.
Todo con tipos de TypeScript. Sin `any`.
Por qué funciona: la parte que más tiempo ahorra es la idempotencia del fixture y el no borrar objetos de Stripe en cada test. Crear productos y precios vía API en cada test multiplica por diez el tiempo de la suite, y los IDs hardcodeados en el repo son un desastre.
2. Testear la verificación de firma del webhook
Prompt:
Este es mi handler de webhooks de Stripe:
[PEGAR CÓDIGO]
Y este es mi middleware de firma:
[PEGAR CÓDIGO]
Genera los tests de seguridad con vitest:
1. Firma válida → 200 y se procesa.
2. Firma inválida (otro secreto) → 400, NO se procesa nada.
3. Payload modificado tras firmar (cambio un byte) → 400.
4. Timestamp fuera de la ventana de tolerancia de Stripe (5 minutos) → 400.
5. Header ausente → 400.
6. Body vacío o no-JSON → 400, sin excepción.
7. Cabecera en minúsculas, con mayúsculas y con espacios extra alrededor
del '=' (Stripe varía el formato).
8. El body debe leerse como texto crudo ANTES de JSON.parse: un body ya
parseado y re-serializado rompe la firma. Test que lo demuestre
explícitamente.
Para cada test verifica también que NO se ha escrito nada en la base de
datos cuando la validación falla.
Importante: usa el paquete oficial stripe.webhooks.constructEvent, no una
implementación propia, y testea que el timestamp se valide.
Por qué funciona: el punto 8 es el bug más común de todos. Si el body pasa por un middleware que lo parsea (un express.json() global, por ejemplo), la firma no coincide porque los bytes son otros, y todos los webhooks empiezan a fallar en producción el mismo día que despliegas el middleware.
3. Testear idempotencia con el mismo evento dos veces
Prompt:
Este es mi procesamiento de eventos de Stripe:
[PEGAR CÓDIGO DEL HANDLER Y LA LÓGICA DE PROCESADO]
Genera los tests de idempotencia:
1. El MISMO payload (mismo event.id) enviado dos veces:
- la primera vez se procesa y cambia el estado
- la segunda vez devuelve 200 pero NO vuelve a aplicar el efecto
- verifica con asserts sobre el estado de la base de datos, no solo
sobre el status code
2. Dos eventos DISTINTOS (event.id diferente) con el mismo efecto de negocio
(por ejemplo, dos customer.subscription.updated del mismo objeto):
- ambos se procesan
3. Carrera: dos peticiones con el mismo event.id, simuladas con
Promise.all. Exactamente una aplica el efecto. Esto requiere una
restricción de unicidad en base de datos (unique index sobre
event_id), no solo un SELECT antes del INSERT.
4. Fallo a mitad: el efecto se aplicó pero la transacción no hizo commit.
Reintento del mismo evento: no debe duplicar.
5. Guardar el idempotency key de Stripe (el header Idempotency-Key) en
la request de la API, no solo en los webhooks.
Incluye la migración del índice único que hace esto posible. Si el INSERT
choca con la restricción, captúralo y devuelve 200: es un duplicado, no
un error.
Escribe los tests y el código corregido.
Por qué funciona: el punto 3 es el que separa un test que pasa de una integración que aguanta. Un SELECT seguido de INSERT tiene una ventana de carrera entre ambos; el índice único en base de datos la cierra. Y el principio de devolver 200 en el duplicado es obligatorio: si devuelves error, Stripe reintenta para siempre.
4. Cubrir todos los eventos del ciclo de vida
Prompt:
Estos son los tipos de evento que mi integración maneja:
[PEGAR LISTA DE event.type EN EL SWITCH]
Compara con la lista oficial de eventos de Stripe relacionados con
[payment_intent | setup_intent | invoice | customer.subscription |
checkout.session | charge | refund | dispute]:
[PEGAR LISTA QUE CONSIDERAS NECESARIOS]
Analiza:
1. ¿Qué eventos me faltan y qué efecto tienen en mi flujo? Presta atención
especial a los que solo ocurren en el fallo: payment_intent.payment_failed,
invoice.payment_failed, charge.refunded, charge.dispute.created,
customer.subscription.trial_will_end.
2. ¿Qué eventos manejarían un estado que NO debería ocurrir y por eso
están ignorados? Dime si ese "no debería" es real o es una excusa.
3. Genera los tests para cada evento: payload real de ejemplo, estado
previo en base de datos, estado esperado después.
4. El caso de la nueva versión de API de Stripe (2026-XX): ¿hay cambios
en el shape de los payloads o en los nombres de los campos? Lista los
que son breaking.
Devuelve el switch completo con TODOS los eventos, con el type como tipo
de TypeScript (Stripe.Events.InvoicePaymentFailed, no string), y el fichero
de tests asociado.
Por qué funciona: el switch sobre strings es donde se esconden los typos silenciosos. Con los tipos de Stripe (Stripe.Events.*), TypeScript te dice que te falta un caso al añadir un evento nuevo. Es el mismo mecanismo que el que usas para no romper el contrato de tu API.
5. Testear suscripciones: upgrades, downgrades y cancelaciones
Este es el bloque donde más bugs he visto.
Prompt:
Mi app tiene planes: [FREE con 14 días trial, PRO MENSUAL 29€, PRO ANUAL
290€]. Este es mi código de gestión de suscripciones:
[PEGAR CÓDIGO: upgrade, downgrade, cancel, reactivate]
Genera tests para cada transición, usando test clocks de Stripe para que
no dependan del tiempo real:
1. Trial activo → el usuario hace upgrade a PRO:
- Stripe crea una invoice prorrateada por el resto del trial
- mi base de datos cambia a PRO inmediatamente
- el payment_method del cliente se reutiliza
2. Mensual → Anual: ¿se crea una suscripción nueva o se modifica la
existente? Verifica que el prorrateo es el correcto y que NO se cobra
dos veces.
3. Downgrade de PRO a FREE:
- ¿cuándo se aplica? En mi caso tiene que ser al final del periodo
- ¿se cancela la suscripción de Stripe o se deja correr hasta el final?
- ¿qué pasa si el usuario hace upgrade antes de que expire el
downgrade? (caso real: se pierde el cambio)
4. Cancelación con cancel_at_period_end = true:
- el acceso se mantiene hasta period_end
- llega customer.subscription.deleted y ahí SÍ se quita el acceso
- ¿qué pasa con un webhook de deleted que llega antes de tiempo?
5. Reactivar una suscripción cancelada: ¿crea una nueva o la "des-cancela"?
6. Fallo de pago en la renovación: ¿qué pasa con el acceso? ¿Stripe
redirige a mi web para actualizar la tarjeta (hosted invoice)?
7. Pago rechazado 3 veces: Stripe cancela la suscripción. ¿Llega el evento
y se refleja en mi estado?
Cada test debe usar stripe.test_helpers.test_helpers.advance_test_clock para
saltar al momento exacto y limpiar el clock en el afterEach.
Escribe los tests con el SDK real, no con mocks del cliente de Stripe.
Por qué funciona: los test clocks son el truco que convierte “esto lo probamos en staging dentro de tres meses” en un test de 30 segundos. Y el caso 3 (upgrade antes de que expire el downgrade) es el que te va a llegar por soporte y no vas a saber responder.
6. Testear reembolsos, disputas y cancelaciones
Prompt:
Necesito tests para los caminos de dinero que van en contra:
STACK: [PEGAR CÓDIGO DE REEMBOLSO Y DISPUTAS SI EXISTE]
Genera:
1. Reembolso total: charge.refunded llega con amount_refunded completo.
- ¿Se revierte el acceso? ¿Cuándo? ¿Inmediatamente o al final del
periodo si hay suscripción?
- ¿Se devuelve el uso de IA ya consumido o se pierde el crédito?
2. Reembolso parcial: amount_refunded menor que amount.
- ¿Tu código asume reembolso total? Ese es el bug clásico.
- amount_refunded llega en charge.refund.updated, no solo en el
original.
3. Reembolso vía Stripe Dashboard (sin pasar por tu API): el webhook debe
llegar igualmente y tu estado debe actualizarse.
4. Disputa (charge.dispute.created):
- el dinero ya se ha retirado de tu cuenta
- llega 5-75 días después del pago
- ¿qué hace tu app con el acceso del cliente mientras tanto?
- ¿guardas la evidencia de la compra (importe, IP, email, consenting
timestamp) para poder defenderla?
- la FEE de disputa: ¿tus márgenes asumen que no existe?
5. charge.dispute.closed con won: el dinero vuelve. Tu estado se revierte.
6. charge.dispute.closed con lost: nunca vuelve.
Añade un endpoint de exportación de "prueba de compra" para responder
disputas, y los tests correspondientes.
Incluye un test que verifique que el manejo de reembolso es IDEMPOTENTE:
refund.updated puede llegar dos veces.
Por qué funciona: la disputa es el escenario donde más tiempo pasa entre el evento y el problema, así que nadie lo testea. Y el punto 2 (reembolso parcial) es el que más código rompe: la mayoría de handlers asumen refunded: true como si fuera total.
7. Testear el flujo de checkout y los métodos de pago
Prompt:
Genera tests para el flujo de checkout con Stripe Checkout y Payment
Elements:
[PEGAR CÓDIGO DE CREACIÓN DE SESIÓN Y DEL ELEMENTO DE PAGO]
Casos a cubrir con las tarjetas de test:
- 4242424242424242 → aprobado
- 4000000000000002 → declined (card_declined)
- 4000000000009995 → insufficient_funds
- 4000000000000069 → expired_card
- 4000000000000127 → incorrect_cvc
- 4000000000000119 → processing_error (3DS requiere autenticación)
- 4000000000000341 → attaching falla si la tarjeta ya está en otra cuenta
Además:
1. 3DS: el flujo de autenticación, el return_url, y qué pasa si el
usuario cierra el modal a mitad.
2. SCA: tarjeta de una región que requiere autenticación. ¿Mi backend
reintenta el payment_intent con el mismo idempotency key?
3. El caso del payment_intent que requiere_action: mi frontend debe
confirmar. Testear que el frontend no trata "requires_action" como
"exitoso".
4. Idempotency-Key en la creación del PaymentIntent: si el usuario hace
doble clic en "Pagar", ¿se crean dos intents o uno?
5. Modo test vs live: ¿hay alguna constante que se me pueda colar en
producción? Test que verifique que live secret y publishable key
casan entre sí.
Usa Playwright para los tests E2E del flujo real, mocking solo la API de
Stripe en los casos de frontend.
Por qué funciona: el punto 5 previene el peor error posible: una clave de test en producción, que en realidad Stripe rechaza (no rompe, pero suena a que la app está rota) o al revés, una live key en staging (dinero real). El test que verifica que las dos claves son del mismo modo no cuesta nada y te salva la tarde.
8. Detectar los casos borde que nadie testea
Prompt:
Revisa mi integración de Stripe buscando los casos borde que NO están
cubiertos por tests:
[PEGAR TODOS LOS FICHEROS DE LA INTEGRACIÓN: handlers, servicios, jobs]
Genera una lista de escenarios que podrían fallar:
1. Interrupción a mitad: el servidor se cae entre "cargo traducido" y
"webhook persistido". ¿Qué queda inconsistente?
2. Webhooks que llegan duplicados, fuera de orden (un updated antes que
un created) y con retraso de horas.
3. Relojes: el pago se hace a las 23:59:59 UTC del último día del mes. ¿La
periodo de prueba o la renovación se calculan bien?
4. Multi-tenant: dos clientes distintos, un bug de caching que mezcla sus
datos.
5. Cambios de plan a mitad de ciclo con prorrateo: ¿la factura queda
correcta en los 4 casos posibles?
6. Cambios de tarjeta de pago: tarjeta caduca, tarjeta pasa a fallar.
7. Reembolsos parciales y múltiplos sobre la misma subscription.
8. Timezones: el cliente cambia de país a mitad de suscripción y hay
impuestos distintos.
9. Límites: un cliente con 50 active subscriptions, o un payment intent
de 100.000 €. ¿Hay rate limits de Stripe que pueda alcanzar?
10. El job de reintento: ¿qué pasa si reintenta 3 veces y sigue fallando?
Para cada escenario: probabilidad (alta/media/baja), impacto, y el test
mínimo que lo cubriría.
Prioriza por impacto x probabilidad. Dame los 5 primeros para hacer hoy.
Por qué funciona: esto es razonamiento sobre tu código, no conocimiento genérico. El modelo puede ver que tu handler guarda el estado antes de verificar el idempotency key, y eso es un escenario de fallo concreto con nombre.
9. Generar el dataset de pruebas para escenarios reales
Prompt:
Necesito datos de prueba realistas para mi suite de Stripe. Estos son los
escenarios de negocio de mi app:
[PEGAR DESCRIPCIÓN DEL MODELO DE NEGOCIO: qué cobra, a quién, qué
incluye, cuándo]
Genera:
1. Un catálogo de casos de prueba con: nombre, configuración de
suscripción, serie de eventos esperados con su orden, y aserción final.
2. Los payloads JSON de ejemplo de cada webhook necesario, en el formato
real de Stripe 2026 (no simplificado: incluye todos los campos que
Stripe manda de verdad, para que la validación de firma y el parsing
sean realistas).
3. Casos que cruzan concerns: un cliente que hace upgrade, falla el pago,
se actualiza la tarjeta, reintenta y termina con downgrade. La
secuencia completa de eventos.
4. Un caso de "cliente hostil": intenta explotar el flujo de
trials (múltiples tarjetas, cambio de email, resubscribe). ¿Qué
controles necesita mi app?
Los payloads deben poder usarse directamente con stripe.webhooks.
constructEvent usando el secreto de test.
Por qué funciona: los payloads reales son la diferencia entre un test que valida tu lógica y uno que valida un mock. Si tu test usa un JSON inventado con tres campos, no estás probando la integración: estás probando tu parser con tu propia imaginación.
10. Testear en CI sin tocar producción
Prompt:
Quiero una integración de Stripe que se pueda testear en CI sin dinero
real y sin configuración manual:
CI: [GITHUB ACTIONS]
ENTORNO: [staging con base de datos propia]
STRIPE: cuenta de test, [¿o de live con datos de test?]
Genera:
1. workflow de CI con un job de pagos que:
- use la API key de test desde secrets
- levante Postgres de test
- ejecute migraciones
- corra solo los tests marcados con @payments, en paralelo pero con un
lock para no saturar el rate limit de Stripe (Stripe test mode tiene
límites más bajos que el real)
- no toque nunca una base de datos de staging
2. Un job de "smoke test" que se ejecuta contra staging con la key de
test: crea un producto de 1 céntimo, hace un checkout real con la
tarjeta de test y verifica que el webhook llegó y el estado cambió.
Esto valida la configuración de red (URL pública, TLS, secretos).
3. Guards que impidan que una key de live llegue a un entorno de test:
- assert en el arranque de la app
- job de CI que grep -a sk_live_ y falla
- un script que verifique que STRIPE_SECRET_KEY y
STRIPE_PUBLISHABLE_KEY son del mismo modo
4. Un informe de qué parte de la integración está verificada
AUTOMÁTICAMENTE y qué parte sigue siendo "mirar el dashboard a mano".
Quiero ver los huecos.
5. El rate limiting: cuántos requests puedo hacer por minuto en test mode
sin que Stripe me corte, y cómo implementar backoff exponencial con
jitter en el cliente de Stripe.
Concreto: quiero poder hacer push y tener un indicador verde/rojo sobre
la salud de los pagos.
Por qué funciona: el smoke test del punto 2 es lo que detecta los problemas de infraestructura (webhook URL mal configurada, firewall, certificado) que ninguna prueba unitaria pilla. Y los guards del punto 3 son la diferencia entre un incidente y un susto cuando alguien pega la clave equivocada en un .env.
Eventos, cuándo ocurren y qué hacer con ellos
Esta tabla es la que más veces tengo delante cuando algo no cuadra.
| Evento | Cuándo ocurre | Qué hacer | Trampa |
|---|---|---|---|
checkout.session.completed | Compra finalizada | Activar acceso, guardar customerId | No es prueba de pago si usas async |
payment_intent.succeeded | Cargo OK | Marcar pagado | Puede llegar antes que checkout.session.completed |
payment_intent.payment_failed | Cargo rechazado | Liberar reserva, avisar | Se reintenta, no marcar cancelado aún |
invoice.payment_succeeded | Renovación cobrada | Renovar periodo | En trial NO pasa; el alta se dispara en otro sitio |
invoice.payment_failed | Renovación fallida | Avisar, no quitar acceso aún | Stripe reintenta durante dunning |
customer.subscription.updated | Cambio de plan o estado | Sincronizar periodo y plan | Llega MUCHAS veces por un solo cambio |
customer.subscription.deleted | Cancelada o expirada | Quitar acceso | No distinguir por qué se canceló |
customer.subscription.trial_will_end | 3 días antes de acabar el trial | Emails de conversión | Configurable, no viene por defecto |
charge.refunded | Reembolso | Ajustar saldo | Asumir que es total rompe con parciales |
charge.dispute.created | Chargeback abierto | Congelar, guardar evidencia | Llega semanas después |
El orden de los eventos es la fuente número uno de bugs. payment_intent.succeeded puede llegar antes que checkout.session.completed. Si tu handler asume el orden contrario, la race condition te va a comer un cliente.
Anti-patrones en testing de pagos
❌ Procesar el efecto antes de responder
// ❌ Si Stripe no recibe el 2xx en 20 s, reintenta y cobro dos veces
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
await cobrarAlCliente(event); // ← lento, puede fallar
await enviarEmail(event);
res.json({ received: true });
});
// ✅ Responde rápido, procesa después
app.post('/webhooks/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
await guardarEventoPendiente(event); // rápido, transaccional
res.json({ received: true });
procesarEnSegundoPlano(event.id); // idempotente, reintentable
});
❌ Idempotencia solo en memoria
// ❌ Se pierde en cada despliegue y no cubre la carrera
const procesados = new Set<string>();
if (procesados.has(event.id)) return res.json({ received: true });
// ✅ Índice único en base de datos: es la única que aguanta la carrera
await db.webhookEvent.create({
data: { eventId: event.id, type: event.type },
}); // si choca con el unique → duplicado → 200 y listo
❌ Ignorar el timestamp de la firma
// ❌ Un atacante puede reenviar un webhook viejo firmado
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
// ✅ constructEvent ya valida la ventana de 5 minutos,
// pero verifícalo: si desactivas el timestamp, estás expuesto.
const event = stripe.webhooks.constructEvent(req.body, sig, secret, {
tolerance: 300,
});
❌ Asumir que el evento llega en orden
// ❌ Si llega updated antes que created, el estado queda roto
if (event.type === 'customer.subscription.created') crearAcceso(id);
if (event.type === 'customer.subscription.updated') actualizarAcceso(id);
// ✅ Sincroniza contra el estado real de Stripe, no contra el evento
async function sincronizar(subscriptionId: string) {
const sub = await stripe.subscriptions.retrieve(subscriptionId);
await upsertEstadoDesdeStripe(sub); // idempotente por diseño
}
❌ No probar los fallos porque “en test mode no falla”
Las tarjetas de test existen precisamente para eso: 4000000000000002 siempre se rechaza. Un flujo de pago sin testear el rechazo es un flujo sin testear.
Errores frecuentes y troubleshooting
Los webhooks dejan de llegar de golpe
Comprueba en orden: el endpoint devuelve 2xx rápido, la firma es válida, la URL pública resuelve con TLS válido, y el secreto del webhook es el de ese endpoint concreto (Stripe tiene un secreto distinto por endpoint y por entorno). En local, stripe listen --forward-to y mira la salida: si no aparece event.id, el problema es de red, no de código.
El mismo evento se procesa tres veces
No tienes idempotencia, o la tienes en memoria. Y asegúrate de devolver 200 al duplicado: si el INSERT choca con el índice único, captura el error de restricción y responde 200, no 500. Un 500 hace que Stripe reintente con backoff durante horas.
El usuario pagó pero no tiene acceso
Mismo evento, dos códigos distintos. Comprueba si el payment_intent.succeeded llegó a tu DB (tabla de eventos) pero no llegó al handler de activación. Suele ser un switch sin ese caso, un error de tipografía en el nombre del tipo, o una excepción no capturada en la activación que se pierde en el log. Con los mejores prompts para depurar código puedes pegar el log completo y que te diga dónde se rompe.
El trial no se cobra
invoice.payment_succeeded no se dispara al terminar el trial si el primer cobro lo dispara otro evento. Comprueba que tienes customer.subscription.updated con billing_reason: subscription_create y que tu payment_behavior es el correcto. Y recuerda: el trial no crea invoice hasta que termina, así que si estás escuchando invoice.created para activar acceso, nunca llega.
Se pierde el 30% de los eventos en el entorno de test
No es un fallo de Stripe: es el límite de requests del cliente de Stripe configurado en tu código. Si creas un cliente por request estás abriendo una conexión nueva cada vez. Reutiliza una instancia global y ponle maxNetworkRetries: 2. Cuando los reintentos son de pago, la guía de error 429 con backoff exponencial te viene de perla: reutiliza una instancia global con maxNetworkRetries: 2 y no abras una conexión por request.
“Funciona en local, falla en CI”
Casi siempre es la clave: local tienes sk_test_ en el .env y CI usa un secret que nadie ha puesto. Segundo motivo: la red. CI a veces no resuelve bien hacia dominios externos en runners efímeros. Tercero: los tests corren en paralelo y varios tests comparten un customer de test, se pisan. Aísla con un customer por worker, no por test suite.
Quiero verificar una migración de hace 3 meses
stripe list events --customer cus_xxx --created gte=1750000000 te da el histórico real. Con eso puedes hacer un backfill: reprocesa todos los eventos del pasado por tu handler y mira si el estado final cambia. Si cambia, tenías un bug de idempotencia o de orden.
El flujo completo de testing de pagos
┌──────────────────────────────────────────────────────────┐
│ FIXTURES (prompt 1) │
│ Customer, precios, tarjetas de test, catálogo │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ CAPA 1 — Determinista (sin red) │
│ • firma del webhook (prompt 2) │
│ • idempotencia (prompt 3) │
│ • transiciones (prompt 5) │
│ • reembolsos (prompt 6) │
│ Usan payloads firmados con el secreto de test │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ CAPA 2 — Integración (Stripe test mode, red real) │
│ • checkout E2E (prompt 7) │
│ • eventos reales (prompt 4) │
│ • test clocks (prompt 5) │
│ Coste: 0 €. Dinero real: 0. │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ CAPA 3 — Smoke en staging (prompt 10) │
│ Compra de 1 céntimo con tarjeta de test │
│ Valida webhooks públicos, TLS, secretos │
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ CAPA 4 — Backfill (una vez, manual) │
│ Replay de eventos históricos para detectar divergencia│
└───────────────────────┬──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ AUDITORÍA (prompt 8) — ¿qué NO está cubierto? │
│ Los huecos que quedan son tu lista de trabajo real │
└──────────────────────────────────────────────────────────┘
La capa 4 es la que nadie hace y la que más información da. Reprocesar tus propios eventos históricos contra el handler actual te dice, en una tarde, cuántos clientes tienen el estado mal. Y si el número es cero, tu idempotencia funciona.
Conclusión
Testear pagos no es una WTF si separas las tres cosas que se mezclan en la cabeza: tu lógica (determinista, gratis, en la capa 1), la integración (gratis con Stripe test mode y test clocks, en la capa 2) y la infraestructura (un smoke test de staging, en la capa 3). Ninguna requiere un céntimo real.
Los prompts cubren el espectro entero: montar el harness, verificar firmas, garantizar idempotencia con índice único, cubrir los cuatro estados de una suscripción, los reembolsos parciales que nadie espera, las disputas que llegan semanas después y los casos borde que se te habrían olvidado. Y al final, el prompt 8 te dice qué queda sin cubrir, que es la información más valiosa de todas.
Si tu integración actual ya funciona y lo que te preocupa es el handler en sí, la guía de webhooks con Next.js tiene la implementación base con verificación de firma y parsing de eventos. A partir de ahí, estos prompts la convierten en algo que se puede testear.
Y si además de pagos tienes una parte de IA que consume el mismo dinero, dos cosas se cruzan: el metering (cobrar por uso) y el error de coste. Ahí el prompt 9 del primer artículo, el de coste por caso, te dice si estás ganando o perdiendo por cada usuario.
Para estructurar cualquiera de estos prompts antes de pegarlos en tu herramienta, el generador de prompts te ahorra el trabajo.
Un sistema de pagos que solo has probado en el camino feliz no está probado. Está esperando el primer webhook duplicado de las tres de la mañana. Los test clocks están ahí para que no tengas que esperarlo.