@startuml uml-sequence-onboarding-prestador !include _style.iuml title 8. Onboarding de prestador con código de invitación ' Fuente: apps/web/src/app/api/v1/mobile/invitations/validate/route.ts:1-43 ' Fuente: apps/web/src/app/api/v1/mobile/register/route.ts:1-82 ' Fuente: 00.sql invitaciones_prestadores ' Fuente: apps/web/src/lib/api/handler.ts autoactivate on actor "RRHH" as RH actor "Prestador" as Prov box "Web Admin" #F0F0F0 participant "Workspace RRHH\n(invitaciones)" as Adm end box box "Email" #FFFFFF participant "Resend" as Mail end box box "Mobile Provider (Expo)" #F0F0F0 participant "register/index.tsx" as App end box box "API REST Next.js" #F5F5F5 boundary "POST /invitations/validate" as RVal boundary "POST /mobile/register" as RReg end box box "Persistencia Postgres" #EAEAEA database "invitaciones_\nprestadores" as DBInv database "usuarios" as DBU database "user_credentials" as DBC end box == Generación de invitación == RH -> Adm ++ : Emite invitación (email destino) Adm -> DBInv ++ : INSERT invitaciones_prestadores\n(codigo, expira_en, notas) DBInv --> Adm -- : codigo Adm ->> Mail : Send email "Tu código: ABC123" Adm --> RH -- : "Invitación enviada" Mail ->> Prov : Inbox: código + deep link == Validación previa al registro == Prov -> App ++ : Abre app + ingresa código App ->> RVal ++ : POST /invitations/validate {codigo} note right of RVal validate/route.ts:15-42 auth='public', sin marcar usado. Solo lectura idempotente. end note RVal -> DBInv ++ : SELECT id, expira_en,\nusado_en, revocada_en DBInv --> RVal -- : invitacion RVal -> RVal : checks: !revocada,\n!usada, !expirada alt código válido RVal --> App -- : 200 {valido:true,\nexpira_en, notas} App --> Prov -- : Avanza a formulario else código inválido RVal --> App : 400 BadRequest\n("revocado/expirado/usado") App --> Prov : Mensaje de error end == Registro transaccional == Prov -> App ++ : Completa nombre, email,\npassword, código App ->> RReg ++ : POST /mobile/register {rol:'prestador',\ncodigo_invitacion, ...} RReg -> DBU ++ : SELECT id WHERE email=$1 DBU --> RReg -- : null (no existe) RReg -> RReg : bcrypt.hash(password, 12) note over RReg #FEF3C7 register/route.ts:29-77 withTransaction garantiza atomicidad de los 3 INSERT/UPDATE. end note RReg -> DBInv ++ : BEGIN\nSELECT FOR UPDATE\nWHERE codigo = $1 DBInv --> RReg -- : invitacion (lock) RReg -> RReg : revalida: !revocada,\n!usada, !expirada RReg -> DBU ++ : INSERT usuarios\n(rol='prestador', activo=true) DBU --> RReg -- : nuevo.id RReg -> DBC ++ : INSERT user_credentials\n(user_id, password_hash) DBC --> RReg -- : user_id RReg -> DBInv ++ : UPDATE invitaciones\nSET usado_en = NOW() DBInv --> RReg -- : COMMIT RReg -> RReg : jwt.sign({sub, rol}) RReg --> App -- : 201 {data:{id}} App --> Prov -- : Sesión activa, onboarding\nde documentos en background footer Fuente: apps/web/src/app/api/v1/mobile/invitations/validate/route.ts | apps/web/src/app/api/v1/mobile/register/route.ts | 00.sql invitaciones_prestadores @enduml