{
  "schemaVersion": 1,
  "produto": "ZapOrca",
  "tipo": "caso técnico",
  "verificado_em": "2026-10-30",
  "autor": "Jeremias Cardoso",
  "relacao_comercial": "nenhuma de terceiros; o produto é do autor, que vende a licença Pro",
  "metodo": "Leitura do código, da documentação e do histórico do repositório no commit indicado (somente leitura), mais a suíte de testes e o build rodados localmente em 30/10/2026 numa cópia do repositório (npm ci, npx vitest run, npm run build). O app não foi usado num navegador para esta revisão. Os trechos são literais, exceto as substituições entre colchetes e os cortes marcados com [...]. Nos marcos do histórico, \"assunto\" é a linha do commit e \"resumo\" é descrição do autor. Achados marcados no texto como lidos no código não foram testados.",
  "contagem": {
    "regra": "Arquivos versionados (git ls-files) no commit indicado. Linha de código: linha não branca que não é só comentário (// e /* */ em TypeScript, JavaScript e CSS, com o bloco reconhecido quando abre no início da linha; <!-- --> em HTML; -- em SQL). Aplicação: src/**/*.ts. Estilos: src/**/*.css. Página e tema: index.html e public/*.js. Testes: tests/**/*.ts. Ficam fora documentação, estado, lock de pacotes e configuração.",
    "categorias": {
      "aplicacao_ts": {
        "arquivos": 21,
        "linhas_fisicas": 7413,
        "linhas_nao_brancas": 6746,
        "linhas_de_codigo": 6451
      },
      "estilos_css": {
        "arquivos": 1,
        "linhas_fisicas": 1199,
        "linhas_nao_brancas": 1048,
        "linhas_de_codigo": 1017
      },
      "pagina_e_tema": {
        "arquivos": 2,
        "linhas_fisicas": 109,
        "linhas_nao_brancas": 103,
        "linhas_de_codigo": 68
      },
      "testes_ts": {
        "arquivos": 15,
        "linhas_fisicas": 2271,
        "linhas_nao_brancas": 1962,
        "linhas_de_codigo": 1915
      },
      "script_auxiliar": {
        "arquivos": 1,
        "linhas_fisicas": 165,
        "linhas_nao_brancas": 147,
        "linhas_de_codigo": 134
      },
      "schema_sql_legado": {
        "arquivos": 1,
        "linhas_fisicas": 83,
        "linhas_nao_brancas": 70,
        "linhas_de_codigo": 67
      }
    },
    "testes": {
      "declarados": 131,
      "regra_da_contagem": "chamadas it( e test( no início da linha em tests/*.ts",
      "execucao_local_2026_10_30": "15 arquivos e 131 testes aprovados (Vitest 2.1); build de produção sem erro (tsc e Vite 5.4)"
    },
    "build_local_2026_10_30": {
      "javascript_kb": 202.13,
      "javascript_gzip_kb": 51.34,
      "css_kb": 66.7,
      "css_gzip_kb": 12.48
    },
    "catalogo": {
      "itens": 43,
      "nichos": 8,
      "clausulas": 7
    },
    "historico_git": {
      "commits": 11,
      "primeiro": "2026-09-04",
      "ultimo": "2026-10-30",
      "tags": 0,
      "primeiro_commit": "38 files changed, 10362 insertions(+)",
      "marcos": [
        {
          "commit": "1d2d3e7",
          "data": "2026-10-09",
          "assunto": "feat: publica o ZapOrca no portal, sem nuvem e com conta neste aparelho",
          "efeito": "package.json perde as três dependências de execução (cliente do banco remoto, qrcode e zod); 27 arquivos, 2.390 inserções e 1.336 remoções"
        },
        {
          "commit": "e4db431",
          "data": "2026-10-15",
          "assunto": "feat(zaporca-2.0): implementar historico IndexedDB, upload de logotipo e exportacao PDF A4"
        },
        {
          "commit": "558e155",
          "data": "2026-10-15",
          "assunto": "feat(zaporca): redesign executivo, modal centralizado, biblioteca de logos e anti-interrogacao whatsapp"
        },
        {
          "commit": "6d38799",
          "data": "2026-10-15",
          "resumo": "plano Pro, Pix de R$ 29,90 e exportação CSV"
        },
        {
          "commit": "084a834",
          "data": "2026-10-30",
          "assunto": "fix(pix): perfil de exemplo sem chave real, Pix no texto [...]"
        }
      ]
    }
  },
  "substituicoes": [
    "[e-mail]",
    "[id do perfil de exemplo]",
    "[os dados da conta]",
    "[cidade padrão]",
    "[responsável]",
    "[...]"
  ],
  "limites": [
    "Não é laudo independente de qualidade nem de segurança, e não descreve a proteção da conta local nem da licença.",
    "A suíte e o build foram rodados; o app não foi usado num navegador nesta revisão. Os achados sobre o teto do plano grátis, a numeração e a fila vêm da leitura do código.",
    "O repositório não registra usuários, vendas da licença Pro nem tempo economizado; o texto não afirma adoção nem ganho.",
    "O texto descreve o código do repositório; o que está publicado no portal depende de um passo manual e não foi conferido.",
    "Não se publicam nomes de empresa, de pessoas, de cidade, endereços de e-mail, domínios, chaves nem credenciais."
  ],
  "fontes": [
    {
      "repositorio": "ZapOrca",
      "commit": "a4f7f57ce9b7e25c0768a07c6873b8aa832811cb",
      "data_do_commit": "2026-10-30"
    }
  ],
  "alegacoes": [
    {
      "id": "publico",
      "alegacao": "O produto é para MEIs e prestadores de serviço: fechar o serviço e receber o sinal via Pix.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        10,
        10
      ],
      "trecho": "O **ZapOrca** resolve a principal dor do MEI brasileiro (eletricistas, pintores, marceneiros, mecânicos, prestadores de serviços gerais): **fechar serviços rapidamente e receber o sinal via Pix sem complicação**."
    },
    {
      "id": "tres-passos",
      "alegacao": "A proposta é criada em três passos: cliente, itens e condições de pagamento com sinal Pix.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        12,
        12
      ],
      "trecho": "- **Criação de Proposta em 3 Passos (Menos de 60 segundos):** Cliente -> Itens/Serviços -> Condições de Pagamento e Sinal Pix."
    },
    {
      "id": "quem-marca-sinal",
      "alegacao": "Quem marca \"recebi o sinal\" é o prestador, depois de olhar o app do banco.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        15,
        15
      ],
      "trecho": "- **Visão do Cliente:** Tela em que o cliente copia o Pix Copia e Cola. Quem marca “recebi o sinal” é o prestador, depois de olhar o app do banco."
    },
    {
      "id": "sem-nuvem",
      "alegacao": "A conta é opcional e local; não há nuvem nem confirmação bancária.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        16,
        16
      ],
      "trecho": "- **Painel do prestador:** Propostas neste aparelho. A conta é opcional e local. Não há nuvem nem confirmação bancária."
    },
    {
      "id": "sem-zod",
      "alegacao": "A validação usa tipos e safeParse próprios, sem a biblioteca Zod em tempo de execução.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        24,
        24
      ],
      "trecho": "- **Validação:** tipos e `safeParse` próprios, sem Zod no runtime."
    },
    {
      "id": "br-code-navegador",
      "alegacao": "O BR Code é montado no navegador (TLV e CRC16, polinômio 0x1021) e o QR usa modo byte com correção M.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        25,
        25
      ],
      "trecho": "- **Engine Pix:** BR Code no navegador (EMV TLV + CRC16-CCITT, polinômio 0x1021) e QR em modo byte, correção M."
    },
    {
      "id": "readme-link",
      "alegacao": "O README ainda descreve a mensagem com o link da proposta.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        14,
        14
      ],
      "trecho": "- **Compartilhamento Direto no WhatsApp:** Mensagem persuasiva com link seguro (/p/:uuid) para o cliente final aprovar e pagar o sinal."
    },
    {
      "id": "build-portal",
      "alegacao": "O build para o portal é gerado à parte, com a pasta de publicação configurada.",
      "repositorio": "ZapOrca",
      "arquivo": "README.md",
      "linhas": [
        45,
        50
      ],
      "trecho": "Build para publicar em `/zaporca/` no BRUP (sem mapa de código):\n\n```powershell\n$env:ZAPORCA_BASE = '/zaporca/'\nnpm run build\n```"
    },
    {
      "id": "status-portal",
      "alegacao": "O produto está no ar como porta separada do portal do autor.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        6,
        6
      ],
      "trecho": "> **Status:** No ar como porta separada do portal BRUP. O progresso de cada tarefa está em [estado/TAREFAS.md](estado/TAREFAS.md)."
    },
    {
      "id": "porta-separada",
      "alegacao": "O ZapOrca mora numa pasta própria do portal e não entra na grade de ferramentas grátis.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        14,
        14
      ],
      "trecho": "- *Decisão:* O orçamento mora em `/zaporca/` no portal BRUP. As ferramentas grátis continuam no catálogo. O ZapOrca não entra na grade grátis."
    },
    {
      "id": "tema-idioma",
      "alegacao": "Tema e idioma (pt, en, es) usam as mesmas chaves do portal.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        30,
        30
      ],
      "trecho": "- *Decisão:* A escolha fica em `brup-theme`, com os valores `light` e `dark`. O valor antigo `graphite-blue` em `zaporca_theme` é lido como escuro. O idioma da casca usa `brup_lang` (pt, en, es)."
    },
    {
      "id": "nuvem-saiu",
      "alegacao": "A fila ficou no aparelho; o cliente de nuvem saiu e não há conta remota nem confirmação de Pix por banco.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        38,
        39
      ],
      "trecho": "- *Decisão:* A fila continua no aparelho. O cliente de nuvem saiu do app em 2026-10-08. Não há conta remota nem confirmação de Pix por banco.\n   - *Motivo:* O prestador usa o primeiro orçamento sem depender de um serviço externo. A fila antiga, desenhada para um backend, não empurra mais nada para fora."
    },
    {
      "id": "exclusao",
      "alegacao": "Excluir a conta apaga, no aparelho, perfil, orçamentos, fila e transações; a confirmação é digitar o e-mail.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        54,
        54
      ],
      "trecho": "- *Decisão:* Excluir a conta apaga, neste aparelho, perfil, orçamentos, fila, transações e [os dados da conta]. A confirmação continua sendo digitar o e-mail. Não há descarte remoto porque não há conta remota."
    },
    {
      "id": "sem-banco-remoto",
      "alegacao": "O app publicado não usa banco remoto; o teste de isolamento que resta é um modelo em memória.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        58,
        58
      ],
      "trecho": "- *Decisão:* Não há RLS nem RPC de nuvem no ZapOrca que está no ar. O teste de isolamento que resta é um modelo em memória e não fala com um servidor."
    },
    {
      "id": "indexeddb",
      "alegacao": "IndexedDB, logotipo reduzido e proposta A4 com @page; motivo: a cota de 5 MB do armazenamento simples.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        62,
        63
      ],
      "trecho": "- *Decisão:* Adicionar persistência assíncrona com IndexedDB nativo (database `zaporca_db_v2`), upload e compressão client-side em canvas de logo (máx 360x180 px), exibição no dashboard e na proposta pública, e geração de PDF A4 formal via `@page { size: A4 portrait; margin: 12mm 15mm; }` com barra de ações de impressão.\n    - *Motivo:* Permite armazenar centenas de orçamentos e imagens sem risco de estourar a cota de 5 MB do `localStorage`, mantendo fallback retrocompatível e dando aspecto profissional de agência às propostas do MEI."
    },
    {
      "id": "dados-no-aparelho",
      "alegacao": "O que o prestador editou fica no navegador; não há mescla com servidor.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        75,
        75
      ],
      "trecho": "- **Política de dados neste aparelho:** O que o prestador editou fica no navegador. Não há mescle com servidor no produto publicado."
    },
    {
      "id": "confirmacao-exclusao",
      "alegacao": "O botão de exclusão fica bloqueado até o texto digitado bater com o e-mail da conta.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        77,
        77
      ],
      "trecho": "- **Dupla Confirmação Bloqueante para Exclusão LGPD:** O botão de exclusão permanece bloqueado com opacidade reduzida até que o input de confirmação coincida exatamente com o e-mail do usuário ativo, prevenindo toques acidentais."
    },
    {
      "id": "tabela-remota",
      "alegacao": "Tabela remota só volta se o dono decidir ter servidor de novo.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        78,
        78
      ],
      "trecho": "- **Sem tabela remota no produto publicado:** Uma tabela nova só volta a existir se o dono decidir ter servidor de novo. Até lá, o dado nasce no aparelho."
    },
    {
      "id": "tema-escuro",
      "alegacao": "Fundo claro com letra clara apareceu porque o tema do aparelho e o tema do portal são escolhas diferentes.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        93,
        93
      ],
      "trecho": "- **Tema manual e utilitário `dark:` não são a mesma coisa:** O seletor `dark:` do Tailwind segue o tema do aparelho. A escolha do portal está em `data-theme`. Fundo claro com letra clara apareceu quando só a letra trocou de token."
    },
    {
      "id": "folha-pix",
      "alegacao": "A folha do Pix abria sozinha por empate entre a regra de exibição e a classe que a esconde.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        95,
        95
      ],
      "trecho": "- **Folha Pix e a classe `hidden`:** `display: flex` no backdrop empatava com `.hidden` e a folha abria sozinha. A regra `.bottom-sheet-backdrop.hidden` resolve o empate."
    },
    {
      "id": "logo-canvas",
      "alegacao": "O logotipo é reduzido num canvas de no máximo 360 por 180 pixels.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        97,
        97
      ],
      "trecho": "- **Redimensionamento de imagem client-side em canvas para logotipo:** Redimensionar imagens enviadas pelo usuário em um canvas HTML5 restrito a 360x180 px garante que o base64 gerado fique abaixo de 50 kB, permitindo carregar e imprimir rapidamente propostas sem travar o navegador nem estourar memória, persistindo com folga no IndexedDB."
    },
    {
      "id": "v10",
      "alegacao": "Em 15/10/2026 entraram o IndexedDB, o logotipo e a proposta A4.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        103,
        103
      ],
      "trecho": "- **v10 (2026-10-15):** Expansão ZapOrca 2.0 (Fase 4). Implementação de persistência assíncrona robusta em IndexedDB nativo (`idb.ts`) com fallback e sincronização bi-direcional em `localStorage` (`storage.ts`); suporte a upload e compressão client-side em canvas de logo da empresa (máx 360x180 px); logotipo em alta resolução no dashboard, detalhes, tela do cliente e template de proposta em PDF A4 com CSS `@page`; suporte a duplicação e exclusão rápida de orçamentos. Homologação completa com 101/101 testes unitários aprovados no Vitest e suíte Playwright smoke CA aprovada no portal BRUP."
    },
    {
      "id": "v9",
      "alegacao": "Em 09/10/2026, depois da publicação no portal, a fila ficou sem nuvem.",
      "repositorio": "ZapOrca",
      "arquivo": "MEMORIA-PROJETO.md",
      "linhas": [
        104,
        104
      ],
      "trecho": "[...] **v9 (2026-10-09):** Revisão do que deixou de ser verdade depois da publicação no portal [...] fila sem nuvem [...]"
    },
    {
      "id": "versao",
      "alegacao": "Versão 1.0.0 do pacote.",
      "repositorio": "ZapOrca",
      "arquivo": "package.json",
      "linhas": [
        3,
        3
      ],
      "trecho": "\"version\": \"1.0.0\","
    },
    {
      "id": "so-dev",
      "alegacao": "O pacote declara só dependências de desenvolvimento (Tailwind, TypeScript, Vite e Vitest), sem dependência de execução nem ferramenta de teste de interface.",
      "repositorio": "ZapOrca",
      "arquivo": "package.json",
      "linhas": [
        23,
        30
      ],
      "trecho": "\"devDependencies\": {\n    \"@tailwindcss/vite\": \"^4.3.3\",\n    \"@types/node\": \"^22.8.7\",\n    \"tailwindcss\": \"^4.3.3\",\n    \"typescript\": \"^5.6.3\",\n    \"vite\": \"^5.4.10\",\n    \"vitest\": \"^2.1.4\"\n  }"
    },
    {
      "id": "nuvem-antes",
      "alegacao": "A primeira fase teve esquema com acesso por linha num banco remoto, cadastro, login com carga da nuvem e sincronização.",
      "repositorio": "ZapOrca",
      "arquivo": "estado/TAREFAS.md",
      "linhas": [
        29,
        34
      ],
      "trecho": "| 9 | Schema Supabase com RLS (empresas, orcamentos) e Cliente Configurado | concluida | [responsável] | 2026-08-28 |\n| 10 | Spike: Compatibilidade do Outbox Atual com Sync Idempotente ao Supabase | concluida | [responsável] | 2026-08-28 |\n| 11 | Cadastro de Conta com Consentimento LGPD e Vinculação de Dados Locais | concluida | [responsável] | 2026-09-05 |\n| 12 | Login, Sessão Persistente e Carregamento da Nuvem com Mescle do Outbox | concluida | [responsável] | 2026-09-05 |\n| 13 | Perfil da Empresa — Formulário, Validação de Chave Pix e Sync | concluida | [responsável] | 2026-09-05 |\n| 14 | Sync de Orçamentos via Outbox Idempotente (Documento Completo, Dedupe por UUID, Drain na Reconexão) | concluida | [responsável] | 2026-09-05 |"
    },
    {
      "id": "tarefa-convite",
      "alegacao": "Convidar os primeiros 10 prestadores pelo WhatsApp, sem mídia paga, consta como não iniciada.",
      "repositorio": "ZapOrca",
      "arquivo": "estado/TAREFAS.md",
      "linhas": [
        15,
        15
      ],
      "trecho": "| 22 | Convidar 10 prestadores conhecidos pelo WhatsApp, sem mídia paga | nao_iniciada | jeremias | 2026-10-09 |"
    },
    {
      "id": "crc16",
      "alegacao": "CRC16-CCITT com polinômio 0x1021 e valor inicial 0xFFFF.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        46,
        51
      ],
      "trecho": "/**\n * Calcula o CRC16-CCITT no padrão oficial do Banco Central do Brasil (Polinômio 0x1021, Init 0xFFFF).\n */\nexport function calculateCRC16(payload: string): string {\n  let crc = 0xFFFF;\n  const polynomial = 0x1021;"
    },
    {
      "id": "chave-nula",
      "alegacao": "Os exemplos usam a chave aleatória nula, que não pertence a ninguém.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        67,
        68
      ],
      "trecho": "/** Chave aleatória nula usada só nos orçamentos de exemplo: não pertence a ninguém. */\nexport const CHAVE_PIX_EXEMPLO = '00000000-0000-0000-0000-000000000000';"
    },
    {
      "id": "normaliza-chave",
      "alegacao": "A chave é normalizada: CPF e CNPJ só com dígitos, telefone com +55 e DDD, e-mail e aleatória em minúsculas.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        70,
        76
      ],
      "trecho": "/**\n * Põe a chave no formato em que ela é registrada no Pix: CPF e CNPJ só com dígitos,\n * telefone com +55 e DDD, e-mail e chave aleatória em minúsculas.\n */\nexport function normalizarChavePix(chave: string, tipo?: TipoChavePix): string {\n  const limpa = chave.trim();\n  if (tipo === 'cpf' || tipo === 'cnpj') return limpa.replace(/\\D/g, '');"
    },
    {
      "id": "chave-configurada",
      "alegacao": "A chave do prestador precisa estar preenchida, ser válida e não ser a de exemplo.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        87,
        91
      ],
      "trecho": "/** A chave do prestador está preenchida, é válida e não é a chave de exemplo. */\nexport function chavePixConfigurada(chave: string | undefined, tipo: TipoChavePix | undefined): boolean {\n  if (!chave || !chave.trim() || !tipo) return false;\n  if (chave.trim().toLowerCase() === CHAVE_PIX_EXEMPLO) return false;\n  return validatePixKey(chave, tipo).valid;"
    },
    {
      "id": "pix-estatico",
      "alegacao": "O payload é de Pix estático, sem URL de cobrança.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        94,
        97
      ],
      "trecho": "/**\n * Monta o payload BR Code do Pix estático (sem URL de cobrança) e anexa o CRC16.\n */\nexport function generatePixPayload(params: PixParams): PixGenerationResult {"
    },
    {
      "id": "nome-cidade",
      "alegacao": "Nome do recebedor com até 25 caracteres e cidade com até 15, sanitizados.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        99,
        101
      ],
      "trecho": "const nomeSanitizado = sanitizePixText(params.nomeRecebedor || 'RECEBEDOR', 25) || 'RECEBEDOR';\n  const cidadeSanitizada = sanitizePixText(params.cidadeRecebedor || [cidade padrão], 15) || [cidade padrão];\n  const txIdSanitizado = sanitizePixText(params.txId || '***', 25) || '***';"
    },
    {
      "id": "uso-unico",
      "alegacao": "A tag 01 recebe 12, o código de uso único, um por orçamento.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        107,
        109
      ],
      "trecho": "// Tag 01: Point of Initiation Method. 12 = código de uso único (um por orçamento, com o próprio txid);\n  // 11 seria o código reutilizável.\n  payload += formatTLV('01', '12');"
    },
    {
      "id": "conta-recebedor",
      "alegacao": "A conta do recebedor leva o identificador do Pix e a chave do prestador.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        111,
        113
      ],
      "trecho": "// Tag 26: Merchant Account Information\n  let merchantAccount = formatTLV('00', 'BR.GOV.BCB.PIX');\n  merchantAccount += formatTLV('01', chavePixLimpa);"
    },
    {
      "id": "moeda",
      "alegacao": "Moeda 986, do real.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        125,
        126
      ],
      "trecho": "// Tag 53: Transaction Currency (986 = BRL)\n  payload += formatTLV('53', '986');"
    },
    {
      "id": "crc-final",
      "alegacao": "O CRC é calculado sobre o payload com o prefixo 6304 e anexado no fim.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        146,
        149
      ],
      "trecho": "// Tag 63: CRC16 prefix\n  const payloadParaCRC = `${payload}6304`;\n  const crc = calculateCRC16(payloadParaCRC);\n  const brCode = `${payloadParaCRC}${crc}`;"
    },
    {
      "id": "wa-me",
      "alegacao": "O link do WhatsApp abre a conversa com o número do cliente e o texto codificado.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        160,
        167
      ],
      "trecho": "/**\n * Gera o link direto de envio para o WhatsApp (wa.me) com texto codificado.\n */\nexport function generateWhatsAppShareUrl(phone: string, text: string): string {\n  const digitsOnly = phone.replace(/\\D/g, '');\n  const fullNumber = digitsOnly.startsWith('55') ? digitsOnly : `55${digitsOnly}`;\n  const encodedText = encodeURIComponent(text);\n  return `https://wa.me/${fullNumber}?text=${encodedText}`;"
    },
    {
      "id": "email-77",
      "alegacao": "Chave de e-mail com no máximo 77 caracteres.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pix.ts",
      "linhas": [
        287,
        290
      ],
      "trecho": "case 'email': {\n      if (trimmed.length > 77) {\n        return { valid: false, error: 'Chave de e-mail excede o limite de 77 caracteres do BACEN' };\n      }"
    },
    {
      "id": "txid-numero",
      "alegacao": "O identificador da transação sai do número do orçamento.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        451,
        458
      ],
      "trecho": "const pixResult = temChave ? generatePixPayload({\n      chavePix: orcamento.perfilMei.chavePix,\n      tipoChave: orcamento.perfilMei.tipoChavePix,\n      nomeRecebedor: orcamento.perfilMei.nome,\n      cidadeRecebedor: orcamento.perfilMei.cidade,\n      valor: valorCobrar,\n      txId: orcamento.numero.replace(/[^a-zA-Z0-9]/g, '')\n    }) : null;"
    },
    {
      "id": "crc-teste",
      "alegacao": "O teste confere o CRC de \"123456789\" contra o valor de referência 29B1.",
      "repositorio": "ZapOrca",
      "arquivo": "tests/pix.test.ts",
      "linhas": [
        82,
        83
      ],
      "trecho": "it('CRC16-CCITT (0x1021, início 0xFFFF) de \"123456789\" é 29B1, o valor de conferência do algoritmo', () => {\n    expect(calculateCRC16('123456789')).toBe('29B1');"
    },
    {
      "id": "qr-propria",
      "alegacao": "QR em modo byte, correção M, implementação própria conferida contra o pacote qrcode antes de remover a dependência.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/qr.ts",
      "linhas": [
        1,
        5
      ],
      "trecho": "/**\n * QR Code modo byte, correção M (ISO/IEC 18004).\n * Reimplementação própria para o Pix. A matriz é conferida contra o pacote qrcode\n * em strings forçadas para byte mode antes de remover essa dependência.\n */"
    },
    {
      "id": "qr-congelado",
      "alegacao": "Dois testes exigem que o QR reproduza as matrizes congeladas, uma curta e uma longa.",
      "repositorio": "ZapOrca",
      "arquivo": "tests/qr.test.ts",
      "linhas": [
        25,
        34
      ],
      "trecho": "describe('QR byte mode com matriz congelada', () => {\n  it('reproduz a matriz curta conferida em byte mode', () => {\n    const frozen = readFileSync(resolve(fixtures, 'qr-hello-pix.txt'), 'utf8').replace(/\\r\\n/g, '\\n').trim();\n    expect(bitsOf(SHORT)).toBe(frozen);\n  });\n\n  it('reproduz a matriz longa conferida em byte mode', () => {\n    const frozen = readFileSync(resolve(fixtures, 'qr-pix-byte.txt'), 'utf8').replace(/\\r\\n/g, '\\n').trim();\n    expect(bitsOf(LONG)).toBe(frozen);\n  });"
    },
    {
      "id": "pix-no-texto",
      "alegacao": "O código Pix vai no texto só quando a chave é do prestador, porque o link só abre no aparelho de quem criou.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/whatsapp-message.ts",
      "linhas": [
        9,
        19
      ],
      "trecho": "/**\n * Código Pix copia e cola do orçamento, só quando a chave é do próprio prestador.\n * Ele vai no texto da mensagem porque o link da proposta só abre no aparelho de quem a criou\n * (os dados ficam no navegador dele); o cliente precisa conseguir pagar de qualquer aparelho.\n */\nexport function codigoPixDoOrcamento(orc: Orcamento): string | null {\n  const codigo = orc.payloadPix?.payloadFormatado;\n  if (!codigo) return null;\n  if (!chavePixConfigurada(orc.perfilMei.chavePix, orc.perfilMei.tipoChavePix)) return null;\n  return codigo;\n}"
    },
    {
      "id": "seis-itens",
      "alegacao": "A mensagem lista até seis itens e avisa quantos ficaram de fora.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/whatsapp-message.ts",
      "linhas": [
        27,
        30
      ],
      "trecho": "const itens = orc.itens.slice(0, 6).map((item) => {\n    return `• ${item.descricao}\\n   ${item.quantidade} ${item.unidade} · *${formatCurrency(item.subtotal)}*`;\n  });\n  const extras = orc.itens.length > 6 ? [`• e mais ${orc.itens.length - 6} item(ns) no link`] : [];"
    },
    {
      "id": "avisos-pix",
      "alegacao": "Depois do código vêm os avisos: o valor cai na chave do prestador e o ZapOrca não confirma o pagamento.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/whatsapp-message.ts",
      "linhas": [
        56,
        66
      ],
      "trecho": "const pix = codigoPixDoOrcamento(orc);\n  if (pix) {\n    blocos.push(\n      ``,\n      `📋 *Pix copia e cola* (cole no app do seu banco):`,\n      pix,\n      ``,\n      `O valor cai na chave Pix de *${prestador}*.`,\n      `O ZapOrca não confirma o pagamento do banco.`\n    );\n  }"
    },
    {
      "id": "modo-sem-emoji",
      "alegacao": "Há uma versão sem os emojis que o WhatsApp no computador troca por \"?\".",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/whatsapp-message.ts",
      "linhas": [
        75,
        78
      ],
      "trecho": "/**\n * Mensagem em modo 100% compatível (sem emojis de 4 bytes que o Windows WhatsApp Desktop converte em '?')\n */\nexport function buildCleanWhatsAppQuoteMessage(orc: Orcamento): string {"
    },
    {
      "id": "dica-anti-interrogacao",
      "alegacao": "A tela explica quando usar o modo Anti-'?' ou copiar a mensagem.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        1639,
        1639
      ],
      "trecho": "💡 <strong>Dica de compatibilidade:</strong> Se no WhatsApp no computador os ícones ficarem com \"?\", use o modo <em>Anti-'?'</em> ou clique em <strong>Copiar Mensagem</strong> para colar direto no chat."
    },
    {
      "id": "desconto-limite",
      "alegacao": "O desconto nunca passa do subtotal e é arredondado em centavos.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/orcamento-calc.ts",
      "linhas": [
        39,
        41
      ],
      "trecho": "valorDesconto = Math.min(subtotalArredondado, Math.round(valorDesconto * 100) / 100);\n\n  const valorTotal = Math.max(0, Math.round((subtotalArredondado - valorDesconto) * 100) / 100);"
    },
    {
      "id": "sinal",
      "alegacao": "O sinal é uma porcentagem do total entre 0 e 100; o saldo é o resto.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/orcamento-calc.ts",
      "linhas": [
        43,
        45
      ],
      "trecho": "const percSinal = Math.min(100, Math.max(0, percentualSinal ?? 30));\n  const valorSinal = Math.round((valorTotal * (percSinal / 100)) * 100) / 100;\n  const valorRestante = Math.max(0, Math.round((valorTotal - valorSinal) * 100) / 100);"
    },
    {
      "id": "estados",
      "alegacao": "A máquina de estados do orçamento vai de rascunho a concluído, com recusa.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/orcamento-calc.ts",
      "linhas": [
        83,
        91
      ],
      "trecho": "export const STATUS_TRANSITIONS: Record<OrcamentoStatus, OrcamentoStatus[]> = {\n  rascunho: ['enviado', 'recusado'],\n  enviado: ['visualizado', 'aprovado', 'sinal_pago', 'recusado'],\n  visualizado: ['aprovado', 'sinal_pago', 'recusado'],\n  aprovado: ['sinal_pago', 'concluido', 'recusado'],\n  sinal_pago: ['concluido', 'recusado'],\n  concluido: [],\n  recusado: ['rascunho']\n};"
    },
    {
      "id": "cliente-obrigatorio",
      "alegacao": "No primeiro passo, nome e WhatsApp do cliente são obrigatórios.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        996,
        999
      ],
      "trecho": "if (this.wizardStep === 1) {\n        if (!f.clienteNome || !f.clienteTelefone) {\n          alert('Por favor, preencha o Nome e o WhatsApp do cliente.');\n          return;"
    },
    {
      "id": "cliente-opcional",
      "alegacao": "E-mail e endereço do cliente são opcionais.",
      "repositorio": "ZapOrca",
      "arquivo": "src/contracts/schemas.ts",
      "linhas": [
        32,
        37
      ],
      "trecho": "export interface Cliente {\n  id: string;\n  nome: string;\n  telefone: string;\n  email?: string;\n  endereco?: string;"
    },
    {
      "id": "trava-chave",
      "alegacao": "Sem a chave do prestador, o orçamento não é criado e o perfil é aberto.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        1154,
        1162
      ],
      "trecho": "private finishAndSaveWizard(): void {\n    const f = this.newOrcamentoForm;\n    const perfil = storage.getPerfilMei();\n    // Sem a chave do próprio prestador, o Pix iria para outra pessoa ou para lugar nenhum.\n    if (!chavePixConfigurada(perfil.chavePix, perfil.tipoChavePix)) {\n      this.showToast('Cadastre a sua chave Pix no perfil antes de gerar o orçamento.', '⚠️');\n      this.openPerfilEmpresaModal();\n      return;\n    }"
    },
    {
      "id": "numeracao",
      "alegacao": "O número é ORC-2026- seguido da quantidade de orçamentos guardados mais um.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        1165,
        1165
      ],
      "trecho": "const numero = `ORC-2026-${String(storage.getOrcamentos().length + 1).padStart(3, '0')}`;"
    },
    {
      "id": "copiar-pix",
      "alegacao": "O botão da tela de detalhes copia o código Pix.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        1306,
        1309
      ],
      "trecho": "<button id=\"btn-detalhe-copiar-link\" class=\"btn-touch btn-secondary text-xs font-bold\">\n              ${ICONS.copy}\n              <span>Copiar código Pix</span>\n            </button>"
    },
    {
      "id": "link-nao-abre",
      "alegacao": "A tela do cliente busca a proposta no armazenamento do aparelho e, sem ela, diz que não a encontrou.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        1394,
        1401
      ],
      "trecho": "// VIEW 4: VISÃO DO CLIENTE FINAL (#/p/:uuid)\n  // ==========================================\n  private async renderClientView(uuid: string): Promise<void> {\n    const orc = storage.getOrcamentoByUuid(uuid);\n    if (!orc) {\n      this.container.innerHTML = `\n        <div class=\"p-12 text-center space-y-4\">\n          <p class=\"text-neutral-500 font-medium\">Proposta não encontrada ou expirada.</p>"
    },
    {
      "id": "busca-local",
      "alegacao": "A busca pelo identificador público lê a lista guardada no aparelho.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        441,
        442
      ],
      "trecho": "public getOrcamentoByUuid(uuid: string): Orcamento | undefined {\n    return this.getOrcamentos().find(o => o.uuid === uuid);"
    },
    {
      "id": "teto-gratis",
      "alegacao": "Sem o Pro, o teto é de cinco orçamentos guardados.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        307,
        310
      ],
      "trecho": "this.container.querySelector('#btn-landing-novo')?.addEventListener('click', () => {\n      const orcs = storage.getOrcamentos();\n      if (!isZapOrcaProActive() && orcs.length >= 5) {\n        this.showToast('Você atingiu o teto de 5 propostas do plano Grátis.', '✨');"
    },
    {
      "id": "plano-gratis",
      "alegacao": "O plano grátis vai até 5 propostas; o Pro custa R$ 29,90 por ano sem renovação automática.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        2051,
        2055
      ],
      "trecho": "<span>${isZapOrcaProActive() ? 'Passaporte ZapOrca Pro Ativo' : 'Plano Grátis (Até 5 propostas)'}</span>\n                ${isZapOrcaProActive() ? '<span class=\"text-[9px] bg-amber-100 dark:bg-amber-950/60 text-amber-800 dark:text-amber-300 font-bold px-1.5 py-0.2 rounded-full uppercase\">Oficial</span>' : ''}\n              </div>\n              <div class=\"text-[10px] text-neutral-400\">\n                ${isZapOrcaProActive() ? 'Orçamentos ilimitados, sem marca d’água & exportação CSV' : 'Ative o Pro por R$ 29,90/ano sem renovação automática'}"
    },
    {
      "id": "csv-pro",
      "alegacao": "A exportação em planilha é exclusiva do Pro.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        585,
        585
      ],
      "trecho": "this.showToast('A exportação em planilha é exclusiva do ZapOrca Pro.', '✨');"
    },
    {
      "id": "csv-excel",
      "alegacao": "O CSV usa ponto e vírgula e marca UTF-8 para abrir no Excel com acentos.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/export-csv.ts",
      "linhas": [
        4,
        8
      ],
      "trecho": "/**\n * Exporta a lista de orçamentos para um arquivo CSV formatado para Excel em Português do Brasil.\n * Utiliza ponto e vírgula como separador e inclui o marcador BOM UTF-8 (\\uFEFF)\n * para abertura direta no Microsoft Excel sem desconfigurar caracteres acentuados.\n */"
    },
    {
      "id": "preco-pro",
      "alegacao": "Plano anual de R$ 29,90, pagamento único sem renovação automática.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/licenca/modal-pro.ts",
      "linhas": [
        188,
        190
      ],
      "trecho": "<div class=\"text-xs text-neutral-500\">Valor do Plano Anual (12 meses):</div>\n                  <div class=\"text-2xl font-black text-emerald-600 dark:text-emerald-400\">R$ 29,90 <span class=\"text-xs font-semibold text-neutral-400\">(R$ 2,49/mês)</span></div>\n                  <div class=\"text-[11px] text-neutral-400\">Pagamento único sem renovação automática no cartão</div>"
    },
    {
      "id": "compra-manual",
      "alegacao": "Depois do Pix, o comprador envia o comprovante por e-mail ou WhatsApp para receber a chave Pro.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/licenca/modal-pro.ts",
      "linhas": [
        209,
        209
      ],
      "trecho": "Após pagar os R$ 29,90 no seu banco (Chave: <strong>[e-mail]</strong>), envie o comprovante por e-mail ou WhatsApp para receber sua Chave Pro:"
    },
    {
      "id": "desativar",
      "alegacao": "A licença pode ser desativada no aparelho.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/licenca/modal-pro.ts",
      "linhas": [
        105,
        106
      ],
      "trecho": "<button type=\"button\" id=\"btn-desativar-pro\" class=\"btn-touch btn-secondary flex-1 py-3 text-xs font-bold text-red-600 hover:text-red-700 border-red-200 dark:border-red-900/40\">\n                Desativar neste Aparelho"
    },
    {
      "id": "ativar-codigo",
      "alegacao": "O comprador ativa a licença digitando o código recebido.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/licenca/modal-pro.ts",
      "linhas": [
        229,
        229
      ],
      "trecho": "<form id=\"form-ativar-chave\" class=\"space-y-4\">"
    },
    {
      "id": "passo1-nome",
      "alegacao": "Passo 1: nome do cliente, obrigatório.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        718,
        718
      ],
      "trecho": "<label class=\"block text-xs font-bold text-neutral-700 uppercase tracking-wider mb-1.5\">Nome do Cliente *</label>"
    },
    {
      "id": "passo1-whatsapp",
      "alegacao": "Passo 1: WhatsApp do cliente, obrigatório.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        723,
        723
      ],
      "trecho": "<label class=\"block text-xs font-bold text-neutral-700 uppercase tracking-wider mb-1.5\">WhatsApp com DDD *</label>"
    },
    {
      "id": "passo1-titulo",
      "alegacao": "Passo 1: título do serviço, obrigatório.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        729,
        729
      ],
      "trecho": "<label class=\"block text-xs font-bold text-neutral-700 uppercase tracking-wider mb-1.5\">Título / Objeto do Serviço *</label>"
    },
    {
      "id": "passo1-endereco",
      "alegacao": "Passo 1: endereço da obra, opcional.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        734,
        734
      ],
      "trecho": "<label class=\"block text-xs font-bold text-neutral-700 uppercase tracking-wider mb-1.5\">Endereço da Obra (Opcional)</label>"
    },
    {
      "id": "passo1-prazo",
      "alegacao": "Passo 1: prazo estimado.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        739,
        739
      ],
      "trecho": "<label class=\"block text-xs font-bold text-neutral-700 uppercase tracking-wider mb-1.5\">Prazo Estimado de Execução</label>"
    },
    {
      "id": "sinal-opcoes",
      "alegacao": "O sinal se escolhe entre sem sinal, 30%, 50% e 100%.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        856,
        856
      ],
      "trecho": "${[0, 30, 50, 100].map(p => `"
    },
    {
      "id": "sem-sinal-rotulo",
      "alegacao": "A opção 0% aparece como \"Sem Sinal\".",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        862,
        862
      ],
      "trecho": "${p === 0 ? 'Sem Sinal' : `${p}%`}"
    },
    {
      "id": "termos-livres",
      "alegacao": "O passo 3 tem um texto livre de termos, prazos e garantia.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        920,
        920
      ],
      "trecho": "<label class=\"block text-xs font-bold text-neutral-700 dark:text-neutral-300 uppercase\">Termos, Prazos e Garantia (Texto Final)</label>"
    },
    {
      "id": "condicao-fixa",
      "alegacao": "A condição de pagamento nasce como texto fixo, com 30% de sinal.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        207,
        207
      ],
      "trecho": "condicoesPagamento: '30% de sinal Pix para início e compra de insumos. Saldo na conclusão dos serviços.'"
    },
    {
      "id": "condicao-na-mensagem",
      "alegacao": "A condição vai para a mensagem do WhatsApp.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/whatsapp-message.ts",
      "linhas": [
        54,
        54
      ],
      "trecho": "if (orc.condicoesPagamento) blocos.push(linha('💳 Condição:', orc.condicoesPagamento));"
    },
    {
      "id": "condicao-na-proposta",
      "alegacao": "A condição vai para a proposta.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pdf.ts",
      "linhas": [
        517,
        518
      ],
      "trecho": "${orcamento.condicoesPagamento ? `\n          <div class=\"terms-row\"><strong>Forma de Pagamento:</strong> ${orcamento.condicoesPagamento}</div>"
    },
    {
      "id": "sem-sinal-cobra-total",
      "alegacao": "Sem sinal, o código Pix cobra o total.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        449,
        449
      ],
      "trecho": "const valorCobrar = orcamento.valorSinal > 0 ? orcamento.valorSinal : orcamento.valorTotal;"
    },
    {
      "id": "home-promete-link",
      "alegacao": "A página inicial ainda fala no link da proposta.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/i18n.ts",
      "linhas": [
        11,
        11
      ],
      "trecho": "heroText: 'Três passos no celular: o serviço, o valor e o link. O cliente abre a proposta e copia o Pix. O dinheiro cai na chave do prestador. O ZapOrca não vê o pagamento e não confirma o banco.',"
    },
    {
      "id": "card-link",
      "alegacao": "O cartão do WhatsApp na página inicial ainda fala no link da proposta.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/i18n.ts",
      "linhas": [
        16,
        17
      ],
      "trecho": "cardWhatsTitle: 'Link no WhatsApp',\n    cardWhatsText: 'A mensagem sai do seu WhatsApp, pelo wa.me, com o link da proposta.',"
    },
    {
      "id": "mais-itens-link",
      "alegacao": "Com mais de seis itens, a mensagem manda ver o resto no link.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/whatsapp-message.ts",
      "linhas": [
        30,
        30
      ],
      "trecho": "const extras = orc.itens.length > 6 ? [`• e mais ${orc.itens.length - 6} item(ns) no link`] : [];"
    },
    {
      "id": "idb-lojas",
      "alegacao": "O IndexedDB guarda orçamentos e perfil.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/idb.ts",
      "linhas": [
        26,
        33
      ],
      "trecho": "if (!db.objectStoreNames.contains('orcamentos')) {\n          const store = db.createObjectStore('orcamentos', { keyPath: 'id' });\n          store.createIndex('uuid', 'uuid', { unique: false });\n          store.createIndex('status', 'status', { unique: false });\n          store.createIndex('dataCriacao', 'dataCriacao', { unique: false });\n        }\n        if (!db.objectStoreNames.contains('perfil')) {\n          db.createObjectStore('perfil', { keyPath: 'id' });"
    },
    {
      "id": "teste-reconciliacao",
      "alegacao": "Há testes da reconciliação com a nuvem.",
      "repositorio": "ZapOrca",
      "arquivo": "tests/login-sync.test.ts",
      "linhas": [
        176,
        176
      ],
      "trecho": "it('deve adicionar orçamento novo vindo da nuvem que não existe localmente', () => {"
    },
    {
      "id": "teste-drenar",
      "alegacao": "Há teste do esvaziamento da fila.",
      "repositorio": "ZapOrca",
      "arquivo": "tests/login-sync.test.ts",
      "linhas": [
        253,
        253
      ],
      "trecho": "it('deve drenar pendências locais do Outbox marcando como synced', async () => {"
    },
    {
      "id": "teste-aviso-pagamento",
      "alegacao": "Há teste de um aviso de pagamento simulado.",
      "repositorio": "ZapOrca",
      "arquivo": "tests/outbox.test.ts",
      "linhas": [
        89,
        89
      ],
      "trecho": "it('deve enfileirar PIX_PAYMENT_CONFIRMED ao simular webhook de pagamento', () => {"
    },
    {
      "id": "rodape-pro",
      "alegacao": "O rodapé da proposta muda com o Pro.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pdf.ts",
      "linhas": [
        595,
        600
      ],
      "trecho": "<div class=\"doc-footer\">\n      ${isZapOrcaProActive() ? `\n        <span>Proposta Comercial Oficial • ZapOrca Pro (Documento Licenciado)</span>\n        <span>Emitido para ${perfilMei.nomeFantasia || perfilMei.nome} • Cópia Autêntica</span>\n      ` : `\n        <span>Proposta gerada eletronicamente • ZapOrca 2.0</span>"
    },
    {
      "id": "pdf-impressao",
      "alegacao": "A proposta é formatada em A4 para impressão ou salvamento em PDF pelo navegador.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pdf.ts",
      "linhas": [
        433,
        439
      ],
      "trecho": "<div style=\"font-size: 11px; color: #94a3b8;\">Formatado em padrão A4 para impressão ou salvamento em PDF.</div>\n      </div>\n    </div>\n    <div style=\"display: flex; align-items: center;\">\n      <button class=\"btn-close-action\" onclick=\"window.close()\">✕ Fechar Prévia</button>\n      <button class=\"btn-print-action\" onclick=\"window.print()\">\n        <span>🖨️ Salvar em PDF / Imprimir</span>"
    },
    {
      "id": "abre-impressao",
      "alegacao": "O botão abre a proposta numa janela e chama a impressão do navegador.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pdf.ts",
      "linhas": [
        609,
        628
      ],
      "trecho": "/**\n * Dispara o diálogo nativo de impressão do navegador com o layout otimizado.\n */\nexport function printProposal(orcamento: Orcamento): void {\n  const printWindow = window.open('', '_blank');\n  if (!printWindow) {\n    alert('Permita pop-ups para visualizar a impressão da proposta.');\n    return;\n  }\n\n  printWindow.document.open();\n  printWindow.document.write(generateProposalHTML(orcamento));\n  printWindow.document.close();\n\n  printWindow.onload = () => {\n    printWindow.focus();\n    setTimeout(() => {\n      printWindow.print();\n    }, 250);\n  };"
    },
    {
      "id": "baixa-html",
      "alegacao": "A proposta também pode ser baixada como arquivo HTML.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/pdf.ts",
      "linhas": [
        631,
        640
      ],
      "trecho": "/**\n * Cria e baixa um arquivo HTML standalone da proposta.\n */\nexport function downloadProposalHTML(orcamento: Orcamento): void {\n  const html = generateProposalHTML(orcamento);\n  const blob = new Blob([html], { type: 'text/html;charset=utf-8;' });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = `Proposta_${orcamento.numero}.html`;"
    },
    {
      "id": "logo-360",
      "alegacao": "O logotipo é reduzido para no máximo 360 por 180 pixels.",
      "repositorio": "ZapOrca",
      "arquivo": "src/app.ts",
      "linhas": [
        2258,
        2259
      ],
      "trecho": "const maxW = 360;\n            const maxH = 180;"
    },
    {
      "id": "chaves-armazenamento",
      "alegacao": "Perfil, orçamentos, fila, transações e tema ficam no armazenamento do navegador.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        13,
        19
      ],
      "trecho": "const STORAGE_KEYS = {\n  PERFIL: 'zaporca_perfil_mei',\n  ORCAMENTOS: 'zaporca_orcamentos',\n  OUTBOX: 'zaporca_outbox_queue',\n  TRANSACOES: 'zaporca_transacoes_pix',\n  THEME: 'brup-theme'\n};"
    },
    {
      "id": "exemplo-antigo",
      "alegacao": "Até 30/10/2026 o perfil de exemplo vinha com a chave Pix real do autor; quem já abriu o app tem a chave e o código apagados.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        250,
        252
      ],
      "trecho": "// Até 30/10/2026 o perfil de exemplo (id [id do perfil de exemplo]) vinha com a chave Pix real do autor, e quem\n// não editava o perfil gerava cobrança para ela. Quem já abriu o app guarda esse perfil e os orçamentos\n// feitos com ele; estas funções apagam a chave e o código Pix desses registros, sem mexer no resto."
    },
    {
      "id": "exemplos-tres",
      "alegacao": "Há três orçamentos de exemplo.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        196,
        196
      ],
      "trecho": "numero: 'ORC-2026-003',"
    },
    {
      "id": "exemplos-gravados",
      "alegacao": "Na primeira abertura, os orçamentos de exemplo são gravados na lista do aparelho.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        379,
        380
      ],
      "trecho": "if (!localStorage.getItem(STORAGE_KEYS.ORCAMENTOS)) {\n      localStorage.setItem(STORAGE_KEYS.ORCAMENTOS, JSON.stringify(this.memoryOrcamentos));"
    },
    {
      "id": "fila-sem-destino",
      "alegacao": "Sem um tratador de envio, esvaziar a fila marca cada item como enviado.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/storage.ts",
      "linhas": [
        656,
        663
      ],
      "trecho": "for (const item of pending) {\n      try {\n        let ok = false;\n        if (syncHandler) {\n          ok = await syncHandler(item);\n        } else {\n          ok = true;\n        }"
    },
    {
      "id": "reconciliacao",
      "alegacao": "O código de reconciliação entre nuvem e aparelho continua no repositório.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/sync.ts",
      "linhas": [
        4,
        11
      ],
      "trecho": "/**\n * Algoritmo puro e determinístico de reconciliação entre orçamentos da nuvem e locais:\n * - Regra 1: Entidades com mutação pendente no Outbox (UPSERT_ORCAMENTO) NÃO são sobrescritas pela nuvem (Local Unsynced Wins).\n * - Regra 2: Entidades com exclusão pendente no Outbox (DELETE_ORCAMENTO) NÃO são ressuscitadas pela nuvem.\n * - Regra 3: Entidades existentes na nuvem que não existem localmente são adicionadas.\n * - Regra 4: Entidades existentes em ambos sem pendência local são reconciliadas pela versão mais recente.\n */\nexport function mergeCloudAndLocalOrcamentos("
    },
    {
      "id": "modelo-memoria",
      "alegacao": "O teste de isolamento confere um modelo em memória do esquema do banco remoto.",
      "repositorio": "ZapOrca",
      "arquivo": "tests/rls-isolation.test.ts",
      "linhas": [
        3,
        5
      ],
      "trecho": "/**\n * Modelo representacional de registros do Supabase / PostgreSQL conforme supabase/schema.sql\n */"
    },
    {
      "id": "catalogo",
      "alegacao": "Catálogo de serviços por nicho, offline.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/catalogo-servicos-mei.ts",
      "linhas": [
        1,
        4
      ],
      "trecho": "/**\n * Catálogo Completo de Serviços e Mão de Obra por Nicho MEI.\n * Solução 100% offline para aceleração do orçamento no celular.\n */"
    },
    {
      "id": "clausulas",
      "alegacao": "Cláusulas prontas contra cancelamento, calote, retrabalho e reclamação fora do prazo.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/clausulas-contratuais.ts",
      "linhas": [
        1,
        5
      ],
      "trecho": "/**\n * Cláusulas Contratuais Prontas para Prestadores Autônomos e MEI.\n * Solução 100% offline para proteção jurídica contra cancelamentos,\n * calotes, retrabalho não pago e reclamações fora do prazo legal.\n */"
    },
    {
      "id": "validacao-propria",
      "alegacao": "Os esquemas expõem safeParse e parse próprios.",
      "repositorio": "ZapOrca",
      "arquivo": "src/contracts/schemas.ts",
      "linhas": [
        3,
        3
      ],
      "trecho": "* Os objetos *.Schema expõem safeParse/parse no formato { success, data | error.issues }."
    },
    {
      "id": "idiomas",
      "alegacao": "A interface tem três idiomas: português, inglês e espanhol.",
      "repositorio": "ZapOrca",
      "arquivo": "src/lib/i18n.ts",
      "linhas": [
        1,
        1
      ],
      "trecho": "export type Lang = 'pt' | 'en' | 'es';"
    }
  ]
}
