Decisões de arquitetura¶
Cada decisão abaixo foi tomada uma vez, por um motivo, e vale até ser substituída por outra decisão registrada. Elas existem para que a mesma discussão não volte a cada feature.
A fonte é .specs/STATE.md, no repositório. Esta página é um espelho: quando uma decisão
muda, muda lá primeiro, e aqui depois.
Como ler o estado¶
| Estado | Significa |
|---|---|
active |
Vale agora, sem ressalva |
superseded by AD-xxx |
Substituída; a outra é que vale |
scoped by AD-xxx |
Continua valendo onde nasceu, mas outra decisão limitou seu alcance |
As quatro que mais pegam quem está chegando
AD-020 para autorização, AD-017 para venda, AD-009 para tela de cadastro e AD-004 para qualquer aviso na interface.
Índice¶
| # | Decisão | Área | Estado |
|---|---|---|---|
| AD-001 | Existe um único motor de sincronização ClickUp (ClickUpSyncService + tabelas clickup_*) |
backend/integração | active |
| AD-002 | A representação canônica de qualquer campo sincronizado é string | backend/integração | active |
| AD-003 | Taxonomia de domínio espelhada de sistemas externos vive em tabelas seedadas, servida ao cli… | full-stack | active |
| AD-004 | Feedback visual usa exclusivamente o KP Notify System (KPToast, KPPush, useKPAlert) |
frontend | active |
| AD-005 | Modais de cadastro/edição seguem o padrão canônico de modal KP (overlay `rgba(25,23,21,0.55)… | frontend | superseded by AD-009 |
| AD-006 | Estado de filtro/busca/drawer de telas de listagem vive na URL (searchParams), não em cont… |
frontend | active |
| AD-007 | Importação e sincronização são motores distintos e isolados | backend/integração | active |
| AD-008 | A semântica de uma etapa vem do campo role, nunca do slug ou da posição (sort) |
backend/domínio | active |
| AD-009 | Telas de cadastro/edição usam cortina lateral (KPDrawer) |
frontend | active |
| AD-010 | Permissões novas ou renomeadas são criadas por migration idempotente, nunca só por seeder | backend/deploy | active |
| AD-011 | Arquivo que contenha dado pessoal (mídia de conversa, documento de aluno, foto de contato) vive … | backend/segurança | active |
| AD-012 | O servidor Evolution do KP é próprio e dedicado | infra/integração | active |
| AD-013 | A chave global do Evolution mora no .env, nunca no banco nem na UI, e é usada exclusivamente p… |
backend/segurança | active |
| AD-014 | Entrega importada da planilha não vira compromisso na agenda | backend/importação | scoped by AD-019 |
| AD-015 | Ação disparada por mensagem de WhatsApp é implementada como um efeito que cumpre o contrato `Act… | backend/domínio | active |
| AD-016 | Autorização sobre uma instância de dado usa tabela de vínculo própria, nunca a tabela permissions |
backend/domínio | active |
| AD-017 | O que foi vendido é dado da venda, não do catálogo | backend/domínio | active; segunda metade (mentor) scoped by AD-019 |
| AD-018 | Prazo de contrato tem duas datas: referência e real | backend/domínio | active |
| AD-019 | Importação que traz tutor, hora e duração vira compromisso na agenda, e o especialista da planil… | backend/domínio | active |
| AD-020 | Autorização para agir sobre um registro vem do vínculo com aquele registro, não de uma permissão… | backend/segurança | active |
| AD-021 | Tracking é instrumentação, e instrumentação nunca bloqueia a operação que instrumenta | backend/domínio | active |
| AD-022 | O provedor de IA é configuração, e o dialeto do provedor é o discriminador | backend/integração | active |
AD-001¶
Área: backend/integração · Estado: active · Desde: 2026-08-01
Existe um único motor de sincronização ClickUp (ClickUpSyncService + tabelas clickup_*). Novas áreas se integram a ele por adaptador de área, nunca por um segundo motor paralelo.
AD-002¶
Área: backend/integração · Estado: active · Desde: 2026-08-01
A representação canônica de qualquer campo sincronizado é string. Tipos ricos (labels múltiplas, dropdown, moeda, data) são codificados/decodificados nas bordas (CustomFieldCodec), preservando o merge de 3 vias, o snapshot e os conflitos por campo.
AD-003¶
Área: full-stack · Estado: active · Desde: 2026-08-01
Taxonomia de domínio espelhada de sistemas externos vive em tabelas seedadas, servida ao cliente por endpoint de metadados. Nunca hardcoded no frontend.
AD-004¶
Área: frontend · Estado: active · Desde: 2026-08-01
Feedback visual usa exclusivamente o KP Notify System (KPToast, KPPush, useKPAlert). Toasts desenhados em handoffs são recriados com KPToast, não com componentes próprios.
AD-005¶
Área: frontend · Estado: superseded by AD-009 · Desde: 2026-08-01
Modais de cadastro/edição seguem o padrão canônico de modal KP (overlay rgba(25,23,21,0.55), raio 16px, header/body/footer fixos, labels 10px uppercase, inputs 10px 14px / raio 10px). Handoffs que divergirem são adaptados a esse padrão.
AD-006¶
Área: frontend · Estado: active · Desde: 2026-08-01
Estado de filtro/busca/drawer de telas de listagem vive na URL (searchParams), não em context global.
AD-007¶
Área: backend/integração · Estado: active · Desde: 2026-08-02
Importação e sincronização são motores distintos e isolados. A importação ClickUp → KP (one-way, pontual, para onboarding) tem namespace, cliente HTTP e tabelas próprios; não usa ClickUpSyncService, ClickUpClient nem as tabelas clickup_*. Compartilha apenas o token lido da configuração. Escopo a AD-001, que segue valendo para o motor de sincronização. Duplicação de código HTTP entre os dois é aceita deliberadamente: o sync será descontinuado e precisa poder ser removido sem afetar a importação.
AD-008¶
Área: backend/domínio · Estado: active · Desde: 2026-08-02
A semântica de uma etapa vem do campo role, nunca do slug ou da posição (sort). Etapas pertencem a um pipeline e declaram seu papel (captado, contato_feito, qualificado, reuniao_agendada, reuniao_realizada, ganho, perdido, nenhum). Regras de marcos do funil, contagem de leads ativos e métricas do Painel consultam o papel. Slugs passam a ser identificadores, não semântica.
AD-009¶
Área: frontend · Estado: active · Desde: 2026-08-09
Telas de cadastro/edição usam cortina lateral (KPDrawer), ancorada à direita, não modal centralizado. Os design tokens de AD-005 permanecem válidos (labels 10px uppercase, inputs 10px 14px / raio 10px, foco #af8b61, overlay rgba(25,23,21,0.55), gradiente primário) — muda o contêiner, não o vocabulário visual. A cortina é empilhável, para permitir criação inline de entidades relacionadas. Telas legadas em modal migram sob demanda, não de uma vez. Supersede AD-005.
AD-010¶
Área: backend/deploy · Estado: active · Desde: 2026-08-09
Permissões novas ou renomeadas são criadas por migration idempotente, nunca só por seeder. deploy/deploy.sh roda apenas php artisan migrate --force, sem seed: uma permissão declarada apenas no ProfileSeeder nunca chega à produção. O seeder continua existindo para ambientes novos, mas a migration é a fonte que alcança a produção já ativa.
AD-011¶
Área: backend/segurança · Estado: active · Desde: 2026-08-20
Arquivo que contenha dado pessoal (mídia de conversa, documento de aluno, foto de contato) vive em disco privado, servido por rota que verifica permissão — nunca em Storage::disk('public'). Nasceu no design do WhatsApp e vale para o projeto: impede que o padrão de avatar público se espalhe para dado de terceiro.
AD-012¶
Área: infra/integração · Estado: active · Desde: 2026-08-31
O servidor Evolution do KP é próprio e dedicado. O compartilhado da Avanzzo deixa de ser destino: ele hospeda instâncias de terceiros, sua chave global comanda todas elas, e a configuração de webhook de uma instância aponta para um sistema só — repontar uma silenciaria a operação deles. Com servidor próprio, o raio de alcance da chave global passa a ser apenas nosso, e é isso que torna a criação de instâncias pelo KP aceitável. Instalado em 31/08/2026 na mesma VPS de produção, por decisão do cliente.
AD-013¶
Área: backend/segurança · Estado: active · Desde: 2026-08-31
A chave global do Evolution mora no .env, nunca no banco nem na UI, e é usada exclusivamente para criar e excluir instâncias. Todo o resto da operação continua com o token por instância, que é o que o EvolutionGateway já faz. Consequência aceita: um servidor Evolution por ambiente, e trocar a chave é deploy, não clique.
AD-014¶
Área: backend/importação · Estado: scoped by AD-019 · Desde: 2026-09-01
Entrega importada da planilha não vira compromisso na agenda. As 1243 entregas do controle de entregas entram apenas como student_deliverables; nenhuma cria sessions, calendar_tasks ou mentoria_sessions. Motivo: a planilha traz no máximo uma data, e uma sessão de agenda exige tutor, hora e duração — os três teriam de ser inventados em 520 entregas programadas, e apareceriam para o cliente como compromisso real. A data programada fica preservada em texto na observação da entrega. O tutor aparece na planilha, mas quase não alcança o recorte importado. Nomeiam a mentora: o título em MENTORIA HÍBRIDA (Keilla, Juliana/Tuany, Virgínia), Mastermind 2026 T2 (Keilla) e MENTORIA INDIVIDUAL ("TUTORIA INDIVIDUAL VENDAS - JULIANA"); e o corpo da célula em MENTORIA INDIVIDUAL ("Tutoria com Ju - Vendas"). Só que 21 das 22 linhas de MENTORIA INDIVIDUAL são Encerrado e ficaram fora do recorte — a única ativa não tem texto. Dentro das 1243 entregas importadas, 4 células mencionam tutoria, todas de um aluno só e nomeando o assunto ("Tutoria Comunicação", "Tutoria Conteúdo"), não a pessoa. Nos demais produtos o título traz o papel ("com a Psicóloga", "com o Advogado"), que não identifica ninguém. Se um dia os encerrados forem importados, essa premissa muda e vale reavaliar. Reabrir só com tutor e horário definidos pelo cliente.
AD-015¶
Área: backend/domínio · Estado: active · Desde: 2026-09-03
Ação disparada por mensagem de WhatsApp é implementada como um efeito que cumpre o contrato ActionEffect e se registra no ActionEffectRegistry — nunca como um ramo novo dentro do motor de execução. O motor reconhece, persiste e retenta; o que cada ação faz mora na sua própria classe. Vale para as ações já previstas e para as futuras (criar lead no CRM, anotar na conversa, vincular contato). Um switch por tipo dentro do motor é o antipadrão que esta decisão existe para impedir: ele transformaria cada ação nova em risco para todas as outras.
AD-016¶
Área: backend/domínio · Estado: active · Desde: 2026-09-05
Autorização sobre uma instância de dado usa tabela de vínculo própria, nunca a tabela permissions. permissions descreve telas do sistema: um conjunto fixo, povoado por migration (AD-010). Quando a autorização recai sobre um registro — um pipeline, e amanhã uma unidade, uma turma ou uma instância de WhatsApp —, ela vira <entidade>_access ligando o perfil ao registro, com as habilidades como colunas. Motivo: representar dado como permissão faz a tabela de permissões crescer junto com o cadastro, e o "marcar todos" da tela de Perfis passa a liberar registros de negócio sem que ninguém tenha pedido. Corolário: a ausência de linha é a negativa — não existe estado "negado" gravado, o que torna "ninguém até autorizar" o padrão sem que a criação do registro escreva coisa alguma, e tira a chance de esquecer de negar. A tela de Perfis ganha uma seção por entidade, ao lado das permissões e não dentro delas.
AD-017¶
Área: backend/domínio · Estado: active; segunda metade (mentor) scoped by AD-019 · Desde: 2026-09-06
O que foi vendido é dado da venda, não do catálogo. O pacote negociado é congelado em contract_items no fechamento, e é dele que derivam os entregáveis do aluno, as sessões previstas e — na feature seguinte — o contrato. O cadastro do produto é o default da tela de venda, nunca a fonte do que o aluno recebe: alterá-lo depois não alcança venda alguma. Corolário: quem responde "o que esta venda tem?" lê contract_items, e só cai na composição do produto para vendas anteriores ao snapshot. O mesmo raciocínio vale para o mentor — ele é do cadastro (tutor padrão da ocorrência, tutores habilitados da sessão), nunca uma escolha feita na venda; ambiguidade deixa a ocorrência sem tutor e pendente de atribuição, em vez de impedir o fechamento.
AD-018¶
Área: backend/domínio · Estado: active · Desde: 2026-09-06
Prazo de contrato tem duas datas: referência e real. contracts.end_date é congelado no fechamento (início + duração) e nunca se move; effective_end_date nasce igual e só muda por prorrogação explícita, com autor e data. O prazo não bloqueia agendamento — decisão do cliente: o aluno pode ter dificuldade de agenda e prorrogar, e travar a agenda transformaria isso em chamado. Quem pergunta "até quando esta venda vale" lê o prazo real; a diferença entre as duas datas é o que torna o atraso de cronograma mensurável.
AD-019¶
Área: backend/domínio · Estado: active · Desde: 2026-09-08
Importação que traz tutor, hora e duração vira compromisso na agenda, e o especialista da planilha é registro, não escolha. Escopa AD-014 e a segunda metade de AD-017, que seguem valendo onde nasceram. AD-014 recusou agenda para a planilha de entregas porque ela trazia no máximo uma data — sem tutor, sem hora, sem duração — e o próprio texto dela condiciona a reabertura a "tutor e horário definidos pelo cliente"; a planilha de vendas datadas traz os três em 134 linhas, então a premissa caiu. AD-017 diz que o mentor vem do cadastro e nunca de uma escolha na venda: isso governa a tela, onde escolher seria arbitrar; numa importação o especialista já atendeu, e a linha é registro do que aconteceu. O cadastro passa a validar — o especialista tem de estar habilitado na especialidade — em vez de decidir, e divergência entre os dois vai para o relatório. A primeira metade de AD-017 é obedecida à risca: a importação passa pelo mesmo SaleCheckoutService e grava o mesmo contract_items, para que venda importada e venda digitada sejam o mesmo registro. Consequência aceita: sem sessão de grupo no esquema, uma aula para quatro alunas vira quatro sessões sobrepostas na agenda do tutor, e a checagem de conflito é dispensada por opção nomeada, testada e restrita a esse caso — a linha que se declara Individual tem veto, para que uma coincidência entre clientes não passe por aula em grupo.
AD-020¶
Área: backend/segurança · Estado: active · Desde: 2026-09-10
Autorização para agir sobre um registro vem do vínculo com aquele registro, não de uma permissão de tela. permissions descreve quais telas o perfil abre; quem pode agir sobre esta conversa, este número, este funil vem da tabela de vínculo (whatsapp_instance_shares, profile_pipeline_access, e as que vierem). É o corolário de AD-016, promovido a regra de autorização depois que a ausência dele produziu dois defeitos em produção em uma semana: no WA-30, a supervisão alcançava número pessoal porque a permissão de tela decidia sozinha; no WA-31, compartilhar um número não compartilhava o atendimento, porque responder exigia whatsapp.conversas.send além do cadastro no número — a pessoa recebia o número, pegava a conversa e não tinha campo para digitar. Corolário prático: onde existe vínculo, ele é a autorização; a permissão de tela continua valendo onde não há vínculo que sirva (número próprio, por exemplo). Cuidado nomeado: relação carregada com colunas escolhidas (with('instance:id,label')) devolve nulo em silêncio para o que ficou de fora — numa pergunta de autorização, isso nega acesso a quem tem. Predicado que decide acesso resolve o dado que falta em vez de responder errado.
AD-021¶
Área: backend/domínio · Estado: active · Desde: 2026-09-12
Tracking é instrumentação, e instrumentação nunca bloqueia a operação que instrumenta. Toda escrita de tracking — clique, toque, evento — acontece como efeito depois do commit da operação de origem, e engole a própria exceção registrando a falha. Se a gravação falhar, a mensagem chegou, a resposta saiu e a venda fechou do mesmo jeito. Corolários que vêm junto: o evento guarda o instante do fato separado do instante da gravação, e a leitura ordena pelo fato, porque webhook retentado chega fora de ordem e ordenar pela gravação embaralharia a jornada; indicador não é gravado como se fosse fato, então evento e toque são imutáveis e todo número é leitura derivada deles, o que torna trocar o modelo de atribuição ou corrigir um cálculo uma mudança de leitura e não uma migração de dado; e agregado só nasce quando existe o problema de desempenho que ele resolve, sempre reconstruível a partir dos eventos. Motivo: a alternativa — gravar tracking no caminho crítico — transforma cada indicador novo em risco para o atendimento, que é o que o produto promete.
AD-022¶
Área: backend/integração · Estado: active · Desde: 2026-09-12
O provedor de IA é configuração, e o dialeto do provedor é o discriminador. O cliente cadastra endereço base, identificador de modelo e chave em tela, no padrão das demais integrações (ai_settings espelhando email_settings: discriminador de tipo, last_verified_at, updated_by, segredo no cast encrypted). Quem chama a IA fala com um contrato pequeno — texto entra, texto sai — e cada dialeto de fornecedor é um adaptador atrás dele; um terceiro dialeto entra sem tocar em nenhum chamador. Motivo: pedido explícito do cliente de não ficar preso a fornecedor nenhum, e o mercado de modelos muda rápido o bastante para que trocar provedor não possa ser deploy. Consequências aceitas: sem provedor configurado o sistema opera sem IA e nenhuma tela quebra; IA fora, lenta além do limite ou sem cota deixa a resposta humana seguir, exibindo o motivo da ausência da sugestão, nunca um erro genérico; e valor sugerido por IA mora em campo próprio, separado do definido por pessoa, que nunca é sobrescrito.