# Ajuste no Exent Space — tenant do Nexos por usuário

> **Para quem:** time/agente que vai trabalhar **dentro do projeto do Exent Space**
> (`/var/www/projetos5.exentdev.com.br/st/8.3/space`, backend Laravel em `backend/`).
>
> **Por quê:** o Nexos vai passar a atender Raul Rodrigues e Vitor Freitas do Lago pelo tenant
> `vendrix` em vez do `exent`. Hoje o Space envia um `X-Tenant-ID` **fixo por deploy**, então não
> tem como representar dois usuários em tenants diferentes.
>
> **Como usar:** execute a partir da raiz do projeto do Space
> (`/var/www/projetos5.exentdev.com.br/st/8.3/space`) — todos os caminhos citados são relativos a ela,
> e as linhas foram conferidas nesse checkout em 12/08/2026. Confira cada `arquivo:linha` antes de
> editar: se o arquivo tiver mudado desde então, o número pode ter deslizado.
>
> **Documento irmão (lado Nexos, no outro repo):**
> `nexos/cortex/docs-llm/runbooks/migracao-colaboradores-exent-para-vendrix.md`.

---

## ⚠️ ATUALIZAÇÃO 12/08/2026 — a fonte de verdade passou para o Nexos

A primeira versão deste documento pedia que o Space deduzisse o tenant por conta própria (mapa
`tenant_by_domain` na config). **Isso mudou.** O Nexos agora é o dono da informação e expõe um endpoint
tenant-agnóstico que o Space consulta. Motivos:

- o mapa por domínio não escala (um mesmo domínio pode ter gente em tenants diferentes, e vice-versa);
- havia um bug de leitura nele que o tornava inócuo (ver a seção de correções abaixo);
- não existia caminho de bootstrap: nem o login nem o job conseguiam descobrir o primeiro usuário de um
  tenant novo;
- e o desempate agora tem dono explícito na UI do Nexos — o switch *Habilitar* da Integração Space, na
  tela do colaborador.

> **⚠️ Correção 12/08/2026, fim do dia — não existe mais campo "Tenant do Space".**
> Uma versão anterior desta seção falava de um "override definido na UI" e de uma resposta com
> `"source":"map"`. **Os dois foram removidos.** A tabela que guardaria esse override foi renomeada e
> passou a significar outra coisa (o *workspace* do Space — ver a Mudança 6 no fim deste documento).
> Quem responde por NF/reembolso é resolvido **só** por auto-descoberta, sem campo manual: o Nexos
> procura o colaborador habilitado em cada tenant. Se você implementou tratamento para
> `source == "map"`, remova — essa resposta nunca chega.

### O endpoint (já no ar, testado)

```
GET /api/integration/resolve-tenant?email={email}
Authorization: Bearer {mesmo NEXOS_API_TOKEN de hoje}
# NÃO envie X-Tenant-ID — é justamente o endpoint que descobre o tenant
```

Implementação no Nexos: `app/Http/Controllers/Integrations/CollabController.php:42`, rota em
`routes/api.php:48` (middleware `integration.token:any` — aceita o bearer de qualquer tenant e dispensa
o `X-Tenant-ID`).

Respostas reais deste ambiente:

```jsonc
// o Nexos procurou o colaborador habilitado nos tenants — único source que existe
{"success":true,"email":"raul...","tenant_id":"vendrix","resolved":true,"source":"discovery"}

// e-mail habilitado em MAIS DE UM tenant → desempate humano, use seu fallback
// (esperado ficar raro: o Nexos passou a impedir isso na origem — ver "Trava de
//  exclusividade" abaixo. Continue tratando: dados legados podem estar nesse estado.)
{"success":true,"email":"...","tenant_id":null,"resolved":false,"ambiguous":true,
 "candidates":["exent","vendrix"],"message":"..."}

// não encontrado em nenhum tenant
{"success":true,"email":"...","tenant_id":null,"resolved":false,"ambiguous":false,"candidates":[]}
```

Contrato: sempre HTTP **200** quando o bearer é válido (422 sem e-mail, 401 sem bearer). Trate
`resolved:false` como caso normal, não erro — nesse caso use `config('nexos.tenant_id')`.

**O que isso muda no Space:** a coluna `users.nexos_tenant_id` **continua necessária** (é o que alimenta
as chamadas autenticadas de NF/reembolso sem um round-trip por request). Ela passa a ser um **cache** do
que o Nexos respondeu, preenchida no login. O mapa `tenant_by_domain` pode ser removido.

### Trava de exclusividade (por que `ambiguous` deve ficar raro)

O `ambiguous` não é uma condição que o Space precise resolver de forma inteligente: ele existe porque, em
teoria, duas empresas do Nexos podem ter o mesmo e-mail com a integração ligada. O Nexos passou a
**impedir isso na origem** (`app/Services/SpaceExclusivityService.php`): ligar o switch *Habilitar* em uma
empresa desliga automaticamente nas outras, e a tela avisa antes de salvar quem vai perder o atendimento.

Duas consequências para quem trabalha no Space:

1. **Não implemente desempate próprio.** Se `ambiguous` aparecer, é dado inconsistente no Nexos, não uma
   decisão que o Space deva tomar. Caia no `config('nexos.tenant_id')` e **reporte** — a correção é
   desligar a integração na empresa errada, pela tela do Nexos.
2. **A troca de empresa é silenciosa do ponto de vista do Space.** Quando alguém transfere o atendimento
   no Nexos, nenhum webhook avisa. O `nexos_tenant_id` cacheado fica velho até o próximo login. Se isso
   virar problema, o caminho é revalidar no login (que já é o que o `GoogleAuthController` faz) ou chamar
   `resolve-tenant` no `SyncNexosUsersJob`.

Existe também um atalho que evita o round-trip: `GET /api/integration/collaborators` (esse **exige**
`X-Tenant-ID`) agora devolve `integrations.nexos_tenant` em cada colaborador, que é o próprio tenant do
header. Serve para o `SyncNexosUsersJob` popular `nexos_tenant_id` em lote, sem um `resolve-tenant` por
usuário.

---

## Correções na implementação atual (revisão de 12/08/2026)

O que já está **correto e deve ficar**: a coluna `users.nexos_tenant_id` (migration rodada, no
`$fillable`, `users.tenant_id` intocado); o `NexosService` com `forTenant()` e resolução *lazy* fora do
construtor; e `NfController`/`ReembolsoController` resolvendo por usuário dentro dos métodos, já com
`config()` em vez de `env()`. Nada a mexer nesses três.

O que precisa de correção, em ordem de gravidade:

| # | Problema | Onde | Correção |
|---|---|---|---|
| 1 | **`config()` com chave que contém pontos nunca resolve.** `config('nexos.tenant_by_domain.vendrix.com.br')` → `NULL`, porque o `Arr::get` quebra em `vendrix`/`com`/`br`. Efeito: todo usuário novo caía no fallback `exent` e o lockout continuava | `GoogleAuthController.php:91` | Some junto com o mapa: passa a chamar `resolve-tenant` (ver Mudança 3) |
| 2 | **Sem caminho de bootstrap.** `nexosTenantGroups()` monta os grupos a partir de `users.nexos_tenant_id` no banco — e nenhum usuário tem valor, então o job só percorre `exent` e nunca vê o `vendrix` | `SyncNexosUsersJob.php:145-158` | Derivar os grupos de uma lista de config, não do banco. Ou consultar `resolve-tenant` por e-mail |
| 3 | **`nexos_tenant_id` nunca é backfilled no login.** O `firstOrCreate` só grava na criação, e o `update()` das linhas 147-153 não inclui o campo — usuário pré-existente (os 18 atuais) nunca ganha valor | `GoogleAuthController.php:117,147-153` | Gravar sempre o que o `resolve-tenant` devolveu (ver Mudança 3) |
| 4 | **Thrash do campo a cada execução do job.** O grupo default guarda `null`, mas o login grava a string `'exent'`; os dois se sobrescrevem, gerando escrita e chamada de API dupla por usuário | `SyncNexosUsersJob.php:96,151` | Padronizar: sempre string, ou sempre `null` para o default. Recomendo string |
| 5 | **`env('GOOGLE_ALLOWED_DOMAINS', 'exent.com.br')` fora de `config/`.** Com `config:cache` em produção, `env()` retorna null e o default entra em vigor — **bloqueia todo login `@vendrix.com.br`** antes de qualquer outra coisa. Latente hoje (não há config cacheada) | `GoogleAuthController.php:26` | Mover para `config/services.php` (ou `config/nexos.php`) e ler com `config()` |
| 6 | **Call site cross-tenant descoberto.** `adminProfile(User $user)` chama `getCollaboratorByEmail($user->email)` sem `forTenant()`, então usa o tenant do **admin logado**, não o do usuário visualizado. Admin de um tenant vendo usuário de outro recebe documentos vazios (o `catch (\Throwable)` engole) | `Users/UserController.php:305` | `->forTenant($user->nexos_tenant_id)` |
| 7 | Limpeza não feita: `.env.example:77` ainda sem `vendrix.com.br`; `NEXOS_FINANCE_TENANT` morto em `:62`; `backend/ReembolsoController.php` (cópia morta na raiz) ainda existe com `env('NEXOS_FINANCE_TENANT')` | vários | Apagar/atualizar |

**Bug fora do escopo de tenancy, mas quebra em runtime:** `ProfileController.php:271` chama
`$this->nexos->syncDocumentFiles(...)`, método renomeado para `syncAll()` no mesmo diff
(`NexosService.php:213`). Esse endpoint lança `Call to undefined method`.

Detalhe menor: `nexos_tenant_id` com **string vazia** não é `null`, então `?? config(...)` não dispara o
fallback e o header sai vazio. Garanta `NULL` (ou sempre uma string válida) na coluna.

---

## É só um seletor de tenancy no usuário?

**Sim, o seletor por usuário é o desenho certo — mas ele não é a parte difícil.** O que causa incidente
é *onde* o seletor precisa ser lido e *em que ordem* a virada acontece.

Quatro coisas, em ordem de risco:

1. **Ligar o seletor no fluxo de LOGIN.** É o ponto mais fácil de esquecer, porque não é óbvio que o
   login fale com o Nexos — mas fala (`GoogleAuthController.php:85`). Se só os endpoints de reembolso/NF
   forem ajustados, no momento em que o Nexos desativar os dois no tenant `exent` eles **deixam de
   conseguir entrar no Space**.
2. **Coordenar a troca de e-mail** (`@exent.com.br` → `@vendrix.com.br`). Não é código: é ordem de
   operação. Feito fora de ordem, o Space **cria um usuário duplicado** e reaponta o pareamento.
3. **Não confundir o seletor com o tenant local do Space.** São dois eixos independentes — misturá-los
   isola o Raul e o Victor do feed, calendário e academy dos colegas. Ver o aviso da Mudança 1.
4. **O seletor em si**, que é uma coluna e um fallback.

O comentário está falando do tenant **local**, que aqui deve continuar como está — mas a ideia de usar o
domínio do e-mail serve bem como fallback do seletor do Nexos para usuário novo (Mudança 1).

---

## Estado atual (verificado, não presumido)

**O que já existe e não precisa ser criado:**

| Item | Onde |
|---|---|
| Tabela `tenants` — `id`, `name`, `domain` (nullable unique), `settings` (json) | `backend/database/migrations/0001_01_01_000000_create_users_table.php:11-17` |
| `users.tenant_id` — FK para `tenants`, nullable. **Tenant local: define a convivência no Space, não o tenant do Nexos** | mesma migration, `:21` |
| Trait `BelongsToTenant` com global scope por `users.tenant_id` — aplicado em `FeedPost`, `CalendarEvent`, `AcademyCategory` | `backend/app/Models/Concerns/BelongsToTenant.php`, aplicada em `app/Models/User.php:19` |
| Relação `User::tenant()` | `app/Models/User.php:65-68` |
| `unique(tenant_id, nexos_id)` + índices `(tenant_id,email)` e `(tenant_id,is_active)` | `backend/database/migrations/2026_04_13_000000_add_tenant_indexes_to_users_table.php:54-60` |
| Backfill já rodado — tenant `name='Exent'`, `domain='exent.com.br'` | `backend/database/migrations/2026_04_27_000001_backfill_tenant_id_in_users_table.php:13-15` |
| `GOOGLE_ALLOWED_DOMAINS=exent.com.br,vendrix.com.br` | `backend/.env:75` — o domínio novo **já está liberado** |
| Space chama `integration/*` (não o legado `finance/*`) | `backend/.env:63,67` — ambas as URLs em `.../public/api/integration/` |

**O gap:** `users.tenant_id` é o tenant **local** do Space. O tenant do **Nexos** vem de env global:

```
backend/.env:64   NEXOS_FINANCE_TENANT=exent
backend/.env:69   NEXOS_TENANT_ID=exent
```

E o vínculo entre os dois é um casamento de string frágil: `backend/app/Jobs/SyncNexosUsersJob.php:122-136`
resolve o tenant local comparando `config('nexos.tenant_id')` (`'exent'`) contra `tenants.name` **ou**
`tenants.domain` — só funciona porque o backfill gravou `name='Exent'` e a collation do MySQL é
case-insensitive.

**Os 3 call sites que enviam o header:**

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

---

## Mudança 1 — coluna `nexos_tenant_id` em **`users`** (não em `tenants`)

> ⚠️ **Não use o tenant local do Space como seletor, e não mexa em `users.tenant_id`.**
>
> É tentador derivar o tenant do Nexos do `users.tenant_id` (criando um tenant local "Vendrix"), mas
> isso **isola os dois dos colegas dentro do Space**. O trait `BelongsToTenant` aplica global scope por
> `users.tenant_id` em `FeedPost`, `CalendarEvent` e `AcademyCategory`
> (`backend/database/migrations/2026_04_27_000002_backfill_tenant_id_in_owned_tables.php:17`), então o
> Raul e o Victor perderiam feed, calendário e academy da Exent.
>
> O requisito é o oposto: eles **continuam convivendo no mesmo Space**, só passam a ser atendidos por
> outro tenant do Nexos. São dois eixos independentes:
>
> | Eixo | Campo | Valor para o Raul/Victor |
> |---|---|---|
> | Com quem eu convivo no Space (feed, calendário, academy) | `users.tenant_id` | **inalterado** — segue o tenant local da Exent |
> | Qual tenant do Nexos me atende (NF, reembolso, perfil) | `users.nexos_tenant_id` *(novo)* | `vendrix` |

Nova migration:

```php
Schema::table('users', function (Blueprint $table) {
    // nullable: NULL = usa o default de config('nexos.tenant_id'), preservando o comportamento atual
    $table->string('nexos_tenant_id', 64)->nullable()->after('tenant_id')->index();
});
```

Não precisa de backfill: quem ficar `NULL` cai no fallback `config('nexos.tenant_id')` (= `exent`).
Acrescentar `nexos_tenant_id` ao `$fillable` do model `User` (`backend/app/Models/User.php:21-35`).

✅ **Este item já está implementado corretamente** — nada a fazer.

> **Atualizado 12/08:** o mapa `tenant_by_domain` que esta seção pedia **não é mais necessário** e pode
> ser removido de `backend/config/nexos.php`. Quem responde "de quem é este e-mail" agora é o Nexos, via
> `GET /api/integration/resolve-tenant` (ver o topo deste documento). A coluna continua sendo o cache
> local dessa resposta. Manter apenas `'tenant_id' => env('NEXOS_TENANT_ID')` como fallback final.

> **Por que em `users` e não em `tenants`:** o tenant local do Space e o tenant do Nexos passam a ser
> ortogonais — é justamente essa independência que permite o Raul e o Victor serem faturados pelo vendrix
> **sem** sair do Space da Exent. Amarrar os dois eixos numa coluna de `tenants` tornaria essa combinação
> impossível de representar.

---

## Mudança 2 — `NexosService`: tenant resolvido tarde, nunca no construtor

Em `backend/app/Services/NexosService.php`, remover a linha `:19` (`$this->tenantId = config('nexos.tenant_id')`)
e resolver na hora de montar o header.

**Atenção — não faça consulta ao banco no construtor.** O próprio arquivo já documenta o motivo em
`:302-306`: `SyncNexosUsers` injeta `NexosService` no construtor e o Console Kernel instancia todo
comando ao bootar (inclusive para rodar `migrate`), então tocar o banco ali quebra o boot num banco
novo. Siga o mesmo padrão *lazy* já usado para o token em `headers()` (`:300`, token em `:307`).

```php
private ?string $tenantOverride = null;

/** Força o tenant do Nexos nesta instância (fluxos sem usuário autenticado: login, jobs). */
public function forTenant(?string $nexosTenantId): self
{
    $this->tenantOverride = $nexosTenantId;
    return $this;
}

private function resolveTenantId(): string
{
    if ($this->tenantOverride !== null) {
        return $this->tenantOverride;
    }

    return auth()->user()?->nexos_tenant_id
        ?? (string) config('nexos.tenant_id');   // fallback: comportamento atual
}
```

E em `headers()` (`:311`): `'X-Tenant-ID' => $this->resolveTenantId(),`

O override vale para toda a instância — o que é justamente o que o login precisa (o mesmo objeto faz
o lookup **e** o `pairCollaborator`).

Ajustar também `app/Jobs/SyncNexosUsersJob.php:122-136`: em vez de resolver um tenant único a partir do
env, iterar os valores distintos de `nexos_tenant_id` em uso (mais o default, para os `NULL`) e chamar
`->forTenant($t)` em cada volta. Sem isso o sync em lote continua vendo só o exent. Cuidado no casamento
dos resultados: o job hoje casa por `email` (`:46-69`), o que duplica usuário se o e-mail mudar — casar
por `nexos_id` **dentro do tenant** é mais seguro.

---

## Mudança 3 — login: resolver o tenant ANTES de consultar o Nexos

Este é o item que evita o lockout. Em `backend/app/Http/Controllers/Auth/GoogleAuthController.php`, hoje
a ordem é:

```
:85   getCollaboratorByEmail($email)      ← usa o tenant GLOBAL
:105  $tenantId = Tenant::orderBy('id')…  ← resolve o tenant depois. Tarde demais.
```

Inverter: resolver o tenant do Nexos **antes** do passo 3. O usuário já existe em `users` (é o caso do
Raul e do Victor), então dá para ler o campo dele — sem depender de sessão autenticada, que ainda não
existe nesse ponto do OAuth.

```php
// (novo passo 2b) — logo após a validação de domínio, antes de falar com o Nexos.
// withoutGlobalScopes(): a busca por email não deve ser filtrada por tenant
// (mesma razão já documentada em :97-98).
$existing = User::withoutGlobalScopes()->where('email', $email)->first();

$nexosTenant = $this->nexos->resolveTenantForEmail($email)   // ← pergunta ao Nexos
    ?? $existing?->nexos_tenant_id                           // cache local, se o Nexos não soube
    ?? (string) config('nexos.tenant_id');                   // fallback final

// 3. Consultar Nexos — agora no tenant certo
$collaborator = $this->nexos->forTenant($nexosTenant)->getCollaboratorByEmail($email);
```

> **Atualizado 12/08:** a linha do meio era `config('nexos.tenant_by_domain.' . Str::after($email,'@'))` —
> é o **bug nº 1** da tabela de correções (chave com pontos nunca resolve). Trocada por uma chamada ao
> Nexos, que é a fonte de verdade. O `Str` deixa de ser necessário aqui.

Novo método em `NexosService` (é o único lugar que fala com o endpoint tenant-agnóstico — note que ele
**não** manda `X-Tenant-ID`):

```php
/** Pergunta ao Nexos qual tenant atende este e-mail. null = Nexos não soube (use seu fallback). */
public function resolveTenantForEmail(string $email): ?string
{
    try {
        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . ($this->token ??= SystemTokenResolver::get('NEXOS_API_TOKEN') ?? ''),
            'Accept'        => 'application/json',
        ])->timeout(10)->get(rtrim(config('nexos.api_url'), '/collaborators') . '/resolve-tenant', [
            'email' => $email,
        ]);

        if ($response->failed()) {
            Log::warning('[NexosService] resolve-tenant falhou', ['status' => $response->status()]);
            return null;
        }

        // ambiguous / não encontrado vêm como resolved:false — caso normal, não erro
        return $response->json('resolved') ? $response->json('tenant_id') : null;
    } catch (\Throwable $e) {
        Log::warning('[NexosService] resolve-tenant indisponível', ['error' => $e->getMessage()]);
        return null;
    }
}
```

⚠️ **Cuidado ao montar a URL:** `config('nexos.api_url')` aponta para `.../api/integration/collaborators`,
não para a base. Considere adicionar `'integration_url' => env('NEXOS_INTEGRATION_URL')` na config
apontando para `.../api/integration` e usar isso, em vez do `rtrim` acima, que é frágil.

**Não mexa no `tenant_id` do `firstOrCreate`** (`:107-119`): `Tenant::orderBy('id')->value('id')` na linha
`:105` continua correto e deve ficar como está — é o tenant **local**, que define com quem o usuário
convive no Space.

**Grave o resultado sempre, não só na criação** (é o **bug nº 3** das correções): acrescente
`'nexos_tenant_id' => $nexosTenant` tanto ao array do `firstOrCreate` quanto ao `$user->update([...])`
das linhas `:147-153`. Sem isso os 18 usuários que já existem nunca ganham o valor, e as chamadas de
NF/reembolso continuam usando o fallback.

O `pairCollaborator($user->id, …)` do passo 6 (`:133`) já herda o tenant correto, porque o override ficou
na instância — e o `NexosService` é injetado no construtor do controller (`:18`,
`__construct(private NexosService $nexos)`), ou seja, é uma instância por request. Sem vazamento entre usuários.

Detalhe de implementação: **`Str` não está importado** neste arquivo (os imports vão até `:14`), então
acrescente `use Illuminate\Support\Str;`. `User` já está importado (`:7`).

**Os outros três lookups por e-mail usam o mesmo caminho** e passam a funcionar de graça, pois aí já
existe usuário autenticado (`auth()->user()`): `app/Http/Controllers/Profile/ProfileController.php:83`,
`app/Http/Controllers/Users/UserController.php:305` e `ProfileController.php:302-316` (`ensureNexosId`).
Vale conferir cada um depois da mudança — hoje eles falham em silêncio, devolvendo lista vazia de
documentos.

---

## Mudança 4 — `NfController` e `ReembolsoController`

Nos dois arquivos:

1. Trocar `env('NEXOS_FINANCE_TENANT', 'exent')` por `auth()->user()->nexos_tenant_id ?? config('nexos.tenant_id')`.
2. **Resolver dentro de cada método, não no construtor.** São rotas autenticadas, mas depender de
   `auth()` no construtor é frágil e some com `config:cache`; melhor não criar a dependência.
3. Aproveitar para trocar `env()` por `config()` na URL base também (`NfController.php:21`,
   `ReembolsoController.php:20`).

> **Por que o `env()` importa:** `env()` fora de arquivo de config retorna `null` quando
> `php artisan config:cache` está ativo, e aí o código cai no default `'exent'` do segundo argumento.
> Hoje isso *coincide* com o valor certo e mascara o problema — depois da mudança por usuário, vira
> bug silencioso que manda o tenant errado. O `docs/configuracoes.md:177` (raiz do projeto, não
> `backend/`) já alerta sobre exatamente esse padrão, citando `NexosService::headers()` como o jeito certo.

Sugestão: acrescentar `'finance_url' => env('NEXOS_FINANCE_URL')` em `backend/config/nexos.php` e
manter `tenant_id` só como fallback.

---

## Mudança 5 — troca de e-mail (operação, não código)

Os dois passam a ter e-mail `@vendrix.com.br`. **O e-mail é a chave de identidade no Space** —
`users.email` é `unique` **global** (`create_users_table.php:23`; a migration multi-tenant de abril não
converteu para unique-por-tenant).

O risco está em `GoogleAuthController.php:107-119`:

```php
$user = User::withoutGlobalScopes()->firstOrCreate(['email' => $email], [ … ]);
```

Se qualquer um dos dois logar com o e-mail novo **antes** de o Space atualizar `users.email`, o
`firstOrCreate` não acha o registro e **cria um segundo usuário**. Consequências:

- **`users.id` É o `id_space`** — não existe coluna `id_space` (veja `NfController.php:38`,
  `NexosService.php:208`). Usuário novo = `id_space` novo;
- `:133` chama `pairCollaborator($novoIdSpace, $nexosId)`, **reapontando o pareamento no Nexos**;
- o histórico local (perfil, DISC, feed, calendário, reservas) fica órfão no registro antigo;
- e há **risco** de violar `unique(tenant_id, nexos_id)` — acontece se o `nexos_id` do colaborador no
  vendrix já estiver em uso por outro usuário desse mesmo tenant local. Não é certo, mas é possível, e
  aí o erro aparece no meio do login.

**Ordem obrigatória, na mesma janela:**

1. Mudanças 1-4 no ar (com o tenant `vendrix` já em `tenants`);
2. Nexos: colaboradores criados no vendrix com o e-mail **novo** e `exent_space_status = 1`
   (o login checa isso em `GoogleAuthController.php:91-93`);
3. Space, **preservando o `users.id`** (que é o `id_space`) e **sem tocar em `tenant_id`**:

   ```sql
   UPDATE users
      SET email           = '<novo @vendrix.com.br>',
          nexos_id        = <id do colaborador no vendrix>,
          nexos_tenant_id = 'vendrix'
    WHERE id = <id_space atual>;
   -- tenant_id NÃO entra: eles continuam no tenant local da Exent (feed/calendário/academy)
   ```
4. Só então liberar o login para os dois;
5. Depois de confirmar o login, o Nexos executa a Fase 4 (limpeza no exent).

O passo 3 é o que mantém verdadeira a premissa do runbook do Nexos de que **os `id_space` não mudam**.

---

## Mudança 6 — `space_workspace_id`: o Nexos passou a poder definir `users.tenant_id`

> **Novo em 12/08/2026 (fim do dia).** Esta mudança não existia nas versões anteriores deste documento e
> é a razão do rename descrito no aviso da seção de atualização.

Até aqui o documento insiste, com razão, que `users.tenant_id` (workspace **local** do Space) e o tenant
do Nexos são **eixos diferentes** e não devem ser unificados. Isso continua valendo. O que mudou é que o
Nexos agora tem uma tela para o **primeiro** eixo também, e passou a expor o valor.

**Os dois eixos, lado a lado:**

| Eixo | Onde mora no Space | Quem decide | O que controla |
|---|---|---|---|
| Workspace do Space | `users.tenant_id` | Nexos, campo *Workspace no Space* | Com quem a pessoa convive: feed, calendário, academy (`BelongsToTenant`) |
| Tenant do Nexos | `users.nexos_tenant_id` | Nexos, por auto-descoberta (switch *Habilitar*) | Quem responde por NF e reembolso |

No caso Raul/Victor: `nexos_tenant_id = 'vendrix'` (faturamento) e `tenant_id` = o workspace da **Exent**
(convivência). Continuam sendo valores diferentes — e é exatamente por isso que o campo novo **não**
substitui nada do que já foi implementado.

**Onde o valor chega:** em `GET /api/integration/collaborators` (com `X-Tenant-ID`), dentro de
`integrations`:

```jsonc
"integrations": {
  "exent_space_status": 1,
  "exent_space_id": 45,
  "space_workspace_id": "exent",     // ← workspace do Space; null = "o Space decide"
  "space_workspace_name": "Exent",   // rótulo, só para exibição
  "nexos_tenant": "vendrix"          // tenant do header (cache para nexos_tenant_id)
}
```

No Nexos isso vem da tabela central `space_user_workspaces`, chaveada por **e-mail** (é a única chave que
o Space tem no login). Gravada em `CollaboratorController::syncSpaceWorkspace()`
(`app/Http/Controllers/Core/CollaboratorController.php:1356`), exposta em
`CollabController.php:593`.

**O que o Space precisa fazer:**

1. No `SyncNexosUsersJob`, quando `space_workspace_id` vier **não nulo** e diferente de
   `users.tenant_id`, aplicar em `users.tenant_id`.
2. Quando vier `null`, **não mexer** — `null` significa "não definido, o Space decide". Não interprete
   como "limpar".
3. ⚠️ **Cuidado com o `unique(tenant_id, nexos_id)`.** Mudar `tenant_id` pode colidir se já existir uma
   linha com o mesmo `nexos_id` no workspace destino. Trate a violação e reporte em vez de deixar
   estourar dentro do job.
4. ⚠️ **`BelongsToTenant` aplica global scope.** Mover `tenant_id` **muda o que a pessoa enxerga** em
   `FeedPost`, `CalendarEvent` e `AcademyCategory`. O conteúdo antigo dela não migra junto. Para
   Raul/Victor o valor correto é o workspace atual — ou seja, **nada a fazer**; a Fase 2b segue mandando
   não tocar em `tenant_id`.
5. O `id` do workspace no payload é o que **o próprio Space** expôs em `GET /api/nexos/workspaces`
   (`NexosWebhookController::workspaces()`, `backend/routes/api.php:95`), que devolve `tenants.id` e
   `tenants.name`. O Nexos valida o valor contra essa lista antes de gravar, então um id que o Space não
   conheça não deveria chegar. Se a API estiver fora, o Nexos cai num fallback pelos **tenants dele** e
   avisa o operador na tela — nesse cenário um id estranho *pode* chegar. Valide antes de aplicar.

**Ordem:** esta mudança é **independente** das Mudanças 1-5 e **não é pré-requisito do cutover**. Para
Raul/Victor o workspace não muda, então ela pode ficar para depois. Ela existe para o caso geral (alguém
que precise trocar de workspace) e para tirar o `users.tenant_id` da gestão por SQL manual.

---

## Limpeza (baixo esforço, evita recaída)

| Item | Onde | Por quê |
|---|---|---|
| `GOOGLE_ALLOWED_DOMAINS=exent.com.br,vendrix.com.br` | `backend/.env.example:77` (hoje só `exent.com.br`) | Deploy novo a partir do example quebra o login do vendrix |
| `ALLOWED_DOMAIN = '@exent.com.br'` hard-coded | `backend/app/Http/Middleware/EnsureGoogleDomain.php:16` | Middleware inativo e não registrado hoje, mas é armadilha se alguém plugar |
| Apagar `backend/ReembolsoController.php` | raiz de `backend/` | Cópia integral do controller (mesmo namespace, mesmo `env('NEXOS_FINANCE_TENANT')` em `:23`/`:86`/`:169`). Código morto comprovado — o autoload é PSR-4 puro (`composer.json:24-30`) e o classmap resolve para `app/Http/Controllers/Reembolso/`. Existe só para confundir quem for editar |
| `nexos:diagnose` aceitar tenant como argumento | `backend/app/Console/Commands/NexosDiagnose.php:17` | Já faz o `GET integration/collaborators` com bearer + `X-Tenant-ID`; com o argumento, vira a ferramenta de verificação da migração |
| Criar `docs/integracao-nexos.md` | raiz do projeto, junto de `configuracoes.md` | `CLAUDE.md` e os 3 arquivos de `docs/` não mencionam o Nexos. Tudo neste documento saiu de leitura de código |

---

## O que NÃO mudar

- **O token continua único.** `config('nexos.api_token')` é usado tanto outbound (`NexosService.php:310`)
  quanto para autenticar o inbound (`app/Http/Middleware/ValidateIntegrationToken.php:13`). O lado Nexos
  já decidiu **reutilizar o mesmo `exent_space_token`** nos dois tenants — o middleware do Nexos valida o
  bearer contra o tenant do header, então só o `X-Tenant-ID` varia. Token por tenant obrigaria a
  refatorar o inbound também, sem ganho nesta migração.
- **As rotas inbound** (`/api/nexos/profile-sync` e `/api/nexos/user-status`, `backend/routes/api.php:92-95`)
  não recebem `X-Tenant-ID` e não precisam mudar. Só fique ciente da assimetria de chaves:
  `profile-sync` identifica por `id_space` (`NexosWebhookController.php:85`; o `id_nexos` é validado e
  **nunca** conferido contra `users.nexos_id`) e `user-status` identifica por **e-mail** (`:130`, 404 se
  não achar) — por isso o passo 3 da Mudança 5 é obrigatório, senão toda ativação/desativação vinda do
  Nexos se perde em silêncio.
- **O grupo legado `finance/*` do Nexos** não é usado por nenhum código do Space. Não migre nada para lá.

---

## Testes de aceitação

Em homologação, com o tenant `vendrix` configurado no Nexos:

- [ ] **Login exent (regressão):** usuário `@exent.com.br` entra normalmente; `nexos:diagnose` com
      tenant `exent` continua listando os colaboradores de sempre.
- [ ] **Login vendrix:** Raul/Victor com e-mail `@vendrix.com.br` entram sem criar registro novo —
      conferir que `users.id` (o `id_space`) é o mesmo de antes e que `users.nexos_tenant_id = 'vendrix'`.
- [ ] **Convivência preservada (o teste que valida o desacoplamento):** logados como Raul/Victor, o
      **feed, o calendário e a academy continuam mostrando o conteúdo da Exent**, igual aos colegas.
      Confirmar que `users.tenant_id` deles **não** mudou.
- [ ] **Header correto:** log/dump confirmando `X-Tenant-ID: vendrix` no lookup de login, em
      `invoices/get`, `invoices/add`, `refunds/get`, `refunds/add`, `refunds/cancel` e `collaborators/sync`
      para esses usuários — e `exent` para os demais.
- [ ] **Isolamento:** um usuário do exent não vê reembolso/NF do vendrix e vice-versa.
- [ ] **Documentos do perfil** voltam a aparecer (valida `ProfileController.php:83` e `UserController.php:305`).
- [ ] **Inbound:** `POST /api/nexos/user-status` com o e-mail **novo** encontra o usuário e alterna
      `is_active` (hoje daria 404 se o e-mail estivesse dessincronizado).
- [ ] **`config:cache` ligado:** repetir o teste de header com `php artisan config:cache` ativo — é o
      cenário que expõe qualquer `env()` que tenha sobrado.
- [ ] **Sync em lote:** `SyncNexosUsersJob` roda para os dois tenants sem criar usuário duplicado.
- [ ] **`resolve-tenant` sem `source: "map"`:** chamar o endpoint para Raul/Victor e conferir que a
      resposta vem com `"source":"discovery"`. Se algum código do Space ainda ramifica em `"map"`, é
      código morto — remover (ver o aviso na seção de atualização).
- [ ] **Exclusividade:** habilitar a integração do Space para o mesmo e-mail no exent **e** no vendrix
      pela tela do Nexos. O esperado é que o Nexos desabilite sozinho o primeiro e avise na tela; o
      `resolve-tenant` deve continuar respondendo `resolved:true`, **nunca** `ambiguous`.
- [ ] **Workspace (só se a Mudança 6 for implementada):** `GET /api/integration/collaborators` traz
      `integrations.space_workspace_id`; aplicar em `users.tenant_id` não viola
      `unique(tenant_id, nexos_id)` e `null` **não** limpa o valor existente.

---

## Ordem em relação ao runbook do Nexos

As Mudanças 1-4 são **pré-requisito** da Fase 4 do
runbook do Nexos (limpeza no exent). Se a Fase 4 rodar
antes, os dois ficam sem conseguir logar no Space: o login consulta o Nexos com `X-Tenant-ID: exent`,
não encontra os colaboradores desativados e devolve `?error=unauthorized_user`
(`GoogleAuthController.php:85-89`).

Sequência combinada:

| # | Lado | Ação |
|---|------|------|
| 1 | Space | Mudanças 1-4 em homologação + testes de aceitação |
| 2 | Nexos | Fases 0-2 (credenciais, colaboradores no vendrix com o e-mail novo, setup financeiro) |
| 3 | Space | Mudanças 1-4 em produção |
| 4 | Space | Mudança 5 passo 3 — `UPDATE users` (`email`, `nexos_id`, `nexos_tenant_id`), preservando `users.id` e **sem tocar em `tenant_id`** |
| 5 | Ambos | Login dos dois + testes de fumaça |
| 6 | Nexos | Fase 4 (limpeza no exent) — **só depois do passo 5 passar** |

Janela: dia **6 a 24** do mês, por causa do calendário de jobs do Nexos (snapshot dia 25, invoices dia 1).
