ADR-035: dos caminos para escribir la tarjeta en Stripe, con interruptor
Aceptado el 15/11/2026 (pedido de Andrés). Complementa el ADR-013: agrega un segundo camino de captura de la tarjeta, la página alojada por Stripe, junto al componente de Stripe que el 013 ya definía. La saga, el contrato de la capa y las llaves de `payment-service` no cambian.
Contexto
Andrés, al ver la página de pago publicada (15/11/2026): «el checkout se contradice. Estamos haciendo el formulario de pago nosotros, y el flujo debería ir a Stripe para que sea allá que se ingresa la tarjeta.» Y después: «importante mostrar los dos escenarios y que sea switcheable, para mostrar los dos caminos y aún así las tarjetas no pasan por nosotros.» Ya había fijado que el proyecto no se mete en nada que involucre PCI DSS (Payment Card Industry Data Security Standard, el estándar de seguridad de datos de tarjetas; aplica a quien captura, guarda o transmite números de tarjeta).
Con el ADR-013, la tienda montaba el componente de tarjeta de Stripe (Card Element) dentro de su página. El número va del navegador a Stripe por un marco servido por Stripe, y la tienda recibe solo el pm_.... A la vista, sin embargo, el formulario parece de la tienda. En modo simulado, la tienda mostraba además un campo propio de número de tarjeta.
Restricción: no cambiar el contrato de la capa. payment-service cobra con POST /v1/payment_intents, payment_method = payment.token y confirm=true, sin customer.
Opciones
| Opción | Cómo | Contrato de la capa | Qué cuesta |
|---|---|---|---|
| A. Componente de Stripe (incrustado, el del 013) | Marco de Stripe (js.stripe.com) dentro de la página de pago; Stripe.js devuelve el pm_... |
Igual | El campo se ve dentro de la tienda, aunque lo sirve Stripe |
B. Checkout en modo setup (alojado) |
La tienda manda al comprador a la página de Stripe, que solo guarda el medio de pago; a la vuelta, el Worker lee la sesión y toma el pm_... de su SetupIntent |
Igual: payment.token = pm_... |
Un salto fuera del sitio; una llave restringida en el Worker |
C. Checkout en modo payment con captura manual |
Stripe autoriza en su página y la capa captura o cancela | Cambia: payment.token pasa a ser un PaymentIntent autorizado y payment-service deja de crear el cobro |
Cambio de contrato y de servicio; otro carril |
Verificado en la documentación vigente de Stripe (15/11/2026, docs.stripe.com):
customer_creationse puede fijar ensetup. Conif_requiredsolo se crea un cliente cuando la sesión lo exige ("onlysubscriptionmode Sessions andpaymentmode Sessions with post-purchase invoices"). Ensetup, sincustomer, el medio de pago queda sin cliente.- Al crear un PaymentIntent,
customersolo es obligatorio si el medio de pago está asociado a un cliente ("If the payment method is attached to a Customer, you must also provide the ID of that Customer").payment-servicepuede cobrarlo como hoy. success_urladmite{CHECKOUT_SESSION_ID}; la sesión exponesetup_intenty este,payment_method.- En
setup,currencyes obligatoria si no se fijan los tipos de medio de pago; la tienda fijacard.
Supuesto (sin confirmar en la documentación): un medio de pago sin cliente, guardado por una sesión setup, se puede cobrar al menos una vez. Si admite más de un cobro no importa: la saga cobra una sola vez por orden. Si la prueba de punta a punta de Andrés lo desmiente, el camino alojado se desactiva quitando la llave restringida y queda el incrustado, sin tocar código.
Decisión
Quedan las opciones A y B, con un interruptor. La C se descarta: cambia el contrato.
| Incrustado (A) | Alojado (B) | |
|---|---|---|
| Dónde se escribe la tarjeta | En el marco de Stripe dentro de la página de pago | En checkout.stripe.com |
| Qué recibe la tienda | pm_..., del navegador (Stripe.js) |
pm_..., que el Worker lee de la sesión |
| Llave del Worker | STRIPE_PUBLISHABLE_KEY (pk_test_, pública) |
STRIPE_SECRET_KEY (rk_test_, restringida) |
| Orden en la capa | POST /v1/orders con payment.token = pm_... |
Igual |
Flujo alojado:
- El comprador escribe sus datos de entrega y pulsa "Continuar a Stripe". Los datos quedan en su pestaña: no viajan a Stripe ni se guardan en el Worker.
- El Worker crea una sesión de Checkout en modo
setup(payment_method_types=card,client_reference_id= llave de idempotencia de la compra, sincustomer), con esa llave comoIdempotency-Keyde Stripe. - Stripe captura la tarjeta y vuelve a
/pago/vuelta?session_id=...; cancelar vuelve a/carrito?pago=cancelado, sin orden. - El Worker lee la sesión y exige
status=complete, el SetupIntentsucceededy que la sesión sea de esta compra. Crea la orden conpayment.token = pm_...y laIdempotency-Keyde siempre. - Volver dos veces o recargar no crea otra orden: la sesión queda ligada a su orden en
ORDERS(sin datos personales) y la capa deduplica por la llave.
Interruptor:
- Un camino se ofrece solo si su llave está cargada y es de prueba.
GET /api/configdevuelve los caminos disponibles (flows) y el de entrada (flow). PAYMENT_FLOW(variable del Worker,hostedpor defecto, oembedded) elige el de entrada cuando están los dos. Se cambia sin construir la tienda.- La página de pago tiene un selector "Dónde escribir la tarjeta" para mostrar los dos en el demo. La elección queda en la dirección (
?camino=). - Sin ninguna llave, la pasarela simulada (
/pasarela-simulada): rotulada, sin campo de tarjeta, con dos botones que devuelventok_sim_aprobadaotok_sim_rechazada, como antes.
Cada camino dice en la página, en una frase, dónde se escribe la tarjeta y quién la recibe.
Llave del camino alojado
Restringida y de prueba (rk_test_), con solo:
| Permiso de Stripe | Nivel |
|---|---|
| Checkout Sessions | Write |
| SetupIntents | Read |
Todo lo demás en None. Va como secreto STRIPE_SECRET_KEY del Worker, nunca en el repositorio. La crea y la carga Andrés (docs/demo/12-pago-con-stripe-dos-caminos.md). No sirve la llave de acceso total ni la de payment-service.
Qué se sacrifica
- Incrustado: el campo se ve dentro de la tienda (lo sirve Stripe, pero a la vista parece propio) y Stripe.js se carga en la página de pago. Se carga solo cuando el comprador elige ese camino.
- Alojado: un salto fuera del sitio y menos control visual (la página es de Stripe). Además, una segunda llave secreta fuera de
payment-service, acotada: no puede cobrar ni reembolsar, solo crear y leer sesiones. La regla del ADR-013 queda así: la llave que cobra es solo depayment-service. - Alojado: el medio de pago queda guardado en la sandbox de Stripe sin cliente. Es un recurso de prueba en Stripe, nunca en la tienda ni en la capa.
- Dos caminos son más código y más pruebas que uno. Se aceptan porque el demo los compara.
Por qué ninguno trae la tienda ni la capa al alcance de PCI DSS
En los dos, la tarjeta se escribe en un elemento servido por Stripe (su página o su marco) y va directo a Stripe. La tienda, su Worker y la capa solo ven el pm_.... Es el modelo de integración de Stripe para quien no quiere tocar datos de tarjeta. La forma exacta en que cada uno lo declararía ante un auditor no se evaluó: no aplica a un demo sin cobro real.
Cómo se verifica
apps/storefront/tests/unit/checkout.test.ts, contra un Stripe y una capa simulados. Interruptor: sin llaves, simulada; con las dos, alojado por defecto yPAYMENT_FLOWelige; sin la llave restringida, el alojado no se ofrece ni se abre a mano; una llave viva no habilita nada. Sesión: modosetup, sin cliente, sin datos personales. La vuelta crea la orden conpm_.... Volver dos veces da una sola orden. Una sesión abierta o de otra compra no crea orden.apps/storefront/tests/e2e/t8-pago-tarjeta.spec.ts, contratests/mock-stripe/server.mjsy un Stripe.js falso servido en lugar dejs.stripe.com, recorre los dos caminos con la tarjeta de prueba. Ninguna petición del navegador hacia la tienda lleva el número, ningún cuerpo que recibe la capa lo lleva, y el registro del Worker tampoco. "Stripe" sí lo recibe.apps/storefront/tests/e2e/t3-compra.spec.ts: compra, rechazo y casos de error por la pasarela simulada. Cancelar vuelve al carrito sin orden; volver a aprobar o recargar lleva a la misma orden; la página de pago no tiene campo de tarjeta propio.- La compra de punta a punta con Stripe en el dominio publicado la hace Andrés, por los dos caminos. El agente no escribe tarjetas, ni de prueba, en un sitio público.