Eu estava configurando login com Google, GitHub e Apple no meu SaaS, e pra publicar o app no Google você precisa de uma home pública com link pra termos de uso e política de privacidade. Os meus eram cinco seções de template genérico, daqueles que servem pra qualquer produto e por isso não servem pra nenhum. Sentei pra reescrever querendo uma coisa só: passar autoridade pro revisor.

O que aconteceu foi melhor, e bem mais desconfortável. Quando o documento passou a descrever o que o sistema realmente faz, ele começou a acusar o que o sistema não faz. Política honesta é auditoria de código disfarçada de burocracia, e eu não tinha percebido isso em nenhum dos SaaS que já escrevi.

O que o documento acusou

A política dizia que os dados ficam 30 dias depois do cancelamento e depois são excluídos em definitivo. Bonito, padrão de mercado, e eu tinha certeza de que era verdade porque o meu Team usa SoftDeletes. Fui conferir: não existe um único forceDelete no código de produção inteiro. Um Time excluído há dois anos continua no banco, inteirinho. O soft delete que eu achava que era metade da solução era na verdade o problema, porque ele me dava a sensação de ter implementado retenção quando eu só tinha implementado "não apagar".

E aí veio a parte que me fez rir de nervoso. Fui olhar o outro lado, a exclusão de usuário:

// app/Http/Controllers/Settings/ProfileController.php
Auth::logout();
$user->delete();   // User não usa SoftDeletes. Isso é hard delete, agora.

Enter fullscreen mode Exit fullscreen mode

Ou seja, a mesma frase da política estava errada nas duas pontas ao mesmo tempo, e em direções opostas: o Time ficava pra sempre, o usuário sumia na hora, e nenhum dos dois tinha a tal janela de 30 dias que eu prometia por escrito.

Depois foi a vez dos logs. A política falava em guardar registro de acesso por 12 meses, citando o art. 15 do Marco Civil, que é a coisa certa a se dizer. O meu config estava assim:

// config/activitylog.php
'delete_records_older_than_days' => null,   // null = pra sempre

Enter fullscreen mode Exit fullscreen mode

E esse log grava IP e user agent de toda ação. O art. 15 é prazo mínimo de guarda, nunca foi licença pra guardar eternamente, então eu estava violando o meu próprio documento pra pior.

O terceiro achado foi exportação de dados. A política dizia que o dono do time exporta os dados dele pelo painel, e não existe rota pra isso. O que existe é um endpoint na API que exporta um usuário final, que é outra coisa completamente: é ferramenta pro meu cliente atender os titulares dele, não pra mim atender ele. Eu tinha confundido as duas.

A cláusula falsa que ninguém mentiu pra escrever

Agora a parte que interessa mais que os achados, no mesmo espírito do teste verde que me enganou por meses no post anterior.

Aquela frase sobre exportação de dados no painel foi escrita na mesma sessão, poucas horas antes de eu descobrir que era falsa. Ninguém mentiu. A IA leu o código, viu um DataExportsController, viu que ele exporta dados, e deduziu que existia exportação. É uma dedução razoável, escrita com confiança, e completamente errada. Se eu não tivesse pedido pra conferir cláusula por cláusula contra o código, ela ia pra produção com cara de verdade.

É exatamente o mesmo inimigo do post passado, com outra roupa. Lá, a IA copiava o padrão errado porque ele era o mais visível no código. Aqui, ela escreveu o que era plausível porque plausível é o que ela otimiza. Imitação convincente é o modo de falha, não a mentira. E texto jurídico é o pior lugar possível pra isso acontecer, porque ninguém revisa política de privacidade com o mesmo rigor que revisa um pull request. O documento passa batido justamente por ser chato.

Identidade em config, não no texto

Duas coisas viraram código e valem pra qualquer projeto.

A primeira é boba e resolve uma dor real: tirar a identidade da empresa e os prazos de dentro do texto. Virou um config/legal.php com razão social, CNPJ, endereço, foro, emails e os prazos de retenção e de resposta. O texto legal só interpola :taxId, :retentionDays, :privacyEmail. Mudar de endereço agora é uma linha de env em vez de uma caçada em dois idiomas, e o prazo publicado passa a ter um lugar único de verdade.

Teve um detalhe pequeno aí que eu gostei mais do que devia. O default vazio renderiza "inscrita no CNPJ sob o nº ." e ninguém enxerga isso numa revisão, porque o olho pula frase quebrada em texto jurídico. Então o default virou [CNPJ] literal, que é impossível de publicar sem querer.

O gate, porque regra sem gate é sugestão

A segunda é a que fecha o ciclo. Se o texto interpola :taxId e a chave não existir na config, o Laravel não dá erro: ele renderiza :taxId literal, cru, no meio da política. O leitor vê, eu não. É falha silenciosa, que é o tipo que eu mais odeio.

Então o texto legal ganhou um teste que varre todas as strings, extrai cada token e falha se ele não tiver par:

// tests/Feature/LegalPagesTest.php
// Token sem par não dá erro, renderiza literal. Este teste transforma isso em falha.
foreach ($matches[1] as $token) {
    $this->assertContains(
        $token,
        $known,
        "landing.{$key} interpola :{$token}, que não é compartilhado em config/legal.php",
    );
}

Enter fullscreen mode Exit fullscreen mode

Junto com ele, um teste que percorre as 29 seções dos dois documentos nos dois idiomas e falha se qualquer uma resolver pra vazio. É a mesma tese do post anterior aplicada num lugar que eu nunca tinha pensado em testar: se importa, vira teste que quebra o build. Política de privacidade importa, então vira teste como qualquer outra regra que eu levo a sério.

O que fez o documento parar de parecer genérico

Uma coisa que eu não esperava: o que dá autoridade não é o documento ser bem escrito, é ele bater com o que o app faz.

O meu produto tem dois papéis ao mesmo tempo. Eu sou controlador dos dados da conta do meu cliente (cadastro, login, cobrança), e operador dos dados dos usuários finais dele, que chegam pelo widget que ele instala no produto dele. São regimes jurídicos diferentes, com responsabilidades diferentes, no mesmo banco de dados. Nenhum template tem essa distinção, porque template não sabe o que o seu produto faz.

Colocar isso em seção própria foi o que mudou o texto de "genérico bonito" pra "esse cara sabe do que está falando". E é justamente a primeira coisa que o jurídico de um cliente B2B maior vai procurar.

As 3 regras que eu tiro disso

  1. Escreva a política contra o código, não contra um template. Cada cláusula é uma afirmação verificável sobre o seu sistema. Se você não consegue apontar o arquivo que cumpre a frase, a frase é ficção.
  2. Prazo publicado é prazo exigível. No minuto que você escreve "30 dias" ou "12 meses", isso deixou de ser aspiração e virou dívida. Ou o agendador cumpre, ou o número sai do texto.
  3. Texto jurídico também merece gate. É o lugar onde falha silenciosa mais sobrevive, porque ninguém revisa com carinho e o failure mode não é erro, é o usuário lendo :taxId no meio da sua política.

Pra fechar, com honestidade

Eu não publiquei nada disso ainda. As três falhas viraram issue no meu repo, e a da retenção é bloqueante: enquanto Time excluído ficar pra sempre no banco, o documento promete o que o código não faz, e publicar assim é trocar um problema de template por um problema de LGPD. A poda de log é a mais barata das três, config mais uma linha no agendador, e ainda assim estava lá.

As frases falsas eu corrigi na hora pra descreverem o processo real de hoje, que no caso da exportação é um pedido manual por email. É menos bonito que "exporte pelo painel" e tem a vantagem de ser verdade.

Se você tem um SaaS rodando agora, faz um teste: abre sua política de privacidade e escolhe uma frase, qualquer uma, que prometa prazo. Vai no código procurar quem cumpre. Me conta o que você achou.