@startuml uml-activity-procesamiento-pago-facturacion !include _style.iuml scale max 2700 height title 2. Procesamiento de pago (Stripe real) y facturacion CFDI 4.0 start partition "Cliente (movil)" { :Confirmar orden; :Abrir PaymentScreen; :POST /api/v1/mobile/payments/intent\n(orden_id); } partition "API web (Vercel)" { :defineEndpoint requireSession; :SELECT orden + monto; if (orden valida\ny no pagada?) then (si) :ensureStripeCustomer\n(crea o reusa cus_...); :stripe.paymentIntents.create\n(idempotencyKey =\norden-{id}-{pago}); :INSERT pagos\n(estatus='pendiente',\npayment_intent_id,\nraw_status); :stripe.ephemeralKeys.create; :Devolver { client_secret,\ncustomer_id, ephemeral_key,\npublishable_key }; else (no) :Rechazar 409 / 422 / 404; stop endif } partition "Cliente (movil)" { :StripeProvider.initPaymentSheet; :StripeProvider.presentPaymentSheet; :Cliente ingresa tarjeta\n(Stripe maneja UI PCI-compliant); } partition "Stripe (api.stripe.com)" { :Confirmar PaymentIntent\ncon tarjeta; if (3DS requerido?) then (si) :Mostrar challenge; endif if (cargo aprobado?) then (si) :status = "succeeded"; :Disparar webhook\npayment_intent.succeeded; else (no) :status = "requires_payment_method"; :Disparar webhook\npayment_intent.payment_failed; endif } partition "Cliente (movil)" { :Recibe completed o failed\nde PaymentSheet; if (success?) then (si) :Navega a /payment/success; else (no) :Mostrar mensaje de error,\npermitir reintento; stop endif } partition "API web — webhook receiver" { :POST /api/v1/webhooks/stripe; :verifyAndConstructEvent\n(stripe-signature, whsec); if (firma valida?) then (si) else (no) :Responder 400; stop endif :INSERT webhook_events\n(proveedor, external_id)\nON CONFLICT DO NOTHING; if (evento ya procesado?) then (si) :Responder 200 duplicate; stop else (no) switch (event.type) case (succeeded) :UPDATE pagos\nSET estatus='procesado',\nraw_status, metodo,\nreferencia_pasarela,\nwebhook_received_at; case (failed | canceled) :UPDATE pagos\nSET estatus='fallido'; case (refunded) :UPDATE pagos\nSET estatus='reembolsado'; case (otro) :Marcar 'unhandled'; endswitch :UPDATE webhook_events\nSET processed_at; endif } if (pago procesado?) then (si) partition "lib/billing — Phase 5 (pendiente)" { :emitirFacturaSimplificada\n(pago_id) async; :INSERT facturas\n(estatus='pendiente_timbrado'); :PacAdapter.facturama.timbrar\n(CFDI 4.0 sandbox); if (PAC respondio UUID?) then (si) :UPDATE facturas\n(estatus='timbrada',\nuuid_cfdi, serie, folio,\nxml_contenido, pdf_contenido); :Email cliente con PDF/XML; else (no) :UPDATE facturas\n(estatus='fallida',\nwebhook_events.error); :queue.retryCron 15min\n(max 3 intentos); endif } stop else (no) partition "Soporte" { :Trigger crear ticket\ncategoria='cobro_incorrecto'; :Auto-asignar grupo finanzas_l2; :Notificar cliente; } stop endif footer Fuente: apps/web/src/app/api/v1/{mobile/payments,webhooks/stripe}/**, apps/web/src/lib/payments/**, migrations/007_pagos_facturas_extensions.sql, .planning/architecture-pagos-cfdi.md @enduml