Pular para conteúdo

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.