Ir al contenido

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.

5 min de lectura

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.

Preguntas relacionadas

¿Cashier o el SDK de Stripe directamente?
Cashier si vende planes recurrentes con una forma razonablemente estándar, porque trae el ciclo de vida de la suscripción, el prorrateo y las facturas que usted escribiría mal. El SDK directamente para pagos únicos, marketplaces, liquidaciones repartidas o cualquier cosa donde autorice ahora y capture después. Mezclar ambos es normal y está bien.
¿No podemos guardar el objeto de Stripe y consultarlo cuando haga falta?
No, y este es el atajo más caro de estos proyectos. Stripe no es su base de datos: limita peticiones, se cae, y una página que muestra el historial de pedidos de un cliente no debería depender de ella. Mantenga su propia fila con su propio estado, trate a Stripe como el sistema de referencia del movimiento de dinero, y concilie ambos a conciencia.
¿Dónde guardamos el importe?
Un entero en la unidad menor de la moneda, más el código de moneda en una columna aparte, que es como lo maneja Stripe también. Nunca un decimal de coma flotante. Y tampoco un único importe: el importe capturado, el devuelto y la comisión son tres números distintos que divergen en la primera devolución parcial.
¿Necesitamos certificación PCI para esto?
No mientras los datos de la tarjeta no lleguen a su servidor, que es justo el objetivo de Stripe Elements o de una página de pago alojada. La tarjeta se tokeniza en el navegador contra Stripe directamente y su aplicación solo ve un identificador. En cuanto un número de tarjeta toca su aplicación, en una línea de log o en un campo de formulario, eso cambia por completo.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto