# Runbook — Migração de colaboradores do tenant `exent` para o `vendrix` (mantendo o Exent Space)

> **Escopo:** transferir Raul Rodrigues e Vitor Freitas do Lago do tenant **exent** para o tenant **vendrix**, mantendo o envio de NFs e reembolsos pelo **Exent Space**.
>
> **Agnóstico de ambiente:** nenhum ID está fixado neste documento. Todos os valores (IDs de colaborador, `space_id`, IDs de token) são descobertos na **Fase 0** no próprio ambiente alvo (homologação e depois produção).
>
> **Documento irmão (lado Space):** [space-ajuste-multi-tenant-nexos.md](space-ajuste-multi-tenant-nexos.md) — as mudanças de código no Exent Space, que são **pré-requisito da Fase 4**.
>
> **Decisões de projeto:**
> - *(11/08/2026)* O Space passa a guardar, por usuário, o tenant Nexos alvo e a enviar o `X-Tenant-ID` correspondente.
> - *(11/08/2026)* O histórico financeiro (NFs, reembolsos, pagamentos, contratos) **permanece no exent**; os dois começam limpos no vendrix.
> - *(11/08/2026)* Os dois **não acessam o Nexos** (nenhum tenant) — interagem apenas pelo Space. Nenhum vínculo `tenant_user` deve ser criado.
> - *(12/08/2026)* Os e-mails **mudam de `@exent.com.br` para `@vendrix.com.br`** — ver Fase 2b.
> - *(12/08/2026)* O `exent_space_token` é **reutilizado**: o mesmo valor nos dois tenants. Não haverá token distinto por tenant (ver Fase 1).
> - *(12/08/2026)* No Space, o seletor é `users.nexos_tenant_id`; o `users.tenant_id` (tenant **local**) **não muda**, para os dois continuarem convivendo com os colegas no feed/calendário/academy.
> - *(12/08/2026, fim do dia)* **Não existe campo manual para "qual tenant do Nexos atende este e-mail".** A resolução é por **auto-descoberta** (`GET /api/integration/resolve-tenant`), e o desempate é o switch *Habilitar* da Integração Space na tela do colaborador, protegido por uma **trava de exclusividade** — ver a seção seguinte.
> - *(12/08/2026, fim do dia)* O campo novo na tela do colaborador é **"Workspace no Space"**, que é o **outro** eixo: com quem a pessoa convive no Space (feed/calendário/academy = `users.tenant_id` lá). Para Raul/Victor esse valor **não muda** — eles continuam no workspace da Exent.

---

## ⚠️ Avisos críticos (ler antes de executar)

> **Avisos 1 e 2 foram corrigidos no código** na branch `feat/migracao-colaboradores-tenant-space` (12/08/2026). Eles **continuam valendo em qualquer ambiente que ainda não tenha esse commit** — confirme o que está deployado no ambiente alvo antes de decidir. Avisos 3 e 4 seguem válidos em todos os ambientes.

1. **~~NUNCA desativar os colaboradores no exent pelo botão da UI.~~ [CORRIGIDO]** O `toggle_status` (`app/Http/Controllers/Core/CollaboratorController.php:518`) agora exige `space_id` além do e-mail antes de chamar `syncStatusToSpace()`. **Antes da correção**, ele disparava `POST /nexos/user-status {status: 0}` sempre que o colaborador tivesse e-mail, **desativando os usuários no Space**, onde eles devem continuar ativos.
   **Atenção — a correção não dispensa o SQL da Fase 4:** a Fase 4 zera `status` e `space_id` no mesmo `UPDATE`. Pela UI você desativaria com o `space_id` ainda preenchido, e o push aconteceria. Use SQL/tinker.
2. **~~NUNCA salvar credenciais do Space pelo formulário de Empresa do Master.~~ [CORRIGIDO]** `company_store`/`company_update` agora persistem `exent_space_url_api` (`app/Http/Controllers/MasterController.php:236` e `:602`), com validação em `:67` e `:482`. **Antes da correção**, o payload era montado só com `exent_space_url` + `exent_space_token`, **apagando `exent_space_url_api`** — e o formulário até exibia o campo pré-preenchido (`company_edit.blade.php:298`), o que tornava a armadilha fácil de acionar sem perceber. Isso matava em silêncio os pushes outbound (`profile-sync`/`user-status`).
   Em ambiente sem a correção, usar somente **Master > Credenciais** (`credentials_update`, `MasterController.php:1097-1139`, que faz `array_merge` preservando as chaves existentes).
3. **NUNCA mexer em membros de tenant pelo formulário de Empresa.** O `company_update` faz `$tenant->users()->sync()` destrutivo (`MasterController.php:574` — a lista postada vira a membresia completa; sem a chave `users` no request, o default `[]` **desassocia todos**) e revalida todas as credenciais. Ajustes em `tenant_user` são via SQL direto.
4. **Não existe UI para setar `core_collaborators.space_id`.** O JS de `#space_id_select` (`add.blade.php:540`, `edit.blade.php:720`) referencia elementos que não existem no markup — é código morto. O pareamento é via tinker (Fase 2) ou via `PUT /api/integration/collaborators/pair` chamado pelo Space. **Não confundir com o seletor "Workspace no Space"**, que é novo, funciona, e controla outra coisa (a convivência no Space, não o pareamento).

---

## Como a integração funciona (contexto mínimo)

- **Credenciais por tenant** (banco central): `IntegrationProvider(slug='exent_space')` → `IntegrationToken` (payload criptografado) → `company_token(tenant_id)`. Chaves do payload:
  - `exent_space_token` — bearer bidirecional (inbound e outbound);
  - `exent_space_url_api` — base da API do Space (pushes outbound);
  - `exent_space_url` — URL do front do Space (tag `{link_space_nf}` dos e-mails de cobrança de NF).
- **Inbound (Space → Nexos):** grupo `integration/*` em `routes/api.php:45-54`, middleware `integration.token` + `tenancy.header`. Exige `X-Tenant-ID` e `Authorization: Bearer` igual ao `exent_space_token` **do tenant indicado no header** (`app/Http/Middleware/IntegrationApiTokenMiddleware.php`, comparação com `hash_equals`). O colaborador é resolvido por `core_collaborators.space_id` **dentro do banco do tenant**.
  - Existe um espelho legado `finance/*` (`routes/api.php:25-31`) com autenticação fraca por domínio de e-mail (`google.domain` — o bearer **é** o e-mail). **Verificado: o Space não usa esse grupo**, apenas `integration/*`.
- **Resolução de tenant (Space → Nexos, sem `X-Tenant-ID`):** `GET /api/integration/resolve-tenant?email=` (`routes/api.php:48`, `CollabController.php:42`). É o **primeiro** endpoint do login do Space, chamado antes de ele saber o tenant; por isso usa `integration.token:any` (bearer válido de **qualquer** tenant) e **não** usa `tenancy.header`. Resolve procurando o colaborador com `status=1 AND exent_space_status=1` em cada tenant. Um único candidato → `resolved:true`. Mais de um → `ambiguous`, e **nenhum** tenant é devolvido.
- **Trava de exclusividade** (`app/Services/SpaceExclusivityService.php`): é o que impede o `ambiguous`. Ligar a Integração Space em uma empresa **desliga automaticamente nas outras** (só `exent_space_status`/`exent_space_admin`; `status` e `space_id` ficam intactos, para o rollback continuar simples). Roda no `store` (`CollaboratorController.php:739`) e no `update` (`:1018`), e a tela avisa antes de salvar (`space_exclusivity_check`, `:1333`).
  - **Por que é trava e não preferência:** `finance_refunds` guarda só o `id_space` (string), **sem `collaborator_id`**. Um reembolso gravado no tenant errado parece perfeitamente válido.
  - **Exceção deliberada no `toggle_status` (`:525`):** ali reativar o colaborador liga o Space como *efeito colateral*, não como ato explícito. Então, se outra empresa já atende o e-mail, o Nexos **não** rouba o atendimento: deixa a integração desligada e avisa. Transferência de propósito só pela tela de edição.
  - **Roda depois do `DB::commit()`**, junto com a gravação do workspace. As duas escrevem em **outras conexões** (banco central e banco dos outros tenants), que o rollback do tenant atual não alcança — antes do commit, uma falha adiante deixava efeito externo de pé sem o registro local correspondente.
- **Workspace no Space** (`space_user_workspaces`, banco **central**, chaveada por e-mail): o **outro** eixo — em qual workspace do Space o colaborador convive. Não confundir com o tenant do Nexos. Definida na tela do colaborador (`add.blade.php:337`, `edit.blade.php:604`), gravada em `syncSpaceWorkspace()` (`:1369`, chamada em `:732` e `:1012`) e exposta ao Space em `integrations.space_workspace_id` (`CollabController.php:593`). Vazio = "o Space decide". A lista do seletor vem de `GET {exent_space_url_api}/nexos/workspaces`; se a API estiver fora, cai nos tenants do Nexos como aproximação **e avisa na tela**.
  - ⚠️ **Os ids são os do Space, não os do Nexos.** Verificado neste ambiente: a API devolve `{"id":"1","name":"exent"}` e `{"id":"4","name":"vendrix"}` — numéricos. O fallback, por vir dos tenants do Nexos, produziria `"exent"`/`"vendrix"`. Um workspace salvo **enquanto a API estava fora** grava um id que o Space não reconhece. Se o aviso de fallback aparecer na tela, **não salve o workspace** — corrija a `exent_space_url_api` primeiro.
- **Outbound (Nexos → Space):** `syncProfileToSpace` (aprovação/rejeição de alterações de perfil; early-return se `space_id` nulo, `CollaboratorController.php:1162`) e `syncStatusToSpace` (ativação/desativação; identifica o usuário **por e-mail**, `:1227-1257`).
- **Ciclo das NFs `expected`** (jobs por tenant, `routes/console.php`):
  - dia 25 às 08:00 (`console.php:97`) `GenerateRecurrenceSnapshot` cria snapshot das recorrências ativas (`finance_payments_collab_recurrence.status=1`) de colaboradores com `status=1` (`RecurrenceSnapshotService.php:61-71`);
  - dia 1 às 08:00 (`console.php:98`) `DispatchSnapshotInvoices` cria `finance_invoices` com `status='expected'` **somente se** o colaborador tiver contrato com `end_date` nulo e `contract_type` na allowlist (`SnapshotInvoiceService.php:71-83`). **A allowlist depende do tipo:** recorrência aceita `['PJ', 'Estágio/PJ']`; **prêmio aceita apenas `['PJ']`** (`:77-79`). E o contrato avaliado é **o último por `start_date DESC, id DESC`** (`:72-75`) — não "o vigente": um contrato mais recente de outro tipo decide a elegibilidade.
  - Sem invoice `expected`, o `invoices/add` do Space responde `"Não há NF aguardando envio."` (`CollabController.php:886`, com HTTP **200**).
  - **Todos os crons rodam para todos os tenants** via `Tenant::cursor()` (`console.php:51-61`), sem allowlist — o vendrix já está no calendário.
- **Nudges de NF:** `SendNFsAlertCollab` é agendado **todo dia às 09:30** (`console.php:127`); a restrição aos dias **1, 3 e 5** é interna (`EmailNFsCollabService.php:32-39`). `SendNFsSummaryCollab` roda nos dias **4 e 5** às 10:00 (`console.php:134`). `NfsNotifyCollabJob` (Slack) roda **só nos dias 4 e 5**, às 11h/15h/18h (`console.php:157-162`).
- NF de recorrência só é aceita para o **mês anterior**; NFs de prêmio só em **abril e outubro** (`CollabController.php:841-862`).

---

## Fase 0 — Levantamento no ambiente alvo (somente leitura)

Rodar em `php artisan tinker` e **anotar os resultados** — eles alimentam todas as fases seguintes.

```php
$emails = ['<email atual do Vitor>', '<email atual do Raul>']; // confirmar no ambiente

// 0.1 Colaboradores no exent: anotar id, space_id, status
Tenant::find('exent')->run(fn() => dump(
  \App\Models\Core\CoreCollaborator::whereIn('email', $emails)
    ->get(['id','name','email','space_id','status','exent_space_status'])
));

// 0.2 Pendências abertas no exent (usar os ids/space_ids do passo 0.1)
Tenant::find('exent')->run(function () use ($collabIds, $spaceIds) {
  dump(\App\Models\Finance\FinanceInvoice::whereIn('collaborator_id', $collabIds)
    ->whereIn('status', ['expected','delayed','rejected'])
    ->get(['id','status','origin_type','reference_date']));
  dump(\App\Models\Finance\FinanceRefund::whereIn('id_space', array_map('strval', $spaceIds))
    ->where('status', 'Pendente')->get(['id','value','status']));
  dump(\App\Models\Finance\FinancePaymentsCollaboratorRecurrence::whereIn('collaborator_id', $collabIds)
    ->get(['id','status','value']));
  // contratos: anotar contract_type e end_date (decidem elegibilidade de invoice — ver Fase 2.3)
  dump(\App\Models\Core\CoreCollaboratorContract::whereIn('collaborator_id', $collabIds)
    ->orderByDesc('start_date')->get(['id','collaborator_id','contract_type','start_date','end_date']));
});

// 0.3 Tokens exent_space de cada tenant: quais existem, a quais tenants estão anexados,
//     e quais chaves o payload já tem (NÃO logar os valores dos secrets)
\App\Models\Integration\IntegrationToken::whereHas('provider', fn($q) => $q->where('slug','exent_space'))
  ->with('companies')->get()
  ->map(fn($t) => ['id'=>$t->id, 'label'=>$t->label,
                   'payload_keys'=>array_keys($t->payload ?? []),
                   'tenants'=>$t->companies->pluck('tenant_id')]);

// 0.4 Usuário central / tenant_user (podem não existir — em dev não existiam)
\App\Models\User::whereIn('email', $emails)->with('tenants')->get(['id','email']);

// 0.5 Setup de e-mail do vendrix — sem isso o ciclo de NF falha EM SILÊNCIO (ver Fase 2.4)
Tenant::find('vendrix')->run(function () {
  dump(\App\Models\Finance\FinanceEmailTemplate::whereIn('type', [
    'collaborator_first_notification',
    'collaborator_second_notification',
    'collaborator_last_notification',
    'invoice_summary_status',
  ])->pluck('type'));
  dump(\App\Models\Finance\FinanceEmailTo::where('type','invoice_recipients')->first());
  dump(\App\Models\Finance\FinanceConfig::first());
});
```

Checar ainda:

- [ ] **Credenciais do vendrix:** `google_cloud` **e** `openai` anexadas via `company_token`.
      - Sem `google_cloud`: falha **alta e clara** — 422 ao Space no upload (`CollabController.php:911-916`).
      - Sem `openai`: **falha silenciosa perigosa** — a NF é gravada como `status='received'`, `status_value=1` **sem validação de valor** (`CollabController.php:926-968`). Tratar como **bloqueante**.
- [ ] `storage/tenant_vendrix/app/private/googleBucket/` contém o JSON de service-account.
- [ ] **Credencial SMTP do escopo `finance`** no vendrix (`SmtpCredentialService`) — sem ela nenhum e-mail sai.
- [ ] **Estado dos dois no Space** (ver o documento irmão): `users.id` (é o `id_space`), `users.email`, `users.nexos_id`, `users.tenant_id` e se a coluna `users.nexos_tenant_id` já existe.
- [ ] Qual commit está deployado no ambiente alvo — define se os Avisos 1 e 2 ainda valem.

---

## Fase 1 — Credenciais do Space no tenant vendrix **[NEXOS-CONFIG]**

Pela tela **Master > Credenciais** (em ambiente sem a correção do Aviso 2, nunca pela tela de Empresa):

1. Se o vendrix não tiver `IntegrationToken` do provider `exent_space` anexado (`company_token`), criar e anexar. Se já existir com payload incompleto (caso encontrado em dev), apenas completar.
2. Payload — **as 3 chaves**:
   - `exent_space_url` e `exent_space_url_api` = mesmos valores do token do exent;
   - `exent_space_token` = **mesmo valor do token do exent** (decisão de 12/08/2026). O middleware valida o bearer contra o tenant do header, então o Space mantém um único bearer e varia apenas o `X-Tenant-ID`.
     > Token distinto por tenant foi **descartado**: no Space o `nexos.api_token` é um valor global usado tanto outbound (`NexosService.php:310`) quanto para autenticar o inbound (`ValidateIntegrationToken.php:13`). Separar exigiria refatorar o inbound, sem ganho nesta migração.

   ⚠️ **Ao criar um token novo, informe as 3 chaves de uma vez:** `credentials_store` (`MasterController.php:969-1007`) grava o payload cru, **sem merge** — só o `credentials_update` (`:1121`) faz `array_merge`.

**Smoke test:** antes da configuração, `POST /api/integration/refunds/get` com `X-Tenant-ID: vendrix` deve retornar **401**; depois, **200** com `data: []`.

---

## Fase 2 — Dados no vendrix: colaboradores + setup financeiro e de e-mail **[NEXOS-CONFIG]**

1. Criar os dois colaboradores pela UI **Core > Colaboradores > Adicionar** (o `store` não dispara sync ao Space), **já com o e-mail `@vendrix.com.br`** (ver Fase 2b).

   Na seção *Integração Space* da tela de cadastro, definir também:
   - **Habilitar:** ligado. Isso aciona a trava de exclusividade (`store`, `CollaboratorController.php:739`).
   - **Workspace no Space:** o workspace da **Exent** — é o que preserva a convivência deles no feed/calendário/academy. Não é o tenant que fatura.

   > **Por que a trava não dispara aqui:** ela é chaveada por **e-mail**, e nesta fase o registro do exent ainda tem o e-mail `@exent.com.br` antigo enquanto o do vendrix já nasce `@vendrix.com.br`. São chaves diferentes, então nada é desligado no exent — a limpeza continua sendo a Fase 4, por SQL.
   >
   > ⚠️ **Numa migração futura em que o e-mail NÃO mude, o comportamento é o oposto:** salvar com *Habilitar* ligado no tenant novo **desliga a integração no tenant antigo na hora**, antes de o Space estar pronto. A tela avisa e pede confirmação ("Entendi, quero transferir"). Nesse cenário, ou faça o passo por tinker (o `update` direto no model não passa pela trava), ou aceite que a Fase 4 já começou.
2. Completar `space_id` via tinker, com os valores anotados na Fase 0.1 (os `space_id` do Space **não mudam** — desde que a Fase 2b seja executada na ordem certa, ver o alerta lá):

```php
// $dados = [['email' => <novo @vendrix>, 'name' => ..., 'space_id' => <da Fase 0.1>], ...]
Tenant::find('vendrix')->run(function () use ($dados) {
  foreach ($dados as $d) {
    $c = \App\Models\Core\CoreCollaborator::firstOrCreate(['email' => $d['email']], ['name' => $d['name']]);
    $c->update(['space_id' => $d['space_id'], 'status' => 1, 'exent_space_status' => 1]);
  }
});
```

   `exent_space_status = 1` é obrigatório: o login do Space rejeita quem não tiver (`GoogleAuthController.php:91-93`).

   Não há conflito de unicidade: a checagem de `space_id` duplicado do `pair` é por banco de tenant (`CollabController.php:52-61`, 409). Também **não há índice unique em `space_id`** — entre esta fase e a Fase 4 o mesmo `space_id` existe nos dois tenants, o que é inofensivo (queries são tenant-scoped) e é justamente o que viabiliza o rollback.

3. **Setup financeiro** (sem isso não nascem invoices `expected` e o upload de NF falha):
   - **Contrato:** `core_collaborator_contracts` com `contract_type='PJ'` (ou `Estágio/PJ`) e `end_date=NULL` — via tela de contrato do colaborador. **Atenção:** o critério usa o **último contrato por `start_date DESC, id DESC`**; se houver mais de um, garanta que o mais recente é o correto. Para NF de **prêmio** só `PJ` é aceito.
   - **Conta bancária:** `finance_collaborator_bank_accounts` com `main_account=1`, `is_active=1` (a recorrência referencia `bank_id`);
   - **Recorrência:** tela Finance > Pagamentos > Colaborador > Recorrência — `finance_payments_collab_recurrence` com `value`, `bank_id`, `reference_description`, `status=1` para cada um.

4. **Setup de e-mail do tenant** — ⚠️ **passo obrigatório, modo de falha silencioso.**

   Não existem seeders no projeto (`Jobs\SeedDatabase` está comentado em `TenancyServiceProvider.php:30`; não há `database/seeders/`). Um tenant recebe as migrations de `database/migrations/tenant/` **sem nenhum dado**. Se faltar template:

   - `sendFinanceTemplate()` retorna `['success' => false]` (`FinanceMailService.php:32-38`);
   - `notification_stage` só avança **se** o envio deu certo (`EmailNFsCollabService.php:109-112`);
   - o job busca `notification_stage == prev`, então o estágio 1 é retentado nos dias 3 e 5 e **nunca avança**.
   - **Resultado: o colaborador nunca é cobrado e nada aparece como erro.**

   Criar no vendrix:
   - `finance_email_templates` dos tipos `collaborator_first_notification`, `collaborator_second_notification`, `collaborator_last_notification` e `invoice_summary_status`;
   - `finance_email_to` do tipo `invoice_recipients` (`EmailNFsCollabService.php:129`);
   - `finance_config`;
   - credencial **SMTP do escopo `finance`**.

   A partir daí o ciclo automático roda no vendrix: dia 25 snapshot → dia 1 invoices `expected` → nudges nos dias 1/3/5 usando o `exent_space_url` do token do vendrix.

---

## Fase 2b — Troca de domínio de e-mail **[NEXOS-CONFIG + SPACE]**

Os dois passam de `@exent.com.br` para `@vendrix.com.br`. **O e-mail é chave de identidade e de junção nos dois sistemas**, então a ordem importa mais que o conteúdo.

**Por que é delicado:**

- No Nexos, `syncStatusToSpace` identifica o usuário **por e-mail** (`CollaboratorController.php:1227`, payload `{email, status}`). E-mail dessincronizado = ativação/desativação perdida em silêncio (o Space responde 404 — `NexosWebhookController.php:130`).
- **O Nexos não tem caminho para propagar a troca.** `syncProfileToSpace` só é chamado por `changes_approve` (`:1121`) e `changes_reject` (`:1327`), que exigem um `CoreCollabPendingChange` originado no Space. Criar o colaborador no vendrix com e-mail novo **não empurra nada**.
- No Space, `users.email` é unique **global** e o login faz `firstOrCreate(['email' => $email], …)` (`GoogleAuthController.php:107-119`). Se qualquer um dos dois logar com o e-mail novo **antes** do `UPDATE`, o Space **cria um segundo usuário** — e `users.id` **é** o `id_space`, então o `id_space` muda e o `pairCollaborator` (`:133`) reaponta o pareamento. Histórico local órfão, e risco de violar `unique(tenant_id, nexos_id)`.

**Ordem obrigatória (não inverter):**

1. Mudanças de código do Space no ar (documento irmão, Mudanças 1-4);
2. Nexos: colaboradores criados no vendrix com o e-mail **novo** e `exent_space_status = 1` (Fase 2);
3. Space: `UPDATE users SET email = '<novo>', nexos_id = <id no vendrix>, nexos_tenant_id = 'vendrix' WHERE id = <id_space atual>;`
   — **preservando `users.id`** e **sem tocar em `tenant_id`** (eles continuam no tenant local da Exent, mantendo feed/calendário/academy);

   > O `nexos_tenant_id` no `UPDATE` é **cinto e suspensório**, não obrigatório: com o `resolve-tenant` no ar, o login preenche esse valor sozinho a partir do e-mail novo. Setar à mão só encurta a janela em que o fallback (`config('nexos.tenant_id')` = `exent`) valeria — e não custa nada. Já o `email` e o `nexos_id` **são** obrigatórios.
4. Só então liberar o login para os dois.

O login com o domínio novo já é aceito: `GOOGLE_ALLOWED_DOMAINS=exent.com.br,vendrix.com.br` no `.env` do Space, e o Socialite não usa `hosted_domain`. O `GOOGLE_HOSTED_DOMAIN=exent.com.br` do Nexos (`config/services.php:43`) **não é bloqueador** — só afeta o grupo legado `finance/*`, que o Space não usa. Vale corrigir por higiene.

---

## Fase 3 — Requisitos para o time do Exent Space **[SPACE]**

> **Detalhado no documento irmão:** [space-ajuste-multi-tenant-nexos.md](space-ajuste-multi-tenant-nexos.md). Resumo aqui.

**Correção de premissa:** o Space **não** é "sistema único sem tenancy". Ele já tem tabela `tenants`, `users.tenant_id`, trait `BelongsToTenant` e `unique(tenant_id, nexos_id)` desde abril/2026. Mas esse `tenant_id` é o tenant **local** (define a convivência no feed/calendário/academy) e **não deve ser usado como seletor do tenant Nexos** — fazer isso isolaria os dois dos colegas.

O que muda:

1. **Coluna nova `users.nexos_tenant_id`** (nullable; `NULL` = fallback `config('nexos.tenant_id')`), mais um mapa por domínio em `config/nexos.php` para usuário novo. **`users.tenant_id` fica inalterado.**
2. **Trocar o tenant global pelo do usuário nos 3 call sites.** Hoje vem de env fixo (`NEXOS_TENANT_ID=exent` e `NEXOS_FINANCE_TENANT=exent`):

   | Arquivo (raiz do Space, `backend/`) | Header em | Cobre |
   |---|---|---|
   | `app/Services/NexosService.php:19` | `:311` | **login/lookup** (`:30`, `:57`), `pair` (`:85`), `sync` (`:121`, `:226`) |
   | `app/Http/Controllers/Nf/NfController.php:23` | `:36`, `:78` | `invoices/get`, `invoices/add` |
   | `app/Http/Controllers/Reembolso/ReembolsoController.php:22` | `:78`, `:158` | `refunds/get|add|cancel` |

3. ⚠️ **O caminho de login é o mais crítico e o mais fácil de esquecer.** O login do Space consulta o Nexos (`GoogleAuthController.php:85`). Se só reembolso/NF forem ajustados, no momento em que a Fase 4 desativar os dois no exent eles **deixam de conseguir entrar no Space**. Mesmo lookup em `ProfileController.php:83`, `UserController.php:305` e `ensureNexosId` (`:302-316`).
4. Os `id_space` **não mudam**, desde que a Fase 2b seja executada na ordem. Se o Space armazena o `nexos_id`, atualizar para os novos IDs do vendrix (via `GET /api/integration/collaborators` com header `vendrix`, ou no retorno do `pair`).
5. **Comunicar aos usuários:** `refunds/get`/`invoices/get` passam a ser respondidos pelo banco do vendrix — o histórico antigo (exent) deixa de aparecer no Space.
6. Ciência: os pushes outbound do Nexos passam a se originar do tenant vendrix para esses usuários. Atenção à assimetria de chaves: `profile-sync` identifica por `id_space`, `user-status` por **e-mail**.

7. **Mudança 6 do documento irmão (aplicar `space_workspace_id` em `users.tenant_id`) NÃO é pré-requisito do cutover.** Para Raul/Victor o workspace não muda, então ela pode ficar para depois. As Mudanças **1-4** continuam sendo o gate da Fase 4.

**Item removido:** *"confirmar que o Space usa `integration/*` e não o legado `finance/*`"* — **verificado**: o Space usa `integration/*` em 100% dos call sites (`.env:63,67`). Nada a migrar.

**Item corrigido:** a versão anterior deste runbook e do documento irmão falavam de um **campo manual "Tenant do Space"** na UI do Nexos, e de uma resposta `"source":"map"` no `resolve-tenant`. **Os dois foram removidos no fim do dia 12/08.** O tenant do Nexos é resolvido só por auto-descoberta; o campo que existe na tela é *Workspace no Space*, que é o outro eixo. Se o time do Space implementou ramificação em `source == "map"`, é código morto.

---

## Fase 4 — Limpeza no exent, sem vazar para o Space **[NEXOS-CONFIG]**

> Executar **somente depois** de (a) as mudanças do Space estarem no ar, (b) a Fase 2b concluída e (c) os dois conseguirem logar no Space (Fase 6, passo 5). Fora dessa ordem, eles ficam trancados fora do Space.

1. Desativar e desparear **via SQL/tinker — não pela UI** (ver Aviso 1):

```sql
-- Banco do tenant exent; ids anotados na Fase 0.1
UPDATE core_collaborators
   SET status = 0, exent_space_status = 0, space_id = NULL, deleted_at = NOW()
 WHERE id IN (<ids da Fase 0.1>);
```

   **Quem realmente faz o trabalho é `status = 0`:** `CoreCollaborator` **não usa SoftDeletes** (`app/Models/Core/CoreCollaborator.php:8-12`) e `deleted_at` **nunca é lido** em nenhuma query do app — é escrito só pelo `toggle_status`. O filtro do snapshot é `status = 1` (`RecurrenceSnapshotService.php:65`). Manter o `deleted_at` por consistência com a UI, mas sem contar com ele para proteger nada.

   Nular o `space_id` também neutraliza o `syncProfileToSpace` no exent (early-return em `CollaboratorController.php:1162`) caso alguém aprove uma pendência antiga deles.

2. Encerrar pendências financeiras no exent (o histórico permanece, apenas sem itens "abertos"):
   - `finance_invoices` deles em `expected/delayed/rejected` → `status='cancelled'`. **Load-bearing:** os nudges filtram `finance_invoices.status = 'expected'` (`EmailNFsCollabService.php:62`), então é isso que os faz parar de citá-los;
   - recorrências em `finance_payments_collab_recurrence` → `status=0`; se um snapshot `draft/closed` do mês corrente já os incluir, cancelar os itens pelo fluxo de snapshot;
   - `finance_refunds` `Pendente` com `id_space` deles → pagar ou cancelar antes do corte.

3. **Encerrar o contrato deles no exent** (`core_collaborator_contracts.end_date`): sem isso sobra um contrato PJ aberto de colaborador inativo — sujo para relatórios e para o critério de elegibilidade (que olha o último contrato).

4. `tenant_user` (se a Fase 0.4 encontrou linhas): remover por SQL direto — nunca pelo form de Empresa (ver Aviso 3). **Não** criar vínculo com o vendrix:

```sql
-- Banco central
DELETE tu FROM tenant_user tu
  JOIN users u ON u.id = tu.user_id
 WHERE tu.tenant_id = 'exent'
   AND u.email IN ('<email do Vitor>', '<email do Raul>');
```

---

## Fase 5 — Correções de código **[NEXOS-CODE]**

1. ✅ **FEITO** (12/08/2026, branch `feat/migracao-colaboradores-tenant-space`): `exent_space_url_api` incluído no payload (`MasterController.php:236` e `:602`) e na validação (`:67` e `:482`) de `company_store`/`company_update` — elimina o bug que zerava a URL outbound ao salvar a Empresa.
2. ✅ **FEITO** (mesma branch): early-return em `toggle_status` quando `space_id` é nulo (`CollaboratorController.php:518`) — paridade com `syncProfileToSpace`. Não dispensa o SQL da Fase 4 (ver Aviso 1).
3. ✅ **FEITO** (mesma branch): endpoint `GET /api/integration/resolve-tenant` (`CollabController.php:42`, rota `routes/api.php:48`) e o modo `integration.token:any` no `IntegrationApiTokenMiddleware` — permite ao Space descobrir o tenant no login, sem `X-Tenant-ID`.
4. ✅ **FEITO** (mesma branch): trava de exclusividade (`app/Services/SpaceExclusivityService.php`) + aviso na tela antes de salvar (`space_exclusivity_check`, `CollaboratorController.php:1333`; JS em `add.blade.php:511` e `edit.blade.php:691`). É o que garante que o `resolve-tenant` nunca precise responder `ambiguous`.
5. ✅ **FEITO** (mesma branch): campo **Workspace no Space** nas telas de cadastro e edição, tabela central `space_user_workspaces` e exposição em `integrations.space_workspace_id` / `space_workspace_name` / `nexos_tenant` na API de colaboradores.
   - Duas migrations, e a **segunda corrige a primeira**: `2026_08_12_170000_create_space_user_tenants_table` criou a tabela com o significado errado ("qual tenant do Nexos atende"), e `2026_08_12_190000_rename_space_user_tenants_to_workspaces` renomeia tabela e coluna para o significado correto (workspace do Space). **Rodar as duas, na ordem** — não tente pular para o estado final.
6. ✅ **FEITO** (13/08/2026, mesma branch): guarda em `update` (`:857`) impedindo `exent_space_status = 1` em colaborador com `status = 0`. Sem ela, salvar um inativo com o switch ligado acionava a trava e **roubava** o atendimento do tenant ativo — deixando o e-mail sem nenhum tenant resolvível, já que o `resolve-tenant` exige `status = 1`.
7. ✅ **FEITO** (13/08/2026): paridade da tela de cadastro — o seletor de workspace e o aviso de exclusividade existiam só na edição, e o `store` não gravava o workspace. Agora `add_view` carrega a lista (`:208`) e o `store` chama `syncSpaceWorkspace` (`:732`).
8. ✅ **FEITO** (13/08/2026): `resolveTenant` trocou `Tenant::cursor()` por `get()` na conexão central — o `$tenant->run()` aninhado troca a conexão no meio da iteração, e um cursor aberto continuaria lendo da conexão trocada. Era o mesmo padrão que o `SpaceExclusivityService` já documentava como armadilha.
9. ✅ **FEITO** (13/08/2026): gravação do workspace e trava movidas para **depois do `DB::commit()`** em `store` e `update` — escrevem em outras conexões, que o rollback do tenant não cobre.
10. ✅ **FEITO** (13/08/2026): `space_exclusivity_check` ganhou `$id = null`. A rota declara `{id?}` e a tela de cadastro chama sem id; sem o default, essa chamada estourava em 500.
11. Pendente / opcional: comando artisan idempotente encapsulando as Fases 2 e 4, com `--dry-run` (útil se houver novas migrações de colaboradores entre tenants).
12. Higiene, não bloqueante: incluir `vendrix.com.br` em `GOOGLE_HOSTED_DOMAIN` (hoje `.env` tem só `exent.com.br`); só afeta o grupo legado `finance/*`, que não está em uso.
13. **Pendente, não relacionado à migração:** `update()` tem um `return` de 404 (`CollaboratorController.php:797`) **depois** do `DB::beginTransaction()` (`:792`) e **sem** `DB::rollback()` — vaza uma transação aberta. Nenhuma escrita aconteceu até ali, então o efeito prático é pequeno, mas é sujeira que confunde. Encontrado em 13/08/2026, deixado como está por estar fora do escopo desta migração.

---

## Fase 6 — Sequência de cutover (checklist)

**Janela recomendada: entre o dia 6 e o dia 24 do mês** — depois dos resumos de NF (dias 4-5) e antes da geração do snapshot (dia 25, 08:00). Feita a virada até o dia 24, as recorrências do vendrix entram no snapshot do mês e as NFs do ciclo seguinte já caem no vendrix. Prêmios só existem em abril/outubro.

| # | Quando | Ação |
|---|--------|------|
| 1 | D-x | Fase 0 (levantamento) — **gate de "OK to go"**: revela templates, SMTP, `openai`, `google_cloud` e o estado dos dois no Space |
| 1b | D-x | **Deploy do Nexos** com a branch `feat/migracao-colaboradores-tenant-space` + `php artisan migrate` no **banco central** (as duas migrations de `space_user_*`, na ordem). Sem isso o `resolve-tenant` não existe e o passo 4 não tem o que consumir |
| 2 | D-x | Fase 1 (credenciais) + smoke test 401→200 |
| 3 | D-x | Fase 2 (colaboradores com e-mail novo, `space_id`, contratos, contas, recorrências, **templates de e-mail e SMTP**) |
| 4 | D-x | Space implementa e testa em staging as Mudanças 1-4 do documento irmão, **incluindo o caminho de login** |
| 5 | D-0 | Space em produção: Mudanças 1-4 + Fase 2b passo 3 (`UPDATE users`) |
| 6 | D-0 | **Teste de fumaça obrigatório: os dois logam no Space** com o e-mail novo, e o feed/calendário/academy continua o da Exent |
| 7 | D-0 | Restante da Fase 7 (verificação fim a fim) |
| 8 | D-0 | Fase 4 (limpeza no exent — SQL, nunca UI) — **só depois do passo 6 passar** |
| 9 | D+1 até dia 1 | Monitorar: snapshot do dia 25 inclui os dois; invoices `expected` criadas no dia 1; upload de NF via Space OK; nudges chegando |

**Rollback:** reverter `nexos_tenant_id` no Space para `exent` (e o `email`/`nexos_id`, se já trocados); restaurar no exent `status=1, exent_space_status=1, space_id=<valores originais da Fase 0.1>, deleted_at=NULL` e reativar as recorrências; desativar os registros do vendrix **por SQL**. Nenhuma fase destrói dados: tudo é reversível por `UPDATE`s.

---

## Fase 7 — Verificação fim a fim

- [ ] **Auth/tenancy:** `POST /api/integration/refunds/get` com bearer + `X-Tenant-ID: vendrix` → 200 com `data: []`.
- [ ] **`resolve-tenant`:** `GET /api/integration/resolve-tenant?email=<novo @vendrix>` com bearer e **sem** `X-Tenant-ID` → `{"resolved":true,"tenant_id":"vendrix","source":"discovery"}`. Repetir com um e-mail do exent → `"tenant_id":"exent"`. Sem bearer → 401; sem e-mail → 422.
- [ ] **Exclusividade (o teste que protege o `resolve-tenant`):** com o e-mail já habilitado no vendrix, abrir o mesmo e-mail no **exent** e ligar o switch *Habilitar* → a tela avisa quais empresas perdem o atendimento; ao salvar, o vendrix é desabilitado sozinho. Conferir que `resolve-tenant` continua `resolved:true` (agora `exent`) e **nunca** devolve `ambiguous`. **Desfazer depois do teste.**
- [ ] **Guarda de inativo:** num colaborador com `status = 0`, tentar salvar com *Habilitar* ligado → `exent_space_status` deve continuar `0` e **nenhuma** outra empresa deve ser desabilitada.
- [ ] **Workspace:** `GET /api/integration/collaborators` (header vendrix) traz `integrations.space_workspace_id` = workspace da **Exent** para os dois. Se vier `null`, o seletor não foi preenchido na Fase 2.1.
- [ ] **Fallback do seletor:** com `exent_space_url_api` inválida no tenant, abrir a tela do colaborador → o seletor ainda renderiza e aparece o aviso "não foi possível carregar os workspaces do Space". **Restaurar a URL depois.**
- [ ] **Pareamento:** `GET /api/integration/collaborators` com header vendrix → os dois presentes com `integrations.exent_space_id` corretos. No Space há `php artisan nexos:diagnose` (`NexosDiagnose.php`) que já faz exatamente essa chamada — só precisa aceitar o tenant como argumento.
- [ ] **Login (o teste que valida a Fase 3):** os dois entram no Space com o e-mail `@vendrix.com.br`, **sem criar registro novo** — `users.id` igual ao de antes.
- [ ] **Convivência preservada:** logados como eles, feed, calendário e academy continuam mostrando o conteúdo da Exent; `users.tenant_id` inalterado.
- [ ] **Reembolso:** `POST refunds/add` (Space de staging → vendrix) → 201; linha em `finance_refunds` do banco vendrix e arquivo no bucket (credencial `google_cloud` do vendrix).
- [ ] **NF (caminho feliz):** `POST invoices/get` → `can_upload: true` (após o dia 1, ou criando uma invoice `expected` manual em homologação); `POST invoices/add` com `invoice_type=recurrence` → 201 e invoice `received` no banco vendrix. NF de recorrência só é aceita para o **mês anterior**.
- [ ] **NF com valor divergente (valida a credencial `openai`):** enviar NF com valor diferente do esperado → deve gravar `status='rejected'`, `status_value=0`. Se vier `received`, a credencial `openai` do vendrix está faltando e **toda NF está sendo aceita sem validação**.
- [ ] **Nudges:** confirmar que o e-mail de cobrança sai no vendrix e que `notification_stage` **avança** (se não avançar, falta template — ver Fase 2.4).
- [ ] **Isolamento:** `refunds/get`/`invoices/get` com header `exent` não retornam nada novo para os `space_id` deles; nenhum e-mail/Slack de nudge do exent os cita no ciclo seguinte.
- [ ] **Outbound:** partindo do **Space de homologação**, enviar uma alteração de perfil sensível via `POST collaborators/sync` (é o que cria o `CoreCollabPendingChange`) e aprová-la no Nexos-vendrix → `POST {exent_space_url_api}/nexos/profile-sync` chega ao Space. A edição pela tela do Nexos (`update`) **não** dispara o sync — só grava histórico (`CollaboratorController.php:899`).
- [ ] **Status no Space permanece ativo** para os dois após a Fase 4.

---

## Arquivos de referência

| Arquivo | Relevância |
|---|---|
| `docs-llm/runbooks/space-ajuste-multi-tenant-nexos.md` | **Mudanças no Space — pré-requisito da Fase 4** |
| `app/Http/Controllers/MasterController.php` | `exent_space_url_api` corrigido (validação 67 e 482; payload 236 e 602); `credentials_update` com merge (1121); `credentials_store` sem merge (969-1007); `users()->sync()` destrutivo (574) |
| `app/Http/Controllers/Core/CollaboratorController.php` | `toggle_status` (503, guarda de `space_id` em 558, exceção da trava em 525), `add_view` (173, workspaces em 208), `edit_view` (216, workspaces em 322), `store` (595, commit em 721, workspace em 732, trava em 739), `update` (760, guarda de inativo em 857, commit em 1002, workspace em 1012, trava em 1018), `space_exclusivity_check` (1333), `syncSpaceWorkspace` (1369), `syncProfileToSpace`, `syncStatusToSpace` |
| `app/Http/Controllers/Integrations/CollabController.php` | `resolveTenant` (42); inbound `pair`, `refunds/*`, `invoices/*`; `space_workspace_id`/`nexos_tenant` no payload (593-599); carga em lote dos workspaces (523) |
| `app/Services/SpaceExclusivityService.php` | **Trava de exclusividade** — `otherActive()` (aviso) e `enforce()` (desliga nos outros tenants) |
| `app/Services/SpaceWorkspaceService.php` | Lista de workspaces do Space (`GET /nexos/workspaces`), com fallback pelos tenants do Nexos e memo por request |
| `app/Models/Integration/SpaceUserWorkspace.php` | Mapa e-mail → workspace do Space; **força a conexão central** em qualquer contexto |
| `database/migrations/2026_08_12_170000_*` e `2026_08_12_190000_*` | Criação e **rename corretivo** da tabela central. Rodar as duas, na ordem |
| `app/Http/Middleware/IntegrationApiTokenMiddleware.php` | Validação do bearer por tenant do header; modo `:any` para o `resolve-tenant` |
| `app/Services/IntegrationCredentialService.php` | Resolução de credenciais por tenant |
| `app/Services/Finance/SnapshotInvoiceService.php` | Elegibilidade das invoices `expected` (71-83) |
| `app/Services/Finance/RecurrenceSnapshotService.php` | Filtro de colaborador ativo (`status=1`, linha 65) |
| `app/Services/Finance/InvoiceCollab/EmailNFsCollabService.php` | Nudges, `notification_stage` (109-112), destinatários (129) |
| `app/Services/Finance/FinanceMailService.php` | Falha silenciosa por template ausente (32-38) |
| `routes/console.php` | Calendário dos jobs por tenant (define a janela de cutover); macro `tenantJob` (51-61) |
| `docs-llm/api-space/` | Documentação da integração com o Space |
