@startuml uml-sequence-cliente-paga-orden !include _style.iuml title 3. Secuencia — Cliente paga orden con Stripe PaymentSheet autoactivate on actor "Cliente" as USER box "App movil cliente" #F5F5F5 participant "PaymentScreen\n(Expo)" as APP participant "StripeProvider\n(@stripe/stripe-react-native)" as SP end box box "API web (Next.js / Vercel)" #F5F5F5 participant "POST /api/v1/mobile/\npayments/intent" as API participant "createPaymentIntent\nForOrder" as INT participant "ensureStripe\nCustomer" as ESC end box box "Postgres (Neon)" #F5F5F5 database "ordenes_servicio,\npagos, usuarios" as DB end box participant "Stripe API\n(api.stripe.com)" as STRIPE USER -> APP : Pagar orden APP -> API : POST { orden_id }\n(Bearer JWT) API -> INT : createPaymentIntentForOrder() INT -> DB : SELECT orden + estatus + monto_total DB --> INT : OrdenRow note right of INT Guards (apps/web/src/lib/payments/intent.ts:31-67): - Orden existe y pertenece al cliente - Estatus no en {cancelada, completada} - Monto >= 100 centavos end note INT -> DB : SELECT pago previo por orden_id DB --> INT : pago previo o null alt pago previo procesado INT --> API : ConflictError\n"Esta orden ya fue pagada" API --> APP : 409 else INT -> ESC : ensureStripeCustomer(cliente_id) ESC -> DB : busca customer_id_pasarela existente DB --> ESC : null o customer existente alt customer no existe ESC -> STRIPE : POST /v1/customers\n(email, nombre, telefono) STRIPE --> ESC : { id: "cus_..." } ESC -> DB : UPDATE pagos.customer_id_pasarela\npara este cliente end ESC --> INT : customer_id INT -> STRIPE : POST /v1/payment_intents\n(amount, currency=mxn,\nautomatic_payment_methods,\nidempotencyKey=orden-{id}-{pago}) STRIPE --> INT : { id: pi_..., client_secret, status: "requires_payment_method" } INT -> DB : INSERT pagos\n(estatus='pendiente',\npayment_intent_id, raw_status) DB --> INT : pago_id INT -> STRIPE : POST /v1/ephemeral_keys\n(customer) STRIPE --> INT : { secret: ek_... } INT --> API : { pago_id, payment_intent_id,\nclient_secret, customer_id, ephemeral_key } API --> APP : 200 { data } end APP -> SP : initPaymentSheet(\nclient_secret, customer_id,\nephemeralKey, publishableKey) SP --> APP : ok APP -> SP : presentPaymentSheet() SP -> STRIPE : confirma intent\n(client_secret, tarjeta del usuario) STRIPE --> SP : { status: "succeeded" } SP --> APP : completed APP --> USER : Navega a /payment/success note over APP, STRIPE El estado en DB se reconcilia via webhook (ver uml-sequence-stripe-webhook.puml). Hay ventana de 2-5 segundos entre exito UI y estatus='procesado' en DB. end note footer Fuente: apps/web/src/app/api/v1/mobile/payments/intent/route.ts, apps/web/src/lib/payments/{intent,customer,stripe}.ts, apps/mobile-client/app/payment/index.tsx, migrations/007_pagos_facturas_extensions.sql @enduml