@startuml uml-sequence-reembolso-sod !include _style.iuml title 4. Reembolso con segregación de funciones (SoD) ' Fuente: apps/web/src/lib/dashboard/actions/reembolsos.ts:13-120 ' Fuente: 00.sql:565-576 (tabla reembolsos) ' Fuente: migrations/004_rbac_permisos_zero_trust.sql (permisos finanzas) ' Fuente: 00.sql logs_auditoria (logAccion) autoactivate on actor "Cliente" as Cli actor "Finanzas A\n(aprobador)" as FA actor "Finanzas B\n(procesador)" as FB box "Web Admin (Next.js)" #F0F0F0 participant "Workspace\nFinanzas" as UI end box box "Server Actions" #F5F5F5 control "aprobarReembolso()" as ActAp control "marcarReembolsoProcesado()" as ActPr control "requirePermiso\n('reembolsos.aprobar')" as Gate end box box "Persistencia Postgres" #EAEAEA database "reembolsos" as DBR database "logs_auditoria" as DBLog end box == Solicitud (cliente) == Cli -> UI ++ : Abre ticket "reembolso" UI -> DBR ++ : INSERT reembolsos\n(estatus = 'solicitado') DBR --> UI -- : reembolso_id UI --> Cli -- : "Solicitud recibida" == Fase 1: aprobación (Finanzas A) == FA -> UI ++ : Click "Aprobar" UI -> ActAp ++ : aprobarReembolso(id) ActAp -> Gate ++ : requirePermiso('reembolsos.aprobar') Gate --> ActAp -- : viewer (userId = A) ActAp -> DBR ++ : SELECT FOR UPDATE\nestatus = 'solicitado' DBR --> ActAp -- : ok ActAp -> DBR ++ : UPDATE estatus='aprobado'\naprobado_por = A DBR --> ActAp -- ActAp -> DBLog ++ : logAccion('reembolso.aprobado') DBLog --> ActAp -- ActAp --> UI -- : {ok:true} UI --> FA -- : "Reembolso aprobado" == Fase 2: procesamiento (debe ser distinto) == FB -> UI ++ : Click "Marcar procesado"\nreferencia="STRIPE-..." UI -> ActPr ++ : marcarReembolsoProcesado(id, ref) ActPr -> Gate ++ : requirePermiso('reembolsos.aprobar') Gate --> ActPr -- : viewer (userId = B) ActPr -> DBR ++ : SELECT FOR UPDATE\nestatus, aprobado_por DBR --> ActPr -- : {estatus:'aprobado', aprobado_por:A} alt aprobador != procesador ActPr -> ActPr : check viewer.userId !== aprobado_por note right of ActPr reembolsos.ts:97-103 Guard SoD a nivel server action. end note ActPr -> DBR ++ : UPDATE estatus='procesado'\nreferencia_pasarela=ref DBR --> ActPr -- ActPr -> DBLog ++ : logAccion('reembolso.procesado') DBLog --> ActPr -- ActPr --> UI -- : {ok:true} UI --> FB -- : "Procesado correctamente" else mismo usuario ActPr --> UI : {ok:false,\nmessage:'Segregación de funciones...'} note right of UI #FEF3C7 reembolsos.ts:97-103 Bloquea explícitamente que A cierre el ciclo que A aprobó. end note UI --> FB : Error visible end footer Fuente: apps/web/src/lib/dashboard/actions/reembolsos.ts | 00.sql reembolsos | migrations/004_rbac_permisos_zero_trust.sql @enduml