# Solicitações de proposta da ÂMBAR Solar

## Conteúdo, oferta e natureza demonstrativa

`site.json`, schema `1.1.0`, é a fonte única de textos públicos, CTAs, campos, estados e regras. `brand.json` define marca, voz e escolhas fictícias. `copy.md` reproduz integralmente ambos os JSONs, além da apresentação editorial da oferta. Todos os arquivos usam UTF-8. Não duplicar microcopy em componentes. Usar `page.demoBanner` como badge persistente em todas as rotas, inclusive `/demo`, sem cobrir campos ou controles.

ÂMBAR Solar é uma marca fictícia. Residencial, comercial, condomínio e rural são aplicações ilustrativas; os cenários do portfólio não são clientes, obras, instalações, depoimentos ou resultados. Endereço e horários são narrativos. Não há WhatsApp, telefone comercial, e-mail, Maps, pagamento, gratuidade prometida ou credencial técnica. A oferta é organizar informações antes de investir; valores dependem de análise, sem preços numéricos, economia, payback, financiamento, garantias ou dimensionamento automático.

A conversão é `solar_quote`: uma solicitação de proposta de teste. Não é agendamento, contrato, orçamento emitido, pedido de conexão à distribuidora ou atendimento real. O caminho solicitação → análise → proposta descreve um processo de serviço; a demo executa somente a primeira parte, sem alegar análise posterior. Projeto, instalação, homologação e acompanhamento são escopos ilustrativos a definir numa contratação real.

## CTA e composição

Os CTAs de conversão usam `href: #proposta` e `action: open_quote`. CTAs de serviços e cenários conceituais preenchem `propertyType` com o contexto correspondente. CTAs genéricos não apagam escolhas existentes; preservam campos compatíveis. Não há `serviceId`, profissional, agenda ou seleção de slot. Nenhuma escolha de telhado ou solo comprova viabilidade.

As âncoras públicas são `inicio`, `solucoes`, `processo`, `escopo`, `portfolio`, `duvidas`, `proposta`, `estudio`, `primeiro-passo` e `privacidade`. A gestão local é `/demo`. Sem JavaScript, manter navegação, conteúdo, FAQ acessível e aviso `ui.noJavascript` no formulário, sem sucesso simulado. Após CTA, levar foco ao título da solicitação; não bloquear a âncora. Associar erros a campos, anunciar estados em região viva, preservar foco visível e devolver o foco ao acionador após diálogo. Confirmar cancelamento, exclusão individual e reset, sem depender apenas de cor.

As quatro necessidades de mídia são Casa Luz, Ateliê Horizonte, Pátio Comum e Campo Aberto, além do hero. As legendas de natureza conceitual acompanham as imagens. O conteúdo não fixa paleta ou DNA; vocabulário de luz, arquitetura, superfície e clareza serve como contexto editorial.

## Campos, IDs, tipos e limites

O payload canônico tem ordem de campos definida em `integration.contract.payloadFields`; `interest.payloadSchema` é JSON Schema Draft 2020-12 para o payload normalizado. Todos os campos listados existem no objeto; os opcionais usam `null`. Não permitir propriedades desconhecidas ou coerção de booleanos, números e enums.

- `schemaVersion`: string constante `1.1.0`.
- `brandId`: string constante `ambar-solar-demo`.
- `requestId`: UUID aleatório gerado uma vez por tentativa, por `crypto.randomUUID()`.
- `kind`: string constante `solar_quote`.
- `propertyType`: obrigatório, `residencial|comercial|condominio|rural|nao_sei`. Não atribuir serviço automaticamente ao valor `nao_sei`.
- `city`: obrigatório, string com trim e normalização Unicode NFC, 2–80 pontos de código. Texto de contexto limitado, não campo aberto de observações. Usar cidade de teste, sem endereço exato.
- `state`: obrigatório, uma das 27 siglas de UF presentes em `interest.fields`. Não há inferência de localização, IP, geocodificação ou validação de cobertura comercial.
- `consumptionBand`: obrigatório, `ate_200|201_500|501_1000|1001_2500|acima_2500|nao_sei`. A unidade é kWh/mês. Limites contínuos são até 200; acima de 200 até 500; acima de 500 até 1.000; acima de 1.000 até 2.500; acima de 2.500. Os IDs legados do enum são rótulos, não números a converter em cálculo. A faixa é entrada, nunca estimativa de geração, economia ou preço.
- `installationArea`: opcional, `telhado|solo|outro|nao_sei|null`; as opções são válidas para todo tipo de imóvel. `null` indica sem preferência; `nao_sei` registra uma dúvida explícita.
- `preferredShift`: opcional, `manha|tarde|sem_preferencia|null`. Não é horário reservado nem promessa de contato.
- `name`: obrigatório, string com trim e NFC, 2–80 pontos de código; somente nome de teste.
- `phone`: obrigatório, entrada bruta com no máximo 20 pontos de código; validar o limite antes de remover caracteres não numéricos. Payload normalizado com 10 ou 11 dígitos ASCII e DDD, sem acrescentar `55`. Não validar titularidade ou fazer contato; aceitar o número fictício de teste `00000000000`.
- `demoAcknowledged`: booleano obrigatório `true`, sem pré-marcação; não aceitar string `"true"`.

As únicas dependências são CTA → tipo de imóvel e modos → destino/aceites. Não condicionar upload, CPF, endereço ou campos sensíveis a nenhuma seleção. Não coletar e-mail, observações livres, documentos, conta de luz, coordenadas ou arquivos. City e name normalizados precisam estar sem espaços nas extremidades; o Schema valida comprimento/tipo, e a validação de aplicação verifica a normalização. Autoridade dos enums é a cópia controlada de `site.json` no servidor; o cliente não pode substituir o catálogo.

## Persistência local e registros

O padrão é `demo_local`, com `PUBLIC_DEMO_GAS_ENDPOINT` vazio. Usar exclusivamente `ambarSolarDemo:solar-quotes:v1` para registros e `ambarSolarDemo:solar-quotes:v1:pending` para tentativas pendentes. Não reutilizar namespaces de outro portfólio.

Um registro contém todo o payload normalizado e estes metadados gerados pelo storage: `recordId` UUID, `createdAt` ISO-8601 UTC, `status: received|cancelled`, `cancelledAt` nulo enquanto recebido e timestamp quando cancelado, `mode: demo_local|demo_remote`. Não aceitar metadados enviados como parte do payload de criação. Capabilities, tokens e segredos não entram no registro persistido.

No modo local, validar antes da escrita; escrever, reler e validar o registro persistido antes de mostrar sucesso. Falha mantém campos e exibe `storageError`, sem confirmação falsa. JSON corrompido ou schema inválido não é sobrescrito silenciosamente: exibir `corruptStorage`, conservar dados e oferecer exclusão confirmada apenas sem pendência. Retry com mesma marca+nonce e mesmo payload devolve o registro original; payload diferente com mesmo nonce é conflito. Não deduplicar por telefone ou por cidade.

Coordenar mutações entre abas com Web Locks e releitura dentro do lock; refletir alterações com evento `storage`. Sem Web Locks, não prometer atomicidade entre abas. Revalidar e deduplicar por nonce antes de cada gravação. Uma pendência válida ou armazenamento pendente ilegível bloqueia novos envios e reset em todas as abas desse namespace.

`/demo` é gestão de solicitações locais, sem autenticação alegada ou uso com dados reais em dispositivo compartilhado. Métricas de estado contam `received` e `cancelled`; contagens por tipo contam apenas `received`. Todas as contagens consideram o conjunto completo válido, independentemente dos filtros. Filtros de tipo, estado, cidade e UF combinam com AND. Não gerar cidades a partir de fontes externas. Diferenciar lista global vazia e filtro sem correspondência. Não exibir receita, ocupação, potência, retorno, economia, instalações ou clientes reais.

Cancelar requer confirmação e muda somente `received` para `cancelled`, com timestamp; repetir mantém o mesmo resultado. Excluir um registro ou reset requer confirmação e apaga apenas a cópia local. No modo remoto, confirmação local de cancelamento depende de resposta definitiva do servidor; excluir local nunca exclui remoto. Não executar qualquer exclusão/reset enquanto houver tentativa de criação ou cancelamento pendente.

## GAS opcional, autorização e transporte

Uma configuração de endpoint não é prova de serviço ativo. O usuário escolhe explicitamente o modo remoto e aceita `remoteOptIn` após ver o destino efetivo. Sem endpoint autorizado, o modo remoto fica indisponível, com registro local acessível. Nunca enviar ao remoto automaticamente ou migrar de modo silenciosamente. O aceite de demo permanece obrigatório em ambos os modos.

O contrato usa `doGet` e `doPost` de Google Apps Script com Sheets. `doGet?op=health` é o único GET público: responde somente `{ok, schemaVersion, brandId}`. GET de listagem/contagens/PII é privado, exige autorização administrativa e deve passar por um intermediário autenticado que forneça o segredo fora da URL. No GAS direto, operações administrativas de PII devem usar POST autenticado; nunca colocar tokens em query string. Nenhum GET público expõe pedidos, nomes ou telefones. Não há disponibilidade de agenda.

Enviar JSON como corpo de `POST` com `Content-Type: text/plain;charset=UTF-8`; validar o corpo e o envelope no servidor. A resposta precisa ser legível no navegador. `no-cors`, resposta opaca, HTML, timeout, redirecionamento ilegível ou HTTP 200 isolado deixam o resultado desconhecido. O endpoint autorizado ou intermediário deve suportar o transporte real do navegador; não afirmar CORS configurável nativo do GAS. Origin, Content-Type e CORS não substituem autenticação. Aplicar limites de tamanho e taxa no servidor.

Tokens de usuário, admin e cancel capability ficam somente em RAM no navegador, nunca em localStorage, sessionStorage, URL, `PUBLIC_*`, export ou logs. Segredos do servidor usam Script Properties ou armazenamento privado equivalente. Endpoint pode ser público; credencial não. Ao recarregar, pedir token de novo antes de reconciliar. Não iniciar chamada sem autorização e opt-in. Remover token da memória não apaga o payload pendente. Não registrar PII em logs.

## Envelope e respostas

Criação privada:

```json
{
  "op": "register_quote",
  "authToken": "token-somente-em-RAM",
  "payload": {
    "schemaVersion": "1.1.0",
    "brandId": "ambar-solar-demo",
    "requestId": "123e4567-e89b-42d3-a456-426614174000",
    "kind": "solar_quote",
    "propertyType": "residencial",
    "city": "Cidade de teste",
    "state": "SP",
    "consumptionBand": "nao_sei",
    "installationArea": null,
    "preferredShift": null,
    "name": "Pessoa de teste",
    "phone": "00000000000",
    "demoAcknowledged": true
  }
}
```

Whitelist do envelope: `op`, `authToken`, `payload`. Registro usa exatamente `interest.payloadSchema`. Consulta `request_status` usa payload `{brandId, requestId, fingerprint}`; cancelamento `cancel_record` usa `{brandId, requestId, recordId, operationId, cancelCapability}`. `operationId` é UUID estável daquela tentativa de cancelamento. Rejeitar campos extras e tipos incorretos em todas as operações. `list_records` usa `{brandId}` e exige admin; `delete_record` usa `{brandId, recordId, operationId}` e exige admin. Nenhum segredo precisa aparecer no GET ou em uma lista pública.

Toda resposta privada inclui `schemaVersion`, `brandId`, `op`, `requestId` quando aplicável, `ok` booleano, `outcome` e `fingerprint` quando vinculada à tentativa. `outcome` pertence a `pending|received|cancelled|rejected`. Recebimento confirmado exige `ok:true`, marca/versão/nonce/fingerprint correspondentes e `record` válido, persistido, com `status:received|cancelled`. Retry de um pedido cancelado retorna o original cancelado, sem ressuscitá-lo. Nova aceitação fornece `cancelCapability` somente pela resposta privada; não persistir essa capability. Uma sessão autorizada pode recuperá-la privadamente em `request_status`, após validar marca/nonce/fingerprint e autorização do registro.

Rejeição definitiva exige `ok:false`, `outcome:rejected`, `definitive:true`, `accepted:false`, `code` de enum `VALIDATION|UNAUTHORIZED|NONCE_CONFLICT|REJECTED_FINAL` e `tombstoned:true` para aquela marca+nonce+fingerprint. Não é sucesso. Falta de autorização, conflito de payload ou mero “não encontrado” sem tombstone não prova não aceitação da tentativa original; continuar bloqueado até reconciliação autorizada. Rejeição só autoriza fallback quando o servidor prova que a mesma tentativa nunca será aceita e não possui registro correspondente.

## Nonce, fingerprint, lock e ledger durável

Antes do primeiro POST, normalizar e validar o payload; gerar nonce; calcular SHA-256 do JSON dos campos em `integration.contract.payloadFields`, nessa ordem, com `null` explícito. Excluir token, opt-in de interface e metadados. Persistir e reler `{schemaVersion,brandId,op,requestId,fingerprint,payload,mode,endpoint,state,createdAt}` no namespace pending. `state` é `pending`; `mode` é `demo_remote`. Só então iniciar rede. Não editar esse payload, trocar endpoint ou gerar novo nonce enquanto pendente.

No GAS usar `LockService.getScriptLock()` com espera limitada, soltando em `finally`. Sob o mesmo lock, consultar ledger por marca+nonce, comparar fingerprint e persistir intenção durável antes de efeitos. O ledger contém marca, nonce, fingerprint, estado e ponteiro para registro/resultado; não é somente cache em RAM. Sheets não oferece transação: manter sequência recuperável. Se cair após inserir o registro mas antes de finalizar ledger, buscar pela chave e reparar o ledger, sem segunda linha. Nunca retornar received antes de persistir registro e resultado consistente.

Mesmo nonce e fingerprint devolvem o resultado original; mesmo nonce com fingerprint diferente é conflito, sem mutação. Rejeição definitiva grava tombstone durável sob lock antes da resposta e bloqueia aceitação tardia do nonce. Cancelamento tem ledger de `operationId`, capability vinculada ao registro e transição idempotente; criação tardia não ressuscita registro cancelado. Exclusão remota mantém tombstones/ledger suficientes para impedir recriação por replay. O prazo de retenção deve ser definido pelo operador; não expirar proteção enquanto retries puderem ocorrer.

## Resultado desconhecido, retry e fallback

Timeout, resposta inválida ou desconhecida preservam pending, inclusive após reload. Bloquear novo envio, edição, exclusão, reset, mudança de modo e fallback. Reconciliar por `request_status` autenticado, ou repetir a criação com o mesmo payload/nonce/fingerprint. “Não encontrado” isolado mantém a pendência, pois uma chamada anterior ainda pode concluir. Não apagar a única chave de reconciliação.

Sucesso local somente após persistência validada. Sucesso remoto somente após resposta recebida e registro servidor validado. Se o servidor confirmou e a cópia local falhou, usar `remoteAcceptedLocalCacheError`, preservar pending e o código para reconstruir a cópia pela consulta; não reenviar como pedido novo. Estado received/cancelled definitivo pode concluir a tentativa após atualização local consistente. Falha de atualização mantém a recuperação pendente.

Uma rejeição definitiva válida não mostra sucesso. Conservar dados para correção e, somente com tombstone e não aceitação comprovada da tentativa original, oferecer a escolha explícita de registro local. Registrar a decisão e gerar novo nonce para esse novo registro; jamais converter o envio original em received. Dados corrigidos exigem uma nova tentativa somente após resolver a anterior. Antes de cancelar remotamente, persistir e reler a pendência com `op:cancel_record`, `operationId`, `recordId`, `requestId`, marca, endpoint e fingerprint do registro, sem capability ou token. Em reload, recuperar a capability privadamente com autorização e continuar com o mesmo `operationId`. Se a pendência não puder ser preservada, não iniciar o cancelamento. Cancelamento desconhecido mantém operação pendente e bloqueios; repetir com o mesmo `operationId`, sem mudar o registro local até confirmação. PII remota é consultada e excluída apenas por acesso privado autorizado.

## Downloads e recursos

`resources.links` define source ZIP, `Code.gs`, guia e contrato de interoperabilidade. A regra é `enabled && arquivoExisteNoBuild`. Com `enabled:false` ou arquivo ausente, não renderizar link quebrado, botão ativo ou promessa de download. ZIP/export não incluem credenciais, tokens ou dados armazenados de teste. O guia e o contrato usam a mesma fonte de campos e enums; Code.gs precisa de configuração privada e catálogo autoritativo. Os recursos não certificam integração ativa.

## Referências e inferências editoriais

- [ANEEL — Micro e Minigeração Distribuída](https://www.gov.br/aneel/pt-br/assuntos/geracao-distribuida): fundamenta a necessidade de analisar variáveis do projeto, seguir procedimentos de conexão e não presumir fatura zerada. **Inferência editorial:** qualificar o contexto sem calcular resultado e explicar que a solicitação de teste não inicia homologação. Esta referência não autoriza aconselhamento técnico individual nem promessa financeira.
- [ANEEL — Manuais de micro e minigeração distribuída](https://www.gov.br/aneel/pt-br/centrais-de-conteudos/manuais-modelos-e-instrucoes/micro-e-minigeracao-distribuida): fonte oficial para formulários e procedimentos. **Inferência de escopo:** separar solicitação comercial inicial dos documentos formais de conexão; nenhum documento é coletado nesta demo.
- [Ampla Energia Solar](https://amplasolar.com.br/): a própria empresa apresenta serviços em etapas, de estudo e projeto a instalação e manutenção. **Inferência editorial:** organizar o escopo e o processo antes do CTA. Não transferir clientes, depoimentos, credenciais, resultados, fotografias ou condições comerciais à ÂMBAR.
- [Portal Solar](https://www.portalsolar.com.br/): referência comercial/editorial do setor com conteúdos de orientação, aplicações e ferramentas. **Inferência editorial:** oferecer dúvidas e contexto de uso; a ferramenta de cálculo do site de referência não é parte da oferta ÂMBAR. Não copiar claims, textos, identidade ou imagens.

As referências sustentam limites e estrutura editorial, não comprovam operação, vínculo ou direitos sobre materiais de terceiros. As decisões fictícias ficam explicitadas em `brand.assumptions`; o contrato não declara GAS, credenciais ou contato comercial reais.
