# Rota `main`, view `view_lotacao_professor` e cache de lotação

Implementação da especificação em `docs/003 - Tela Inicial.md`: rota autenticada que devolve a
lotação do professor (ano > escolas > turmas > séries > disciplinas), lida da view
`view_lotacao_professor` e armazenada em cache do usuário, invalidado a cada login/logout.

## 1. Endpoints

| Método | Rota | Middleware | Descrição |
|---|---|---|---|
| GET | `/api/v1/main` | `identify.tenant`, `auth:sanctum` | Retorna a lotação do professor autenticado (cache ou view) |
| POST | `/api/v1/logout` | `identify.tenant`, `auth:sanctum` | Revoga o token atual e limpa o cache de lotação do usuário |

`POST /api/v1/login` passou a limpar o cache de lotação do usuário que está autenticando, para
garantir que o próximo `GET /main` sempre releia a view.

## 2. Fluxo de dados

```
GET /api/v1/main
  → identify.tenant       (resolve o tenant, seta request->attributes['tenant_id'])
    → auth:sanctum        (autentica via token, usando o model de tenant, ver seção 4)
      → MainController::index
        → LotacaoProfessorServiceContract::getLotacaoProfessor(tenantId, id_usuario)
          → Cache::rememberForever("tenant:{tenantId}:lotacao_professor:{id_usuario}")
            → LotacaoProfessorRepositoryInterface::findByUsuario(id_usuario)
              → Model LotacaoProfessor (view_lotacao_professor, conexão tenant)
          → monta a árvore ano > escolas > turmas > series > disciplinas
        ← JSON
```

O cache é por tenant + usuário (`tenant:{tenantId}:lotacao_professor:{id_usuario}`), sem TTL
(`rememberForever`), e só é removido explicitamente:
- no login (`AuthService::authenticate`), antes de emitir o novo token;
- no logout (`AuthService::logout`), ao revogar o token.

## 3. Arquivos criados

| Arquivo | Camada | O que faz |
|---|---|---|
| `app/Models/Tenant/LotacaoProfessor.php` | Model | Mapeia a view `view_lotacao_professor` (conexão `tenant`, sem PK/timestamps, somente leitura) |
| `app/Repositories/Contracts/LotacaoProfessorRepositoryInterface.php` | Repository (contrato) | Define `findByUsuario(idUsuario)` |
| `app/Repositories/EloquentLotacaoProfessorRepository.php` | Repository | Consulta a view filtrando por `id_usuario`, ordenando como no `ORDER BY` original |
| `app/Services/Contracts/LotacaoProfessorServiceContract.php` | Service (contrato) | Define `getLotacaoProfessor()` e `forgetLotacaoProfessorCache()` |
| `app/Services/LotacaoProfessorService.php` | Service | Cache (`Cache::rememberForever` / `Cache::forget`) + montagem da árvore JSON a partir das linhas planas da view |
| `app/Http/Controllers/Api/V1/MainController.php` | Controller | `index()`: resolve `tenant_id` e usuário autenticado, delega ao Service, devolve JSON |
| `app/Models/Tenant/PersonalAccessToken.php` | Model | Sobrescreve o model de token do Sanctum para usar a conexão `tenant` (ver seção 4) |
| `database/tenant/migrations/2026_08_06_090000_add_performance_indexes_for_lotacao_professor.php` | Migration | Cria os 6 índices de performance pedidos para as tabelas usadas pela view |
| `database/tenant/migrations/2026_08_06_090100_create_view_lotacao_professor.php` | Migration | `CREATE OR REPLACE VIEW view_lotacao_professor` com a consulta fornecida (CTEs `regente`, `professor`, `via_horario`, `via_substituicao`, `via_regente` + união com AEE/Ativ. Complementar) |
| `docs/004 - Rota Main e Cache de Lotacao.md` | Documentação | Este arquivo |
| `tests/Feature/Api/V1/TenantApiTestCase.php` | Teste (base) | Prepara um tenant fake na conexão `central` (sqlite em memória) para o `identify.tenant` resolver nos testes de rota |
| `tests/Feature/Api/V1/AuthControllerTest.php` | Teste (Feature) | `POST /login` e `POST /logout`: tenant ausente/inexistente, validação, credenciais inválidas, sucesso, autenticação ausente |
| `tests/Feature/Api/V1/MainControllerTest.php` | Teste (Feature) | `GET /main`: tenant ausente, autenticação ausente, resposta com o usuário autenticado |
| `tests/Unit/Services/AuthServiceTest.php` | Teste (Unit) | `AuthService::authenticate()`/`logout()`: credenciais, usuário inativo, criação/revogação real de token Sanctum (sqlite isolado) e limpeza de cache |
| `tests/Unit/Services/LotacaoProfessorServiceTest.php` | Teste (Unit) | Montagem da árvore ano/escolas/turmas/séries/disciplinas a partir de linhas planas, cache (hit/forget) e isolamento por tenant+usuário |

## 4. Arquivos modificados

| Arquivo | Mudança | Motivo |
|---|---|---|
| `routes/api.php` | Adiciona grupo `auth:sanctum` com `GET /main` e `POST /logout` | Rota exige login, conforme guia em `docs/002` |
| `app/Http/Middleware/IdentifyTenant.php` | Seta `request->attributes->set('tenant_id', $tenant->id)` | Necessário para montar a chave de cache (`tenant:{id}:...`) sem reler header/query em cada camada |
| `app/DTOs/AuthLoginDTO.php` | Novo campo `tenantId`, lido de `request->attributes` | `AuthService::authenticate` precisa do tenant para limpar o cache do usuário certo |
| `app/Services/Contracts/AuthServiceContract.php` | Novo método `logout(User $user, string $tenantId): void` | Endpoint de logout |
| `app/Services/AuthService.php` | Injeta `LotacaoProfessorServiceContract`; `authenticate()` limpa o cache antes de emitir o token; novo `logout()` que limpa o cache e revoga o token atual | Regra "limpar cache no login e no logout" pedida na especificação |
| `app/Http/Controllers/Api/V1/AuthController.php` | Novo método `logout(Request $request)` | Expõe `POST /api/v1/logout` |
| `app/Providers/AppServiceProvider.php` | Novos bindings (`LotacaoProfessorRepositoryInterface` → `EloquentLotacaoProfessorRepository`, `LotacaoProfessorServiceContract` → `LotacaoProfessorService`); `boot()` chama `Sanctum::usePersonalAccessTokenModel(...)` | Injeção de dependência das novas camadas + correção descrita abaixo |

### Correção necessária: tokens do Sanctum na conexão do tenant

A tabela `personal_access_tokens` só existe na conexão `tenant` (migration
`2026_08_05_154813_create_personal_access_tokens_table.php`). O model padrão do pacote Sanctum,
porém, usa a conexão default da aplicação (`central`, ver `config/database.php`). Sem a
sobrescrita feita em `App\Models\Tenant\PersonalAccessToken` + `Sanctum::usePersonalAccessTokenModel()`,
tanto a criação do token no login (`$user->createToken(...)`) quanto sua leitura pelo middleware
`auth:sanctum` iriam olhar para a tabela errada, e nenhuma rota protegida funcionaria. Essa
correção foi necessária para que `GET /api/v1/main` (e qualquer rota `auth:sanctum` futura)
funcione de fato.

## 5. Migrations

Rodar (banco template do `.env`):

```
php artisan migrate --database=tenant --path=database/tenant/migrations
```

Para todos os tenants cadastrados:

```
php artisan tenants:migrate
```

A migration de índices (`2026_08_06_090000_...`) roda antes da migration da view
(`2026_08_06_090100_...`) — os índices não são um pré-requisito técnico para criar a view, mas
mantê-los antes deixa claro que servem para acelerar as mesmas tabelas que a view consulta.

## 6. Testes

```
composer test
# ou
php artisan test tests/Feature/Api/V1 tests/Unit/Services
```

Estratégia usada (as conexões `central`/`tenant` reais são Postgres, indisponíveis no ambiente
de teste):

- **Feature** (`tests/Feature/Api/V1`): sobem só a tabela `tenants` num sqlite em memória, o
  suficiente para o `identify.tenant` funcionar de verdade. `AuthServiceContract` e
  `LotacaoProfessorServiceContract` são mockados (`$this->mock(...)`) — o que valida é a rota em
  si: status code, middleware de tenant/autenticação, validação do `LoginRequest` e o formato da
  resposta. Rotas protegidas usam `Laravel\Sanctum\Sanctum::actingAs()`, que autentica sem
  precisar de um token real gravado em banco.
- **Unit** (`tests/Unit/Services`): exercitam a regra de negócio de verdade.
  `LotacaoProfessorServiceTest` usa o cache real (`CACHE_STORE=array` no `phpunit.xml`) com o
  Repository mockado, verificando a árvore montada e o hit/miss de cache. `AuthServiceTest` isola
  a conexão `tenant` num sqlite em memória só com `personal_access_tokens`, para que
  `$user->createToken()` / `currentAccessToken()->delete()` do Sanctum rodem de verdade.

## 7. Formato de resposta de `GET /api/v1/main`

```json
[
  {
    "ano": 2026,
    "escolas": [
      {
        "escola": "NOME DA ESCOLA",
        "escola_id": 1,
        "turmas": [
          {
            "turma": "NOME DA TURMA",
            "tipo": "REGULAR",
            "turma_id": 10,
            "series": [
              {
                "serie": "NOME DA SERIE",
                "serie_id": 5,
                "disciplinas": [
                  { "disciplina": "NOME DA DISCIPLINA", "regencia_id": 123 }
                ]
              }
            ]
          }
        ]
      }
    ],
    "rechumano_id": [456]
  }
]
```

Cada item do ano recebe, após `escolas`, o campo `rechumano_id`: a lista (sem duplicados) dos
`id_rechumano` (view `view_lotacao_professor`) do professor autenticado naquele ano. O app (front)
deve armazenar esses ids para usá-los em consultas futuras, sem precisar percorrer a árvore de
escolas/turmas/séries para descobri-los.
