0031 — Convite de garantidora por link¶
| Status | CONCLUÍDA |
| Branch | spec/0031-convite-de-garantidora-por-link |
| Criada em | 2026-08-09 |
| Apps afetados | api · web · e2e |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDA
1. Problema¶
A garantidora é o cliente do SaaS: entra por negociação comercial, não por formulário público. Mas a tela de login exibe "Criar minha conta" em destaque, convidando qualquer visitante a se cadastrar como se o produto fosse auto-serviço. Quem chega pelo link do app vê, como segunda opção mais visível da tela, um caminho que não corresponde ao processo real.
E o caminho que corresponde ao processo não existe. Quando o time fecha com uma garantidora nova, não há nada no SaaS admin para dizer "convidei este contato": manda-se a URL do app por WhatsApp e torce-se para a pessoa achar o botão certo. Depois, na lista de Garantidoras, não dá para distinguir quem foi convidado de quem se cadastrou sozinho, nem saber quais convites estão pendentes.
Para usuário esse convite existe desde sempre (/onboarding/users → e-mail → /accept-invite). Para garantidora — a entidade que de fato passa por negociação — não.
2. Objetivo¶
O ADMIN_GLOBAL passa a emitir, no SaaS admin, um link de convite para a garantidora nova — entregue pelo canal que quiser (hoje, WhatsApp, fora do sistema). Quem recebe o link cai no cadastro com o e-mail já preenchido, e o convite fica vinculado à garantidora criada, dando ao time a rastreabilidade que hoje não existe. Ao mesmo tempo, o auto-cadastro deixa de ser anunciado na tela de login.
O que esta spec deliberadamente não faz: fechar a porta. Decisão do usuário em 2026-08-09 —
POST /auth/registercontinua aberto e funcionando sem convite (RN10). Quem souber o endereço/register, ou chamar a API direto, continua criando garantidora sem convite. O ganho aqui é de processo e rastreabilidade — um caminho oficial para convidar, e o registro de quem convidou quem —, não de bloqueio. Se em algum momento a intenção passar a ser impedir o auto-cadastro, isso é outra spec, e ela mexe no endpoint.
3. Fora de escopo¶
- Envio automático por WhatsApp. A entrega é manual: o admin copia o link e manda. A integração com API de WhatsApp é assunto de outra spec — esta só garante que o link seja fácil de copiar.
- Envio do convite por e-mail. Decisão do usuário: o canal é o WhatsApp. Não há disparo de e-mail na emissão. (Ver RN9 para a consequência sobre verificação de e-mail.)
- Convite de usuário da equipe e de imobiliária parceira. Continuam como estão (
/onboarding/users,/onboarding/partner-agencies/{id}/users), por e-mail. Esta spec não os toca. - Aprovação em duas etapas. O convite emitido já é válido; não há fluxo de "solicitar convite" nem revisão por um segundo admin.
- Cobrança, plano ou limite de garantidoras. Nada de comercial entra aqui.
- Migrar tenants existentes. Quem já está na plataforma continua como está.
- Bloquear o auto-cadastro.
POST /auth/registerfica aberto (RN10). Fechar essa porta é decisão de produto que esta spec não toma.
4. Regras de negócio¶
-
RN1. Só
ADMIN_GLOBALemite convite. É o papel do time do SaaS.ADMIN_TENANTe os demais não veem a área de convites — ela vive no admin, que já é restrito. -
RN2. O convite carrega apenas o e-mail do responsável. Decisão do usuário. O admin informa o e-mail de quem vai receber o link (e, opcionalmente, um nome para identificar o convite na lista). Nenhum dado da garantidora é pedido na emissão — quem preenche é o convidado, na tela de onboarding que já existe.
-
RN3. A garantidora nasce no aceite, não na emissão. Um convite pendente não é um tenant: não aparece na lista de Garantidoras, não conta em métrica, não ocupa CNPJ. Só ao concluir o onboarding existe
Tenant.
Consequência aceita: o admin não vê "garantidoras em cadastro" na lista de Garantidoras — vê convites pendentes na lista de Convites. São coisas diferentes, e a spec prefere isso a criar tenant fantasma.
-
RN4. O link expira em 72 horas e é de uso único. Mesma validade do convite de usuário (
INVITE_TOKEN_EXPIRE_HOURS), para não haver dois prazos no produto. Aceito o convite, o token é queimado — reabrir o link mostra "convite já utilizado". -
RN5. Estados do convite.
PENDING→ACCEPTED, ouPENDING→REVOKED.EXPIREDé derivado deexpires_at < agora, não gravado — assim um convite não precisa de rotina de varredura para mudar de estado. -
RN6. Revogar e reemitir. O admin pode revogar um convite
PENDING(o link morre na hora) e emitir outro para o mesmo e-mail. Revogar um convite jáACCEPTEDnão é possível — a garantidora já existe, e desfazê-la é outro assunto. -
RN7. Um convite pendente por e-mail. Emitir para um e-mail que já tem convite
PENDINGsubstitui o anterior (revoga e cria um novo), em vez de acumular dois links válidos para a mesma pessoa. A resposta diz que houve substituição. -
RN8. E-mail já em uso barra a emissão. Se o e-mail já pertence a um usuário da plataforma, a emissão falha com
EMAIL_TAKEN(409). O convite é para quem ainda não tem conta. -
RN9. O e-mail continua sendo verificado — o convite não substitui a verificação. Decisão do usuário em 2026-08-09 (opção b). Quem chega pelo convite percorre o mesmo
verify-emailde sempre: recebe um código no e-mail informado e só então chega ao onboarding. Na prática isto significa não mexer no fluxo — que já verifica.
Por que importa dizer: o link vai por WhatsApp, então o convite prova que a pessoa foi convidada, mas não prova que o e-mail é dela nem que o admin digitou certo. Sem a verificação, um erro de digitação criaria uma garantidora cujo dono não recebe e-mail nenhum — inclusive o de recuperação de senha, deixando a conta sem caminho de volta. Descartado, por isso, marcar o e-mail como verificado no aceite.
Consequência operacional: o e-mail segue no caminho crítico do cadastro. Onde o SMTP não estiver configurado, o fluxo trava no passo 2 — ver o aviso em §5.
- RN10.
POST /auth/registercontinua aberto, exatamente como está. Decisão do usuário em 2026-08-09, corrigindo a leitura anterior desta spec. O endpoint não passa a exigir convite, não ganha erroINVITE_REQUIREDe não muda de comportamento para quem chama sem token. Quem já usa esse caminho — inclusive a suíte de testes — segue funcionando sem alteração.
O único parâmetro novo é invite_token, opcional. Presente e válido, vincula o cadastro ao convite (RN13). Ausente, o registro corre como sempre correu.
- RN11. Sai o CTA; a rota
/registerfica e continua cadastrando. O botão "Criar minha conta" some da tela de login — essa é a única remoção. O endereço/registercontinua existindo e continua permitindo cadastro sem convite; quem tem o link salvo não cai em 404 nem em porta fechada.
A tela ganha um comportamento a mais, não um bloqueio:
| URL | O que a tela faz |
|---|---|
/register | Formulário de cadastro como hoje — nome, e-mail, senha, tudo editável |
/register?token=<válido> | Mesmo formulário, com o e-mail do convite preenchido e travado, e o convite vinculado ao concluir |
/register?token=<inválido> | Aviso de que o link não vale mais (motivo específico) e o formulário normal logo abaixo — o cadastro não é impedido |
- RN12. Convite inválido não bloqueia cadastro. Token expirado, revogado, já usado ou inexistente produz aviso, não parede: a pessoa segue pelo cadastro comum. O que se perde é o vínculo com o convite, não o acesso.
Por que: com o auto-cadastro aberto (RN10), barrar quem chegou por um link vencido seria pior que a ausência do link — a mesma pessoa consegue se cadastrar apagando o ?token= da URL. Uma parede que se contorna assim só ensina a contorná-la.
-
RN13. O convite é vinculado, não exigido. Concluído o onboarding com
invite_tokenválido, o convite viraACCEPTEDe recebetenant_id— é isso que dá ao admin a rastreabilidade de quem convidou quem. Sem token, nasce uma garantidora sem convite associado, como hoje. -
RN14. Erros previstos.
INVITE_NOT_FOUND(404),INVITE_EXPIRED(410),INVITE_ALREADY_USED(410),INVITE_REVOKED(410) — todos apenas noGET /tenant-invites/validate, para a tela saber o que dizer.EMAIL_TAKEN(409) na emissão. Nenhum deles sai do/auth/register, que não ganha erro novo (RN10).
5. Impacto por app¶
apps/api¶
Modelo novo — TenantInvite (app/models/tenant_invite.py, tabela tenant_invites):
| Campo | Tipo | Nota |
|---|---|---|
id | UUID | UUIDMixin |
email | String(255) | e-mail do convidado, indexado |
contact_name | String(255) nullable | rótulo para identificar o convite na lista |
token | String(64) | UUID4, unique, indexado — é o que vai na URL |
status | enum TenantInviteStatus | PENDING · ACCEPTED · REVOKED (RN5) |
expires_at | timestamptz | emissão + INVITE_TOKEN_EXPIRE_HOURS |
accepted_at | timestamptz nullable | |
revoked_at | timestamptz nullable | |
invited_by_user_id | FK users.id | quem emitiu, ondelete=RESTRICT |
tenant_id | FK tenants.id nullable | preenchido no aceite — liga convite ↔ garantidora |
created_at / updated_at | TimestampMixin |
Por que não reaproveitar
VerificationCode. Ele temuser_idNOT NULL com FK parausers, e o convite de garantidora não tem usuário nem tenant no momento da emissão. Reaproveitá-lo exigiria tornaruser_idnullable, enfraquecendo o modelo para os três tipos que hoje dependem dele (EMAIL_VERIFICATION,USER_INVITE,PASSWORD_RESET). Tabela própria custa uma migration e mantém as duas invariantes.
Sem TenantMixin: o convite é da plataforma, não de um tenant — ele existe justamente antes de haver tenant. É a segunda entidade nessa condição, ao lado de Tenant.
Endpoints — router novo app/api/v1/routers/tenant_invites.py, prefixo /tenant-invites, tag Convites de Garantidora. Os três primeiros exigem ADMIN_GLOBAL; os dois últimos são públicos (quem os usa ainda não tem conta):
| Método | Path | Permissão | O quê |
|---|---|---|---|
POST | /tenant-invites | ADMIN_GLOBAL | Emite (RN2, RN7, RN8) |
GET | /tenant-invites | ADMIN_GLOBAL | Lista, com filtro por status |
POST | /tenant-invites/{id}/revoke | ADMIN_GLOBAL | Revoga (RN6) |
GET | /tenant-invites/validate | público | Valida ?token= antes de exibir o formulário |
POST | /auth/register | público, inalterado | Ganha só invite_token opcional (RN10) |
Regras no service (tenant_invite_service.py): emissão com revogação do pendente anterior (RN7), validação de e-mail livre (RN8), e o consumo do token dentro do fluxo de registro. O accept não ganha endpoint próprio: o convidado percorre o caminho que já existe, agora com token e sem atalho na verificação (RN9):
link do WhatsApp → /register?token=… → verify-email (código) → /onboarding → Tenant
POST /auth/register (invite_token opcional) POST /auth/verify-email POST /onboarding/tenant
É o mesmo fluxo de hoje, com um parâmetro opcional a mais no primeiro passo. Sem token, tudo corre como sempre correu. O convite vira ACCEPTED e recebe tenant_id no último passo, quando o tenant é criado.
Ponto de atenção na implementação: o convite só pode ser marcado
ACCEPTEDnocreate_tenant, não noregister. Se marcasse no registro, alguém que criasse a conta e abandonasse o onboarding queimaria o convite sem garantidora nenhuma — e precisaria de um convite novo para terminar. Entreregisterecreate_tenanto token fica reservado (used_by_user_idpreenchido, status aindaPENDING).
Auditoria: emissão, revogação e aceite entram no AuditLog com entity_type="tenant_invite".
Testes — tests/test_tenant_invites.py: emissão feliz; EMAIL_TAKEN; substituição do pendente (RN7); revogação; token expirado, revogado e já usado; /auth/register sem token recusado (RN10/RN11); fluxo completo convite → registro → tenant → convite ACCEPTED com tenant_id; e que ADMIN_TENANT recebe 403 na emissão (RN1).
Documentação: docstrings de módulo/classe/método no modelo, service e repo; summary/description/response_description em todo decorator; entrada nova em TAGS_METADATA (main.py); e o fluxo principal do DESCRIPTION deixa de começar por "Cadastrar Tenant" para começar pelo convite.
apps/web¶
Admin — página nova Convites (admin/pages/invites/):
- Item no
AdminSider.tsx, seção PLATAFORMA, entre Garantidoras e Usuários (ordem do processo: convido → vira garantidora). Íconeenvelope-simple(Phosphor, já disponível peloicon/map.ts). - Rota
/admin/invitesemApp.tsx. - Lista com e-mail, nome de contato, status, emitido em, expira em, e quem emitiu. Filtro por status. Segue o padrão de lista do design system (
FilterBar+DataGrid), como a de Garantidoras. - Botão "Convidar garantidora" → diálogo com e-mail e nome de contato.
- Ao emitir, o diálogo mostra o link pronto com botão "Copiar link" — é o artefato que vai para o WhatsApp. Copiar também disponível na linha da lista, enquanto
PENDING. - "Revogar" por linha, com confirmação (ação destrutiva).
Login — remover o bloco "Ainda não tem a Garantora?" com o CTA login-register-cta (pages/login/index.tsx). O painel lateral de marketing continua.
Rota /register — permanece em App.tsx, e a pasta pages/register/ não é reescrita: o formulário atual continua sendo o formulário. O que entra é a leitura do ?token= (RN11).
Com token válido: chama GET /tenant-invites/validate, preenche o e-mail e o trava, e mostra uma faixa discreta identificando o convite ("Convite para carlos@…"). Ao enviar, repassa invite_token; daí em diante é o fluxo de sempre — verificação de e-mail (RN9) e onboarding.
Com token inválido: faixa de aviso com o motivo (expirado, revogado, já usado, inexistente) e o formulário normal logo abaixo, utilizável (RN12). Sem token: a tela é exatamente a de hoje.
Como o formulário é o mesmo nos três casos, o diff do
pages/register/é pequeno — o grosso do trabalho do web está na página de Convites do admin.O e-mail está no caminho crítico (RN9), e a produção ainda tem SMTP placeholder — a própria spec 0028 registra isso em §9. Enquanto o SMTP real não for configurado em
srv01, o convidado conclui o passo 1 e trava no passo 2, sem receber o código. Não é defeito desta spec, mas é bloqueio para usá-la em produção: vale conferir o SMTP antes de emitir o primeiro convite real. No local e no dev-remote, o Mailpit resolve.
data-testid: invites-new-button, invite-create-dialog, invite-email-input, invite-contact-name-input, invite-create-confirm, invite-create-cancel, invite-copy-link-{id}, invite-revoke-{id}, invite-revoke-confirm, invites-status-filter, invites-row-{id}, invites-error, register-invite-form, register-invite-name-input, register-invite-password-input, register-invite-submit, register-invite-banner, register-invite-invalid.
Prefixo
register-invite-, e nãoaccept-invite-:/accept-invitejá é a tela do convite de usuário da equipe, que continua existindo (§3). Dois conjuntos de testids parecidos para telas diferentes é armadilha para quem escrever o próximo spec.
Responsividade & PWA: a lista de Convites vira cartões no mobile (RecordCard), como manda o padrão — não <Table>, para não repetir a dívida da tela de Garantias. O diálogo de emissão é coluna única em 375px. O botão "Copiar link" precisa de atenção no mobile: navigator.clipboard exige contexto seguro (há HTTPS nos três ambientes) e pode ser negado — havendo falha, o link aparece em campo selecionável, para não deixar o admin sem saída. O /register é público e precisa funcionar bem no celular, que é onde o link do WhatsApp abre — inclusive a faixa do convite, que não pode empurrar o formulário para fora da primeira dobra em 375px. PWA: sem asset novo e nenhuma rota removida, então atalho salvo e cache do service worker seguem válidos.
apps/e2e¶
Specs — tenant-invite.spec.ts:
ADMIN_GLOBALemite convite, o link aparece e é copiável;- o link leva à tela de aceite com o e-mail preenchido e travado;
- o cadastro conclui e a garantidora aparece em
/admin/tenants; - reabrir o mesmo link mostra "já utilizado" (RN4);
- convite revogado não deixa cadastrar (RN6);
- a tela de login não tem
login-register-cta, e/app/registersem token abre o formulário normal e permite cadastrar (RN10, RN11); - sem verificar o e-mail, o onboarding recusa criar a garantidora (RN9).
Seeds — sem dado novo; o convite é criado pelo próprio teste via API. Atenção ao tenant de e2e: o fluxo cria garantidora nova a cada execução, então o CNPJ e o e-mail precisam ser gerados únicos por execução — a mesma armadilha que o fix 0006 registrou (CNPJ fixo passou na primeira execução e deu 409 nas quatro seguintes).
Verificação de e-mail no teste: reaproveitar fixtures/mailpit (clearMailbox, waitForEmail, extractLink), que o password-reset.spec.ts já usa — é como o código de verificação chega ao teste sem mock.
6. Contrato de API¶
Emitir (ADMIN_GLOBAL)
POST /api/v1/tenant-invites
Authorization: Bearer <token>
{ "email": "carlos@novagarantidora.com.br", "contact_name": "Carlos Silva" }
{
"id": "uuid",
"email": "carlos@novagarantidora.com.br",
"contact_name": "Carlos Silva",
"status": "PENDING",
"invite_url": "https://app.garantora.com.br/register?token=8f3a…",
"expires_at": "2026-08-12T14:03:00Z",
"replaced_previous": false
}
invite_urlé montada comAPP_BASE_URL, que difere por ambiente —/appno local e no dev-remote, raiz na produção (VITE_BASE_PATH, spec 0028). Montar no servidor evita que o front tenha de adivinhar a base.
Validar (público)
Inválido → 404 INVITE_NOT_FOUND ou 410 com INVITE_EXPIRED / INVITE_REVOKED / INVITE_ALREADY_USED.
Registrar (público, inalterado — invite_token é opcional)
POST /api/v1/auth/register
{ "name": "Carlos Silva", "email": "carlos@…", "password": "…", "invite_token": "8f3a…" }
Sem invite_token a chamada é idêntica à de hoje e o comportamento não muda (RN10): nenhum erro novo, nenhum campo obrigatório novo. Com invite_token válido, o cadastro fica vinculado ao convite; o email precisa bater com o do convite (divergindo → 422 EMAIL_MISMATCH). Com token inválido, o registro prossegue e o vínculo é ignorado (RN12).
Revogar e listar seguem o padrão do resto do admin.
7. Critérios de aceite¶
- [x] CA1. Dado
ADMIN_GLOBALem/admin/invites, quando emite um convite para um e-mail livre, então o convite aparece comoPENDINGe o diálogo mostra um link copiável. - [x] CA2. Dado esse link aberto em outro navegador (sem sessão), então
/registermostra o e-mail do convite preenchido e não editável, e pede nome e senha. - [x] CA3. Dado o cadastro concluído — incluindo a verificação do e-mail com o código recebido (RN9) —, então existe uma garantidora nova em
/admin/tenants, e o convite passa aACCEPTEDcomtenant_idpreenchido. - [ ] CA3b. Dado o convidado que definiu a senha mas não verificou o e-mail, então ele não consegue criar a garantidora:
/onboarding/tenantrecusa comEMAIL_NOT_VERIFIED. O convite seguePENDING. - [x] CA4. Dado um link já utilizado, quando reaberto, então a tela diz "convite já utilizado" — não um erro genérico — e o formulário de cadastro continua utilizável logo abaixo (RN12).
- [x] CA5. Dado um convite
PENDINGrevogado pelo admin, quando o link é aberto, então diz "convite revogado", e o cadastro sem vínculo segue possível. - [x] CA6. Dado um convite com
expires_atno passado, então a tela diz "convite expirado", e o convite consta como expirado na lista sem que nenhuma rotina o tenha alterado (RN5). - [x] CA7. Dada a tela de login, então não existe o CTA "Criar minha conta"; e
/app/registersem token abre o formulário de cadastro igual ao de hoje (RN11). - [x] CA8. Dado
POST /api/v1/auth/registerseminvite_token, então o comportamento é idêntico ao de antes desta spec — cadastro criado, nenhum erro novo (RN10). É o critério que protege a suíte existente. - [x] CA9. Dado um e-mail que já é de um usuário da plataforma, quando o admin tenta convidar, então recebe 409
EMAIL_TAKENe nenhum convite é criado. - [x] CA10. Dado um e-mail com convite
PENDING, quando o admin emite outro, então o anterior ficaREVOKED, existe um único convite válido, e a resposta trazreplaced_previous: true(RN7). - [x] CA11. Dado um
ADMIN_TENANTautenticado, quando chamaPOST /tenant-invites, então recebe 403; e o item Convites não aparece no menu dele. - [x] CA12. Dado que o convidado cria a conta mas abandona antes de concluir o onboarding, quando volta pelo mesmo link, então ainda consegue terminar — o convite não foi queimado no registro.
- [ ] CA12b. Dado um cadastro feito sem convite, então a garantidora nasce normalmente e nenhum convite é vinculado a ela (RN13).
- [x] CA13. Em 375px e em desktop: a lista de Convites não rola na horizontal (
scrollWidth - clientWidth == 0), vira cartões no mobile, e a tela de aceite é utilizável no celular. - [x] CA14.
make testpassa com todos os testes anteriores mais os novos;tsclimpo;ruffsem apontamento novo nos arquivos tocados.
8. Plano de implementação¶
Ordem obrigatória: api → web → e2e.
- [x]
apps/api— modeloTenantInvite+ enumTenantInviteStatus - [x]
apps/api— migration Alembic (tabela + índices + unique emtoken), comdowngrade - [x]
apps/api— schemas, repository,tenant_invite_service - [x]
apps/api— router/tenant-invites(3 privados +validatepúblico) - [x]
apps/api—auth/registerganhainvite_tokenopcional (RN10) — sem quebrar quem chama sem ele - [x]
apps/api—create_tenantmarca o conviteACCEPTEDe gravatenant_id - [x]
apps/api— auditoria dos três eventos - [x]
apps/api— docstrings + Swagger +TAGS_METADATA - [x]
apps/api—tests/test_tenant_invites.py - [x]
apps/api—make testverde antes de tocar o web - [x]
apps/web— página/admin/invites(lista, emitir, copiar, revogar) +AdminSider - [x]
apps/web—/registerlê?token=: e-mail travado quando válido, faixa de aviso quando inválido, tela de hoje quando ausente - [x]
apps/web— remover apenas o CTA do login (rota e formulário permanecem) - [x]
apps/web—data-testidem tudo; cartões no mobile - [x]
apps/e2e—tenant-invite.spec.ts(8 cenários, com Mailpit) — executado, 8/8 - [x]
make testemake lint(comparar antes/depois, não o total) - [x]
/devremote-rebuild+ screenshot nos dois viewports (CA13)
9. Decisões em aberto¶
Nenhuma. As duas que havia foram decididas pelo usuário em 2026-08-09:
- [x] Verificação de e-mail: opção (b). O passo de
verify-emailfica depois do aceite (RN9). Descartado marcar como verificado no aceite — o link vai por WhatsApp e não prova o endereço, e um erro de digitação do admin produziria uma conta sem caminho de volta. Descartado também verificar só antes da primeira ação sensível: adiaria o problema para o momento em que ele custa mais caro. - [x] Rota
/register: fica, só o CTA sai (RN11). Descartado removê-la — quem tiver o link salvo cairia num 404 —, e descartado criar/conviteem paralelo, que daria dois endereços para a mesma tela. -
[x]
POST /auth/registercontinua aberto e sem convite (RN10). Uma versão anterior desta spec o fazia exigirinvite_token, apoiada numa suposição declarada sobre o que "manter a rota" queria dizer. A suposição estava errada, e o usuário a corrigiu em 2026-08-09: o endpoint fica como está, einvite_tokenentra apenas como parâmetro opcional.O que a correção muda no alcance da spec: ela deixa de ser um portão e passa a ser processo + rastreabilidade. Quem souber o endereço continua se cadastrando sozinho — está dito na §2 e na §3, para ninguém ler o título e supor bloqueio. Efeito colateral bem-vindo: a suíte existente, que cria conta por
/auth/register, não precisa de ajuste (CA8).
10. Registro¶
Implementada em 2026-08-09.
O que a spec não previa¶
| Achado | O que aconteceu |
|---|---|
| O sider do admin quebrava o mobile — em todas as telas | O AdminSider usava Drawer variant="permanent", reservando 232px de uma tela de 375px. Sobravam ~55px úteis e as quatro telas do admin rolavam na horizontal (medido: overflow=21 em Painel, Garantidoras, Usuários e Convites). É dívida anterior a esta spec, mas a página nova a tornou intolerável: uma lista de convites em 55px não é usável. Corrigido: gaveta temporary no mobile, fechada ao abrir a tela, com botão de menu no AppBar (admin-menu-toggle). Escopo adjacente assumido de propósito — entregar a página nova quebrada seria pior que tocar o layout. |
datetime naive × aware | is_expired comparava datetime.now(timezone.utc) com expires_at, e o SQLite dos testes não guarda timezone: a coluna é DateTime(timezone=True), o PostgreSQL devolve aware e o SQLite devolve naive — TypeError em 8 testes. O VerificationCode nunca esbarrou nisso porque compara datas no SQL, não em Python. A property passou a normalizar para UTC antes de comparar. |
Faltava o ícone copy | O registry de ícones é curado (76 entradas) e não tinha o conceito de copiar — que é a ação central da tela. Adicionado copy (Phosphor), como o /journeys fez com os que precisou. |
| E-mail longo estourava a faixa | Como palavra única sem quebra, o e-mail do convite empurrava a página em 5px no mobile. wordBreak: break-word na faixa. |
Divergências de implementação¶
ConfirmDialogePrimaryActiontêm contratos diferentes do que supus (description/onClose, etestIdem vez dedata-testid). Ajustado ao que existe, em vez de estender os componentes compartilhados.StatusTonenão temneutral— o vocabulário édefault | info | success | warning | error. "Expirado" usadefault.- O
invite_urlé reescrito para a base sob teste no e2e. A API monta comAPP_BASE_URL, que pode apontar para outro ambiente; o spec extrai o token e remonta a URL no alvo.
Verificação¶
- API: 244 testes (230 do baseline + 14 novos). Execução final: 243 passam, 1 falha — o flake conhecido de
test_partner_agency_status_filter.py, em arquivo que esta spec não toca.Numa das execuções apareceu um
ERRORde setup emtest_email_ja_cadastrado_barra_emissaoque não reproduziu nas duas seguintes. A execução afetada rodava em paralelo com o script de screenshot, que batia na mesma API — contenção provável. Fica registrado por honestidade; se reaparecer, é sintoma, não ruído. - Migration: aplicada, revertida e reaplicada no PostgreSQL do dev-remote. O
downgradederruba tabela e enum — sem isso oupgradeseguinte falharia com "type tenant_invite_status already exists". - Ruff: 251 contra 249 do baseline — +2, e os dois são os
F821de forward-refMapped["User"]/Mapped["Tenant"]do model novo: o mesmo falso positivo que todos os models do projeto têm, documentado noCLAUDE.md. tsc: limpo.- e2e: executada a pedido do usuário — 159 passam, 0 falham (7,9 min), sendo 8 do
tenant-invite.spec.ts. Os 2skippedsão os semeadores em massa, já marcadostest.skipno repositório.
A suíte e2e foi executada, a pedido do usuário¶
O tenant-invite.spec.ts rodou contra o dev-remote reconstruído. Duas correções saíram daí, ambas de teste, não de produto:
- O CA5 vivia no
describedo admin, com sessão ativa.goto('/register')autenticado redireciona para o painel, e o teste media o dashboard. É a pegadinha inversa da que oCLAUDE.mddo e2e documenta (lá,gotoem rota protegida perde a sessão; aqui,gotoem rota pública com sessão sai da tela). Movido para odescribesem sessão. - O CA3 prometia mais do que entregava. Chamava-se "a garantidora nasce" e parava no
verify-email. Foi estendido até o fim: extrai o OTP do Mailpit, verifica o e-mail, cria a garantidora e confere que o convite ficouACCEPTEDcom otenant_idda garantidora criada, e que o link passa a responder 410.
Isso fecha a maior lacuna que esta entrega tinha: o link_tenant é o único ponto que nenhum teste de API alcança sozinho — lá o convite só chega a ser reservado. Agora o consumo é exercido de verdade.
O payload do onboarding exige
phone_mobile, o que só apareceu na execução. A mensagem de erro que eu havia posto noexpectmostrou o campo exato — foi o que tornou a correção imediata em vez de investigativa.
Erros meus no caminho¶
- Rodei a suíte dentro do container e disparei o rebuild em paralelo. O rebuild recriou o container no meio do pytest e a execução morreu sem resultado — 4 minutos perdidos. Suíte e rebuild disputam o mesmo container; não podem ser paralelos.
- Um teste de papel que não testava papel.
test_admin_tenant_nao_emitemandava um token inválido e conferia 401 — o que prova apenas que a rota exige autenticação. Passaria mesmo se qualquer usuário logado pudesse emitir convite. Reescrito para autenticar um usuário real semADMIN_GLOBALe exigir 403. - Comparei coluna UUID com string nos testes (
TenantInvite.id == invite["id"]), o mesmo erro que cometi na spec 0027 quatro horas antes.