Compliance Check Operacional do Produto — Cataloga-ai
Versão: 1.0
Data: 2026-04-25
Responsável: Compliance QA (Bias-Check / Calibration)
Escopo: Auditoria LGPD de features do produto Cataloga-ai
Sumário Executivo
Este documento apresenta auditoria operacional de compliance LGPD das principais features do Cataloga-ai, um SaaS B2B brasileiro que atua como controlador dos dados de lojistas e operador dos dados de consumidores finais.
Stack auditado: Supabase (self-hosted), Vercel, Asaas, GPTMaker, WhatsApp Cloud API, Redis
Resultado geral:
- ✅ Conformes: 0/10
- ⚠️ Atenção necessária: 10/10
- ❌ Não conformes: 0/10
Nota: Todos os itens marcados como ⚠️ porque, sem acesso ao código-fonte e documentação técnica do produto, esta auditoria baseia-se em análise de risco presumida para a stack declarada. Recomenda-se verificação técnica in-loco para confirmação.
1. Formulários de Coleta de Dados
Status: ⚠️ Atenção
Gap Identificado
Formulários de signup/onboarding de lojistas e de checkout de consumidores finais tipicamente coletam mais campos do que o necessário para a finalidade declarada. Sem minimização de dados (Art. 6º, III LGPD), há risco de coleta excessiva.
Campos de risco comum:
- CPF quando CNPJ bastaria (para lojista PJ)
- Telefone celular sem opt-in explícito para marketing
- Campos de "informações adicionais" sem finalidade específica
- Data de nascimento completa quando apenas maioridade é necessária
Recomendação Técnica
1. Implementar validação de campos obrigatórios apenas para finalidade essencial:
// cataloga-ai/lib/validators/signup-schema.ts
import { z } from 'zod';
export const LojistSignupSchema = z.object({
// Essencial para cadastro
email: z.string().email(),
cnpj: z.string().regex(/^\d{14}$/), // Apenas CNPJ para PJ
razaoSocial: z.string().min(3),
// Opcional com finalidade específica
telefone: z.string().optional(), // Só se opt-in para suporte
cpfResponsavel: z.string().regex(/^\d{11}$/).optional(), // Só se necessário para KYC Asaas
// Campos removidos (excessivos):
// dataNascimento, enderecoCompleto, redesSociais
}).strict(); // .strict() previne campos extras não declarados
2. Consentimento granular para campos opcionais:
// cataloga-ai/components/SignupForm.tsx
<Checkbox
name="consent_phone_support"
label="Aceito compartilhar meu telefone para suporte técnico (opcional)"
onChange={(checked) => {
if (!checked) {
form.setValue('telefone', null); // Limpa campo se opt-out
}
}}
/>
3. Auditoria de banco de dados:
-- Script de auditoria: cataloga-ai/scripts/audit-data-minimization.sql
-- Identificar colunas potencialmente excessivas
SELECT
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name IN ('lojistas', 'consumidores_finais')
AND column_name IN (
'data_nascimento', 'renda', 'profissao', 'estado_civil',
'numero_filhos', 'redes_sociais'
);
-- Se alguma dessas colunas existir sem finalidade documentada, marcar para remoção
Prioridade de Remediação
ALTO — Coleta excessiva é base para multa por violação do princípio da necessidade.
2. Checkout do Consumidor Final
Status: ⚠️ Atenção
Gap Identificado
No fluxo de checkout (onde o consumidor final compra do lojista via Cataloga-ai), há três riscos LGPD:
- Ausência de aviso de coleta visível antes da finalização
- Base legal não declarada — consumidor não sabe se está consentindo ou se é execução de contrato
- Compartilhamento com Asaas (gateway) sem transparência
Recomendação Técnica
1. Banner de transparência no checkout:
// cataloga-ai/components/checkout/CheckoutPrivacyNotice.tsx
export function CheckoutPrivacyNotice({ lojistName }: { lojistName: string }) {
return (
<div className="border-l-4 border-blue-500 bg-blue-50 p-4 text-sm mb-4">
<p className="font-medium">Sobre seus dados nesta compra:</p>
<ul className="mt-2 space-y-1 text-gray-700">
<li>• <strong>{lojistName}</strong> é o responsável pelos seus dados de compra</li>
<li>• Cataloga-ai processa o pedido como <strong>operador autorizado</strong></li>
<li>• Seus dados de pagamento são enviados para <strong>Asaas</strong> (processador)</li>
<li>• <a href="/privacy" className="underline">Política de Privacidade completa</a></li>
</ul>
</div>
);
}
2. Declarar base legal no modelo de dados:
// cataloga-ai/lib/database/schema.ts
export interface PedidoConsumidorFinal {
id: string;
lojista_id: string;
consumidor_email: string;
consumidor_nome: string;
consumidor_cpf?: string; // Nullable: só obrigatório se exigido por Asaas
// Metadata LGPD
base_legal: 'execucao_contrato' | 'consentimento' | 'legitimo_interesse';
finalidade: string; // Ex: "Processamento de pedido de compra"
consentimento_timestamp?: Date;
consentimento_ip?: string;
consentimento_versao_termos?: string; // Ex: "v2.1-2026-04"
}
3. Opt-in explícito para marketing (separado do checkout):
// cataloga-ai/components/checkout/CheckoutForm.tsx
<Checkbox
name="marketing_opt_in"
label={`Quero receber ofertas de ${lojistName} por e-mail (opcional)`}
defaultChecked={false} // Nunca pré-marcado
className="mt-4"
/>
{/* Garantir que compra proceda mesmo se opt-out */}
Prioridade de Remediação
CRÍTICO — Checkout sem aviso de coleta é violação direta do Art. 9º LGPD (direito à informação).
3. Order Bump / Perfilamento
Status: ⚠️ Atenção
Gap Identificado
Order Bump (ofertas personalizadas durante checkout) envolve perfilamento — tratamento de dados pessoais para análise de comportamento de compra.
Riscos LGPD:
- Art. 20 exige que titular possa solicitar revisão de decisões automatizadas
- Perfilamento sem consentimento específico pode ser considerado violação de finalidade
- Se perfilamento inferir dados sensíveis (ex: condição de saúde via histórico de compras de suplementos), exige consentimento específico
Recomendação Técnica
1. Minimizar inferências — usar apenas dados transacionais agregados:
// cataloga-ai/lib/order-bump/profiling-engine.ts
/**
* Motor de recomendação LGPD-safe:
* - Baseado em categoria de produto (não em análise de comportamento individual)
* - Sem inferência de dados sensíveis
* - Explainability built-in para direito de revisão (Art. 20)
*/
export function getOrderBumpSuggestions(cart: CartItem[]): OrderBumpOffer[] {
const categories = cart.map(item => item.category);
// Lógica baseada em regras (não ML black-box)
if (categories.includes('eletronicos')) {
return [{
product: 'Cabo USB-C',
reason: 'Complementar a eletrônicos' // Explicação clara
}];
}
// NÃO fazer: inferência de saúde, condição financeira, etc
return [];
}
2. Consentimento específico para perfilamento avançado:
// cataloga-ai/components/lojista/SettingsPerfilamento.tsx
<section>
<h3>Ofertas Personalizadas (Order Bump)</h3>
<RadioGroup name="perfilamento_mode">
<Radio value="basic" label="Básico (categoria de produto apenas)" defaultChecked />
<Radio value="advanced" label="Avançado (histórico de compras)" />
</RadioGroup>
{mode === 'advanced' && (
<Alert variant="warning">
⚠️ Modo avançado requer opt-in explícito dos consumidores finais.
<Link href="/docs/lgpd-perfilamento">Entenda as implicações LGPD</Link>
</Alert>
)}
</section>
3. Implementar direito de revisão (Art. 20 LGPD):
// cataloga-ai/api/consumidor/revisao-perfilamento.ts
export async function solicitarRevisaoPerfilamento(
consumidorId: string,
pedidoId: string
) {
// Retornar explicação de como Order Bump foi gerado
const oferta = await getOrderBumpHistory(consumidorId, pedidoId);
return {
oferta_sugerida: oferta.product,
criterios_usados: oferta.reason, // Ex: "Baseado em categoria Eletrônicos no carrinho"
dados_pessoais_usados: ['categoria_produto_no_carrinho'], // Não usar histórico sem consentimento
pode_contestar: true,
formulario_contestacao: '/consumidor/contestar-perfilamento'
};
}
Prioridade de Remediação
CRÍTICO — Perfilamento sem consentimento + impossibilidade de revisão = risco duplo Art. 7º e Art. 20.
4. WhatsApp Opt-in
Status: ⚠️ Atenção
Gap Identificado
Integração com WhatsApp Cloud API para envio de mensagens transacionais e marketing.
Riscos:
- Opt-in não granular — mesmo checkbox para mensagens transacionais E marketing
- Ausência de double opt-in — telefone não verificado antes de adicionar à lista
- Opt-out não implementado via WhatsApp — titular precisa ir ao site para revogar
Recomendação Técnica
1. Separar opt-in transacional de marketing:
// cataloga-ai/components/checkout/WhatsAppOptIn.tsx
<div className="space-y-2">
{/* Transacional: implícito na execução do contrato */}
<div className="text-sm text-gray-600">
✓ Você receberá atualizações do pedido via WhatsApp (base: execução de contrato)
</div>
{/* Marketing: consentimento explícito necessário */}
<Checkbox
name="whatsapp_marketing_opt_in"
label="Quero receber ofertas e novidades via WhatsApp (opcional)"
defaultChecked={false}
/>
</div>
2. Implementar double opt-in com verificação:
// cataloga-ai/lib/whatsapp/opt-in-flow.ts
export async function initWhatsAppOptIn(phone: string, consentType: 'transacional' | 'marketing') {
// 1. Gerar token único
const token = crypto.randomBytes(16).toString('hex');
// 2. Enviar mensagem de confirmação
await whatsappAPI.sendMessage({
to: phone,
template: 'opt_in_confirmation',
params: {
tipo: consentType === 'marketing' ? 'ofertas e novidades' : 'atualizações de pedido',
link_confirmacao: `https://cataloga.ai/confirm-whatsapp/${token}`
}
});
// 3. Só após clique no link, marcar como opted-in
await database.whatsapp_consent.insert({
phone,
consent_type: consentType,
status: 'pending_confirmation',
token,
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24h
});
}
3. Opt-out via comando WhatsApp:
// cataloga-ai/lib/whatsapp/webhook-handler.ts
export async function handleIncomingMessage(message: WhatsAppMessage) {
const body = message.body.toLowerCase().trim();
// Detectar opt-out
if (['sair', 'parar', 'cancelar', 'opt-out', 'stop'].includes(body)) {
await database.whatsapp_consent.update({
where: { phone: message.from },
data: {
status: 'revoked',
revoked_at: new Date(),
revoked_via: 'whatsapp_command'
}
});
await whatsappAPI.sendMessage({
to: message.from,
text: '✅ Você não receberá mais mensagens de marketing. Mensagens transacionais de pedidos continuarão (base legal: execução de contrato). Para revogar totalmente, acesse cataloga.ai/privacidade'
});
}
}
4. Log de consentimento auditável:
-- cataloga-ai/database/migrations/XXX_whatsapp_consent_log.sql
CREATE TABLE whatsapp_consent_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
phone VARCHAR(20) NOT NULL,
consent_type VARCHAR(20) NOT NULL, -- 'transacional' | 'marketing'
action VARCHAR(20) NOT NULL, -- 'granted' | 'revoked'
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ip_address INET,
user_agent TEXT,
evidence_url TEXT, -- Link para screenshot/PDF do opt-in
version_terms VARCHAR(50) -- Ex: 'termos-v3-2026-04'
);
CREATE INDEX idx_whatsapp_consent_phone ON whatsapp_consent_log(phone, timestamp DESC);
Prioridade de Remediação
CRÍTICO — Mensagens WhatsApp sem opt-in verificável = violação Art. 7º + risco de multa pesada (comunicação direta).
5. GPTMaker Integration (PII para IA Externa)
Status: ⚠️ Atenção
Gap Identificado
Integração com GPTMaker (IA externa) para geração de descrições de produtos, chatbot, etc.
Riscos LGPD:
- Transferência internacional de PII — se GPTMaker usa OpenAI/Anthropic, dados vão para EUA (Art. 33)
- Ausência de anonimização — envio de contexto com PII do consumidor/lojista
- Sub-operador sem DPA — GPTMaker pode não ter Data Processing Agreement assinado
- Retenção de dados pela IA — modelos podem armazenar inputs para treinamento
Recomendação Técnica
1. Anonimizar dados antes de enviar para GPTMaker:
// cataloga-ai/lib/ai/gptmaker-client.ts
import { hash } from 'crypto';
/**
* Sanitiza contexto removendo PII antes de enviar para IA externa
*/
function sanitizeForAI(input: {
lojistaNome?: string;
consumidorEmail?: string;
produtoDescricao: string;
}): string {
// Substituir PII por placeholders
return input.produtoDescricao
.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]')
.replace(/\b\d{3}\.\d{3}\.\d{3}-\d{2}\b/g, '[CPF]')
.replace(/\b\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}\b/g, '[CNPJ]')
.replace(/\(?\d{2}\)?\s?\d{4,5}-?\d{4}/g, '[TELEFONE]');
// NÃO enviar: lojistaNome, consumidorEmail, pedidoId real
}
export async function generateProductDescription(produto: Product) {
const sanitizedInput = sanitizeForAI({
produtoDescricao: produto.nome // Apenas nome genérico, sem PII
});
const response = await gptMakerAPI.generate({
prompt: `Crie descrição de produto: ${sanitizedInput}`,
// Garantir opt-out de treinamento (se GPTMaker suportar)
metadata: { data_retention: 'none' }
});
return response.text;
}
2. Exigir DPA com GPTMaker:
<!-- cataloga-ai/compliance/07-sub-operadores-checklist.md -->
## GPTMaker — Status de Compliance
- [❌] DPA assinado com cláusulas LGPD
- [❌] Confirmação de não-uso de dados para treinamento
- [❌] Localização de servidores (Brasil/UE ou cláusulas de transferência internacional)
- [❌] Direito de auditoria por Cataloga-ai
**Ação:** Solicitar DPA imediatamente. Enquanto não assinado, não enviar PII.
3. Implementar fallback sem PII se DPA ausente:
// cataloga-ai/config/features.ts
export const AI_FEATURES = {
gptmaker_enabled: process.env.GPTMAKER_DPA_SIGNED === 'true', // Feature flag
allow_pii_in_context: false, // Sempre false até DPA + anonimização verificada
};
// cataloga-ai/lib/ai/index.ts
export async function generateContent(input: string) {
if (!AI_FEATURES.gptmaker_enabled) {
// Fallback: template estático sem IA
return getStaticTemplate(input);
}
if (containsPII(input) && !AI_FEATURES.allow_pii_in_context) {
throw new Error('PII detectada em input de IA. Sanitize obrigatório.');
}
return gptMakerClient.generate(input);
}
4. Avisar titular sobre uso de IA:
// cataloga-ai/components/ProductForm.tsx (para lojista)
<Alert variant="info">
💡 Descrições geradas por IA são processadas por terceiro (GPTMaker).
Evite incluir dados pessoais de clientes.
</Alert>
Prioridade de Remediação
CRÍTICO — Transferência de PII para IA externa sem DPA + sem anonimização = violação Art. 33 + possível incidente reportável à ANPD.
6. Logs e Debugging (PII em Logs)
Status: ⚠️ Atenção
Gap Identificado
Logs de aplicação (Vercel logs, Supabase logs, Redis logs) frequentemente expõem PII:
- E-mails em query strings (
/api/user?email=joao@example.com) - Tokens de sessão em headers
- Request bodies com CPF/CNPJ
- Stack traces com dados de produção
Risco: Logs são acessíveis por times de engenharia e podem vazar em incidentes de segurança.
Recomendação Técnica
1. Sanitizar logs automaticamente:
// cataloga-ai/lib/logger/sanitizer.ts
import pino from 'pino';
const PII_PATTERNS = {
email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
cpf: /\b\d{3}\.\d{3}\.\d{3}-\d{2}\b/g,
cnpj: /\b\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}\b/g,
phone: /\(?\d{2}\)?\s?\d{4,5}-?\d{4}/g,
creditCard: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
};
function sanitize(obj: any): any {
if (typeof obj === 'string') {
let sanitized = obj;
Object.entries(PII_PATTERNS).forEach(([key, pattern]) => {
sanitized = sanitized.replace(pattern, `[${key.toUpperCase()}_REDACTED]`);
});
return sanitized;
}
if (Array.isArray(obj)) {
return obj.map(sanitize);
}
if (typeof obj === 'object' && obj !== null) {
const sanitized: any = {};
Object.keys(obj).forEach(key => {
// Redact campos sensíveis completamente
if (['password', 'token', 'authorization', 'cookie'].includes(key.toLowerCase())) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = sanitize(obj[key]);
}
});
return sanitized;
}
return obj;
}
export const logger = pino({
serializers: {
req: (req) => sanitize({
method: req.method,
url: req.url,
headers: req.headers,
}),
res: (res) => sanitize({
statusCode: res.statusCode,
}),
err: (err) => sanitize({
type: err.type,
message: err.message,
stack: err.stack,
}),
},
});
2. Política de retenção de logs:
// cataloga-ai/lib/logging/retention-policy.ts
export const LOG_RETENTION = {
// Logs de aplicação com PII sanitizada
application: '90 days',
// Logs de auditoria (quem acessou o quê)
audit: '5 years', // LGPD recomenda manter logs de auditoria mais tempo
// Logs de debugging (nunca em produção)
debug: '7 days', // Só em staging/dev
};
// Configurar no Vercel:
// Settings > Logs > Retention = 90 days
// Configurar no Supabase:
// Dashboard > Logs > Configure retention = 90 days
3. Ambiente-specific logging:
// cataloga-ai/config/logging.ts
export const loggingConfig = {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
// NUNCA logar request/response bodies em produção
logRequestBody: process.env.NODE_ENV !== 'production',
logResponseBody: process.env.NODE_ENV !== 'production',
// Sanitizar sempre, mesmo em dev (treinar boas práticas)
sanitizePII: true,
};
4. Acesso auditado aos logs:
-- cataloga-ai/compliance/audit-log-access.sql
-- Registrar quem acessou logs de produção
CREATE TABLE log_access_audit (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
log_type VARCHAR(50), -- 'vercel' | 'supabase' | 'redis'
accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ip_address INET,
justification TEXT -- Obrigar equipe a justificar acesso a logs
);
Prioridade de Remediação
ALTO — Logs com PII são vetores de vazamento + dificultam compliance com direito ao esquecimento.
7. Acesso de CS / Admin ao Painel (RLS)
Status: ⚠️ Atenção
Gap Identificado
Painéis administrativos (CS, financeiro, tech) frequentemente expõem PII além do necessário para a função:
- CS vê CPF quando só precisaria de nome/e-mail
- Financeiro vê histórico completo de compras quando só precisa de totais
- Ausência de Row-Level Security (RLS) no Supabase permite acesso indiscriminado
Risco: Violação do princípio de least privilege (Art. 46, §1º LGPD).
Recomendação Técnica
1. Implementar RLS no Supabase:
-- cataloga-ai/database/migrations/XXX_enable_rls.sql
-- Habilitar RLS em todas tabelas com PII
ALTER TABLE lojistas ENABLE ROW LEVEL SECURITY;
ALTER TABLE consumidores_finais ENABLE ROW LEVEL SECURITY;
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
-- Política: CS só vê dados do próprio lojista que está atendendo
CREATE POLICY cs_lojista_access ON lojistas
FOR SELECT
TO authenticated
USING (
-- CS precisa ter session var com lojista_id do ticket atual
auth.jwt() ->> 'current_support_ticket_lojista_id' = id::text
OR
-- Ou ser admin com MFA ativo
(auth.jwt() ->> 'role' = 'admin' AND auth.jwt() ->> 'mfa_verified' = 'true')
);
-- Política: Financeiro vê apenas dados agregados
CREATE POLICY finance_aggregated_only ON pedidos
FOR SELECT
TO authenticated
USING (
auth.jwt() ->> 'role' = 'finance'
AND
-- View materializada com apenas totais (sem PII individual)
FALSE -- Forçar uso de view `pedidos_agregados` ao invés de tabela direta
);
2. Views com PII mascarada para CS:
-- cataloga-ai/database/views/cs_lojista_view.sql
CREATE VIEW cs_lojista_safe AS
SELECT
id,
razao_social,
LEFT(email, 3) || '***@' || SPLIT_PART(email, '@', 2) AS email_masked,
LEFT(telefone, 4) || '****' || RIGHT(telefone, 4) AS telefone_masked,
cnpj, -- OK mostrar (dado público)
created_at,
status_assinatura
FROM lojistas;
-- CS usa essa view, não a tabela direta
GRANT SELECT ON cs_lojista_safe TO role_cs;
REVOKE SELECT ON lojistas FROM role_cs; -- Remover acesso direto
3. Auditoria de acesso a PII:
// cataloga-ai/lib/admin/audit-middleware.ts
export async function auditPIIAccess(req: Request, tableName: string, recordId: string) {
if (['lojistas', 'consumidores_finais', 'pedidos'].includes(tableName)) {
await database.pii_access_log.insert({
user_id: req.user.id,
user_role: req.user.role,
table_name: tableName,
record_id: recordId,
action: 'SELECT',
timestamp: new Date(),
ip_address: req.ip,
justification: req.headers['x-access-justification'], // Header obrigatório
});
}
}
4. Painel admin com campo obrigatório de justificativa:
// cataloga-ai/components/admin/PIIAccessModal.tsx
<Modal title="Acesso a Dados Pessoais">
<p>Você está prestes a acessar PII de: {lojistaNome}</p>
<Textarea
name="justification"
label="Justificativa (obrigatório):"
placeholder="Ex: Atender ticket de suporte #1234"
required
/>
<Button onClick={async () => {
await auditPIIAccess(justification);
// Só então libera acesso
}}>
Acessar com Justificativa
</Button>
</Modal>
Prioridade de Remediação
ALTO — Acesso interno excessivo a PII é risco de vazamento + violação de minimização.
8. Cookies e Rastreamento
Status: ⚠️ Atenção
Gap Identificado
Sites brasileiros precisam de banner de cookies (LGPD + ANPD Guia de Cookies). Cataloga-ai provavelmente usa:
- Cookies de sessão (OK sem consentimento — necessário)
- Cookies de analytics (Google Analytics, Hotjar, etc.) — requer consentimento
- Cookies de marketing/remarketing (Meta Pixel, Google Ads) — requer consentimento granular
Risco: Cookies de rastreamento carregados antes do consentimento = violação.
Recomendação Técnica
1. Implementar Consent Management Platform (CMP):
// cataloga-ai/components/CookieConsent.tsx
import CookieConsent from 'react-cookie-consent';
export function CookieBanner() {
return (
<CookieConsent
location="bottom"
buttonText="Aceitar todos"
declineButtonText="Apenas essenciais"
enableDeclineButton
cookieName="cataloga-ai-cookie-consent"
onAccept={() => {
// Carregar scripts de analytics/marketing SOMENTE após aceite
loadGoogleAnalytics();
loadMetaPixel();
}}
onDecline={() => {
// Garantir que scripts não foram carregados
blockThirdPartyScripts();
}}
>
Usamos cookies para melhorar sua experiência.
<a href="/cookies-policy" className="underline">Política de Cookies</a>
</CookieConsent>
);
}
2. Carregar scripts de terceiros APENAS após consentimento:
// cataloga-ai/lib/analytics/conditional-load.ts
export function loadGoogleAnalytics() {
const consent = getCookieConsent();
if (consent.analytics === true) {
// Carregar GA4
const script = document.createElement('script');
script.src = `https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX`;
script.async = true;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag(...args: any[]) { window.dataLayer.push(args); }
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
}
}
// ❌ NUNCA fazer isso (carrega antes do consentimento):
// <script src="https://www.googletagmanager.com/gtag/js?id=G-XXX"></script>
3. Categorizar cookies (essencial, analytics, marketing):
// cataloga-ai/lib/cookies/categories.ts
export const COOKIE_CATEGORIES = {
essential: {
name: 'Essenciais',
description: 'Necessários para funcionamento do site (login, carrinho)',
cookies: ['session_id', 'csrf_token'],
alwaysAllowed: true, // Não precisa consentimento
},
analytics: {
name: 'Analíticos',
description: 'Nos ajudam a entender como você usa o site',
cookies: ['_ga', '_gid', 'hj*'],
requiresConsent: true,
},
marketing: {
name: 'Marketing',
description: 'Usados para mostrar anúncios relevantes',
cookies: ['_fbp', '_gcl_au'],
requiresConsent: true,
},
};
4. Página de gerenciamento de consentimento:
// cataloga-ai/app/cookies-settings/page.tsx
export default function CookieSettings() {
const [consent, setConsent] = useState(getCookieConsent());
return (
<div>
<h1>Gerenciar Cookies</h1>
{Object.entries(COOKIE_CATEGORIES).map(([key, category]) => (
<div key={key}>
<h3>{category.name}</h3>
<p>{category.description}</p>
<Switch
checked={consent[key]}
disabled={category.alwaysAllowed}
onChange={(checked) => updateConsent(key, checked)}
/>
</div>
))}
<Button onClick={saveConsent}>Salvar Preferências</Button>
</div>
);
}
Prioridade de Remediação
ALTO — ANPD já notificou empresas por ausência de banner de cookies. Fácil de implementar, alto risco se ausente.
9. Portabilidade de Dados
Status: ⚠️ Atenção
Gap Identificado
Art. 18, V da LGPD garante direito à portabilidade — titular pode solicitar todos seus dados em formato estruturado e interoperável.
Ausências típicas:
- Nenhuma feature de "Exportar meus dados" no produto
- Processo manual via e-mail DPO (lento, não escalável)
- Dados exportados em PDF (não interoperável — precisa ser CSV/JSON)
Recomendação Técnica
1. Endpoint de portabilidade self-service:
// cataloga-ai/api/lojista/export-data.ts
import { generateZip } from 'lib/export';
export async function POST(req: Request) {
const lojistaId = req.user.id;
// Coletar TODOS os dados do lojista
const [profile, pedidos, consumidores, configuracoes] = await Promise.all([
database.lojistas.findUnique({ where: { id: lojistaId } }),
database.pedidos.findMany({ where: { lojista_id: lojistaId } }),
database.consumidores_finais.findMany({ where: { lojista_id: lojistaId } }),
database.lojista_configuracoes.findMany({ where: { lojista_id: lojistaId } }),
]);
// Exportar em formato estruturado (JSON + CSV)
const exportData = {
format_version: '1.0',
exported_at: new Date().toISOString(),
lojista: profile,
pedidos: pedidos, // CSV também
consumidores_finais: consumidores, // CSV também
configuracoes,
};
// Gerar ZIP com JSONs + CSVs
const zipBuffer = await generateZip({
'lojista.json': JSON.stringify(profile, null, 2),
'pedidos.json': JSON.stringify(pedidos, null, 2),
'pedidos.csv': jsonToCSV(pedidos),
'consumidores.json': JSON.stringify(consumidores, null, 2),
'consumidores.csv': jsonToCSV(consumidores),
'README.txt': 'Exportação de dados Cataloga-ai conforme LGPD Art. 18, V',
});
return new Response(zipBuffer, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="cataloga-ai-dados-${lojistaId}-${Date.now()}.zip"`,
},
});
}
2. UI de portabilidade no painel:
// cataloga-ai/components/settings/DataPortability.tsx
export function DataPortabilitySection() {
const [loading, setLoading] = useState(false);
async function handleExport() {
setLoading(true);
const response = await fetch('/api/lojista/export-data', { method: 'POST' });
const blob = await response.blob();
// Forçar download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `meus-dados-cataloga-ai-${Date.now()}.zip`;
a.click();
setLoading(false);
}
return (
<section className="border p-4 rounded">
<h3>Portabilidade de Dados (LGPD Art. 18, V)</h3>
<p>Exporte todos os seus dados em formato estruturado (JSON + CSV).</p>
<Button onClick={handleExport} loading={loading}>
Exportar Meus Dados
</Button>
</section>
);
}
3. Portabilidade para consumidor final (via lojista):
// cataloga-ai/api/consumidor/export-data.ts
// Endpoint que lojista pode chamar a pedido do consumidor
export async function POST(req: Request) {
const { consumidor_email } = await req.json();
const lojistaId = req.user.id;
// Validar que consumidor pertence a esse lojista
const consumidor = await database.consumidores_finais.findFirst({
where: {
email: consumidor_email,
lojista_id: lojistaId, // Só pode exportar próprios consumidores
},
});
if (!consumidor) {
return new Response('Consumidor não encontrado', { status: 404 });
}
// Exportar dados do consumidor
const pedidos = await database.pedidos.findMany({
where: { consumidor_id: consumidor.id }
});
const exportData = {
consumidor: {
email: consumidor.email,
nome: consumidor.nome,
created_at: consumidor.created_at,
},
pedidos,
};
return Response.json(exportData);
}
Prioridade de Remediação
MÉDIO — Obrigatório por lei, mas pedidos são raros. Implementar em 60 dias.
10. Deleção de Conta (Direito ao Esquecimento)
Status: ⚠️ Atenção
Gap Identificado
Art. 18, VI da LGPD garante direito à eliminação de dados. Problemas comuns:
- Botão "Cancelar conta" apenas desativa, não deleta
- Dados ficam em backups indefinidamente
- Dependências (pedidos, notas fiscais) impedem deleção total
Risco: Titular solicita deleção, empresa não cumpre em tempo razoável (15 dias) → violação.
Recomendação Técnica
1. Deleção lógica vs física + retenção legal:
// cataloga-ai/lib/account/deletion-engine.ts
export async function deleteLojistaAccount(lojistaId: string) {
// 1. Deleção lógica imediata (anonimiza PII)
await database.lojistas.update({
where: { id: lojistaId },
data: {
email: `deleted-${lojistaId}@cataloga-ai.tombstone`,
razao_social: '[CONTA DELETADA]',
cpf: null,
cnpj: null,
telefone: null,
deleted_at: new Date(),
deletion_reason: 'user_request',
},
});
// 2. Anonimizar pedidos (manter por obrigação fiscal: 5 anos)
await database.pedidos.updateMany({
where: { lojista_id: lojistaId },
data: {
// Manter total, data (para fisco)
// Remover PII do consumidor final
consumidor_email: '[ANONIMIZADO]',
consumidor_nome: '[ANONIMIZADO]',
consumidor_cpf: null,
},
});
// 3. Agendar deleção física após período de retenção legal
await database.scheduled_deletions.insert({
entity_type: 'lojista',
entity_id: lojistaId,
scheduled_for: new Date(Date.now() + 5 * 365 * 24 * 60 * 60 * 1000), // 5 anos
});
}
2. Workflow de deleção com período de graça:
// cataloga-ai/api/lojista/request-deletion.ts
export async function POST(req: Request) {
const lojistaId = req.user.id;
// 1. Marcar para deleção (30 dias de período de graça para arrependimento)
await database.lojistas.update({
where: { id: lojistaId },
data: {
deletion_requested_at: new Date(),
deletion_scheduled_for: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
},
});
// 2. Enviar e-mail de confirmação com link de cancelamento
await sendEmail({
to: req.user.email,
subject: 'Solicitação de exclusão de conta recebida',
body: `
Sua conta será excluída em 30 dias.
Caso tenha sido um engano, cancele a exclusão em:
https://cataloga.ai/cancel-deletion?token=${generateToken(lojistaId)}
`,
});
return Response.json({
message: 'Exclusão agendada para 30 dias. Você receberá confirmação por e-mail.'
});
}
3. Job de deleção automática:
// cataloga-ai/jobs/process-scheduled-deletions.ts
// Rodar diariamente via cron
export async function processScheduledDeletions() {
const pendingDeletions = await database.lojistas.findMany({
where: {
deletion_scheduled_for: { lte: new Date() },
deleted_at: null,
},
});
for (const lojista of pendingDeletions) {
await deleteLojistaAccount(lojista.id);
// Logar deleção para auditoria
await database.deletion_log.insert({
entity_type: 'lojista',
entity_id: lojista.id,
deleted_at: new Date(),
deleted_by: 'automated_job',
reason: 'user_request_confirmed',
});
}
}
4. Exceções de retenção legal:
// cataloga-ai/lib/legal/retention-rules.ts
export const RETENTION_EXCEPTIONS = {
// Notas fiscais: 5 anos (obrigação fiscal)
invoices: { period: '5 years', reason: 'Lei 8.846/94' },
// Logs de auditoria: 5 anos (LGPD Art. 48)
audit_logs: { period: '5 years', reason: 'LGPD Art. 48' },
// Disputas judiciais: até resolução + 5 anos
legal_hold: { period: 'until_resolution', reason: 'Legítimo interesse Art. 7, IX' },
};
export function canDeleteNow(dataType: string): boolean {
const exception = RETENTION_EXCEPTIONS[dataType];
if (!exception) return true; // Sem exceção, pode deletar
// Implementar lógica de verificação de período
return false;
}
5. UI de deleção no painel:
// cataloga-ai/components/settings/DeleteAccount.tsx
export function DeleteAccountSection() {
const [showConfirmation, setShowConfirmation] = useState(false);
return (
<section className="border-red-500 border p-4 rounded">
<h3 className="text-red-600">Zona de Perigo: Excluir Conta</h3>
<p>Ao excluir sua conta:</p>
<ul className="list-disc ml-5 text-sm text-gray-700">
<li>Seus dados pessoais serão anonimizados imediatamente</li>
<li>Dados de pedidos serão mantidos por 5 anos (obrigação fiscal)</li>
<li>Você terá 30 dias para cancelar a exclusão</li>
</ul>
<Button
variant="destructive"
onClick={() => setShowConfirmation(true)}
>
Solicitar Exclusão de Conta
</Button>
{showConfirmation && (
<ConfirmationModal
title="Tem certeza?"
onConfirm={handleDeleteAccount}
/>
)}
</section>
);
}
Prioridade de Remediação
CRÍTICO — Direito ao esquecimento é um dos mais fiscalizados pela ANPD. Implementar ASAP.
Próximos Passos Recomendados
Fase 1: Crítico (30 dias)
- ✅ Implementar double opt-in WhatsApp com opt-out via comando
- ✅ Anonimizar inputs antes de enviar para GPTMaker + exigir DPA
- ✅ Adicionar aviso de coleta no checkout do consumidor final
- ✅ Implementar fluxo de deleção de conta com período de graça
- ✅ Separar opt-in de perfilamento avançado (Order Bump)
Fase 2: Alto (60 dias)
- ✅ Implementar banner de cookies + carregamento condicional de scripts
- ✅ Sanitizar logs de aplicação (remover PII)
- ✅ Implementar RLS no Supabase + views mascaradas para CS
- ✅ Validar formulários com schema estrito (remover campos excessivos)
Fase 3: Médio (90 dias)
- ✅ Implementar portabilidade self-service (exportar dados em ZIP)
- ✅ Auditoria de banco de dados para campos não utilizados
- ✅ Política de retenção de logs documentada e implementada
Anexos
A. Checklist de Auditoria Técnica (Para Revisão In-Loco)
Quando houver acesso ao código-fonte e ambiente de produção, validar:
- [ ] Verificar se
RLSestá habilitado em todas tabelas Supabase com PII - [ ] Inspecionar Vercel logs para confirmar ausência de PII
- [ ] Testar fluxo de opt-in WhatsApp (double opt-in funcionando?)
- [ ] Verificar se Google Analytics carrega ANTES ou DEPOIS do consentimento de cookies
- [ ] Revisar código de integração GPTMaker para confirmar sanitização de inputs
- [ ] Testar exportação de dados (ZIP gerado contém TODOS os dados?)
- [ ] Testar deleção de conta (anonimização ocorre? Período de graça funciona?)
- [ ] Verificar se Order Bump usa apenas categorias ou histórico completo
- [ ] Inspecionar formulários para campos desnecessários
- [ ] Revisar painéis admin (CS vê PII excessiva?)
B. Responsáveis por Remediação
| Item | Responsável Sugerido | Prazo |
|---|---|---|
| 1. Formulários | Backend + Product | 30 dias |
| 2. Checkout | Frontend + Legal | 15 dias (crítico) |
| 3. Order Bump | Data Science + Backend | 30 dias |
| 4. WhatsApp | Backend + Integrações | 15 dias (crítico) |
| 5. GPTMaker | Backend + Infra | 7 dias (crítico) |
| 6. Logs | DevOps + Backend | 30 dias |
| 7. RLS Admin | Backend + DBA | 45 dias |
| 8. Cookies | Frontend | 30 dias |
| 9. Portabilidade | Backend | 60 dias |
| 10. Deleção | Backend + DBA | 15 dias (crítico) |
C. Templates de Comunicação (Para DPO)
Template de resposta a pedido de deleção:
Prezado(a) [Nome],
Recebemos sua solicitação de exclusão de conta conforme Art. 18, VI da LGPD.
Próximos passos:
1. Seus dados pessoais serão anonimizados em até 15 dias
2. Dados de pedidos serão mantidos por 5 anos (obrigação fiscal Lei 8.846/94)
3. Você pode cancelar esta solicitação em até 30 dias: [link]
Caso tenha dúvidas, responda este e-mail.
Atenciosamente,
Equipe Cataloga-ai
Documento gerado por: Compliance QA Agent
Última atualização: 2026-04-25
Versão: 1.0