Stripe en Laravel. Modele el pago, no la llamada.
Un pago es una máquina de estados con dos eventos que mueven dinero, no un booleano en el pedido. Qué implica para su esquema, dónde deja de ayudar Cashier y qué columnas necesita el primer día.
La mayoría de integraciones con Stripe empiezan con una llamada al SDK y una
columna llamada paid. Funcionan unos cuatro meses, que es más o menos lo que
tarda en llegar la primera devolución parcial, el primer cargo disputado, o el
primer pedido en el que el banco del cliente pidió autenticación y el cliente no
volvió nunca.
El problema no es la API. Es que un pago se modeló como algo que ocurrió, cuando es algo que todavía está ocurriendo.
Lo que está integrando es una máquina de estados
Un PaymentIntent recorre estados: necesita un método de pago, necesita una acción del cliente, se está procesando, ha tenido éxito, se ha cancelado. Varias de esas transiciones las dirige un banco y no su código, y algunas tardan minutos.
De ahí sale una consecuencia directa para su esquema, y es el artículo entero en
una frase: su fila de pago guarda un estado, no una bandera. Si la columna
es un booleano, entonces "el cliente está ahora mismo en la pantalla de 3D
Secure", "el banco lo rechazó" y "el dinero está autorizado pero no cobrado"
colapsan todos en false, y su equipo de soporte no puede distinguirlos.
Modele los estados sobre los que realmente actúa. La mayoría de negocios necesitan cinco o seis: requiere acción, procesando, autorizado, capturado, fallido, cancelado. Déle un enum de verdad y haga explícitas las transiciones: el sentido de una columna de estado es que los movimientos ilegales se rechacen, no que se sobrescriba una cadena.
Autorizar y capturar son dos eventos
Un pago con tarjeta puede reservar dinero sin cobrarlo. La autorización retiene
los fondos unos días, y la captura es el acto separado de cobrarlos de verdad.
Stripe lo expone como capture_method: manual.
Quien envía mercancía lo necesita, porque cobrar por algo que aún no ha enviado es una devolución en camino, y en algunas jurisdicciones además un problema regulatorio. Quien vende un servicio con señal lo necesita. Quien monta un marketplace lo necesita.
Para el esquema significa que el importe autorizado y el capturado son columnas
distintas, y muy a menudo números distintos. Autoriza la cesta completa, luego
un artículo se queda sin stock, luego captura menos. Un modelo de pedido con un
solo amount no puede expresarlo, y añadirlo después es una migración sobre
todas las filas históricas mientras el equipo financiero espera.
Además: una autorización caduca. Si nada la captura dentro de la ventana, la retención se libera y el dinero queda fuera de su alcance. Eso es un trabajo programado que busca autorizaciones a punto de expirar, y Stripe no se lo va a recordar.
Guardar una tarjeta no es guardar una tarjeta
Cuando un cliente marca "recordar mi tarjeta", usted no guarda nada. Crea el registro de un permiso, y ese permiso tiene condiciones: por qué puede cobrar, si el cliente tiene que estar presente, y si su banco querrá autenticación otra vez.
Stripe separa esto en SetupIntent para recoger el permiso y cargos off_session
para usarlo. La distinción importa porque un cargo fuera de sesión puede fallar
pidiendo autenticación, y no hay ningún cliente ahí para autenticarse. Su código
tiene que manejar un pago que falló sin que sea culpa de nadie y que se arregla
enviándole al cliente un enlace por correo.
Aquí también aterrizan las reglas europeas. Bajo SCA, un cargo sin el cliente presente tiene que caer en una exención o llevar un acuerdo previo, lo que en la práctica significa que el mandato se configuró bien en el momento de guardar la tarjeta. Equivocarse en eso es invisible hasta que la tasa de fallo de sus renovaciones es del quince por ciento y nadie sabe por qué.
Dónde deja de ayudar Cashier
Cashier es buen software y es una librería de suscripciones. Si vende planes con opción mensual y anual, trae el ciclo de vida, la aritmética del prorrateo, los periodos de gracia y los registros de factura, y escribir eso a mano es un mes perdido.
No cubre pagos únicos con captura manual, repartos de marketplace, liquidación entre varias partes, ni un checkout cuyo importe se calcula a partir de una cesta que cambia. Eso es el SDK directamente, y es lo esperable, no un fallo de la librería.
El error que conviene evitar es tomar las tablas de Cashier como su modelo de pagos. Describen suscripciones. Sus pedidos, sus capturas, sus devoluciones y sus comisiones son suyos, y tienen que existir haya o no una suscripción de por medio.
La fila que de verdad necesita
Como mínimo, por cada intento de pago:
- Su propio identificador y el del proveedor, indexados.
- Estado, como enum, con la marca de tiempo de la última transición.
- Importe autorizado, capturado y devuelto - tres enteros en la unidad menor de la moneda, que es como debe guardarse siempre el dinero.
- El código de moneda, aparte.
- La comisión, en cuanto la conozca, porque sus ingresos no son lo que pagó el cliente.
- La clave de idempotencia que envió.
- Una clave foránea a aquello que esto paga.
Las dos últimas trabajan más de lo que parece. La clave de idempotencia es lo que impide que un trabajo reintentado cobre dos veces, y la cola que tiene debajo va a reintentar. Stripe acepta la clave en cada petición que modifica algo y devuelve la respuesta original en lugar de cobrar otra vez, pero solo si la envía, y solo si se deriva del intento en lugar de generarse de nuevo.
Qué construir primero
Construya la máquina de estados y el receptor de webhooks antes que la pantalla de checkout. La pantalla es una tarde; los estados son el sistema. Un checkout que se ve perfecto y da por bueno el éxito desde la vuelta del redirect es la versión que pierde pedidos en silencio, porque el redirect no es lo que le dice que un pago funcionó.
Después concilie. Una vez al día, pida a Stripe todo lo que haya cambiado y compárelo con sus filas. No porque los webhooks sean poco fiables, sino porque quiere enterarse cuando algo se desvía, y la única alternativa es que se lo cuente un cliente.
Hacemos este trabajo dentro de proyectos de comercio electrónico y por separado, y la primera pregunta es siempre la misma: ¿autorizar y capturar juntos, o por separado? La respuesta cambia el esquema, y es más fácil de contestar en la semana uno que en el mes seis.
