# Pedidos de interesse da PULSO

## Conteúdo e conversão

`site.json`, schema `1.1.0`, é a fonte única dos textos públicos, CTAs, nomes acessíveis, estados e regras. `brand.json` registra identidade, voz e escolhas fictícias. `copy.md` espelha todos os campos de `site.json`, com seus caminhos, e apresenta a estratégia de oferta. Não duplicar strings em componentes. O aviso `page.demoBanner` é persistente; `footer.disclosure` explica a natureza ilustrativa. As bios e legendas não recebem diagnósticos internos. O aviso de uso de dados de teste pertence ao formulário e à revisão do envio, sem banners adicionais em cada seção.

Há uma única conversão, `contact_lead`, com dois valores de `intent`: `visita` e `personal`. O CTA “Conhecer a academia” abre `#conversa` com `visita`; “Conversar sobre personal” abre o mesmo formulário com `personal`. Manter a âncora navegável e aplicar `action: open_interest` para a pré-seleção. Sem JavaScript, o formulário ou uma explicação acessível do registro deve permanecer encontrável na âncora, sem simular sucesso.

Os CTAs de catálogo também pré-selecionam `serviceId`; os perfis pré-selecionam `professionalId`. Ao mudar caminho, limpar escolhas incompatíveis; preservar nome, celular e escolhas ainda válidas. Mover o foco para o título do formulário após um CTA. Manter foco visível, associar cada erro ao campo, anunciar resultado em região de status e devolver o foco ao acionador ao fechar confirmações. Não usar a cor como único indicador de estado.

Interesse não é matrícula, reserva, aula, turma ou visita confirmada. Não existe calendário, duração, capacidade ou endpoint de disponibilidade. Os horários de funcionamento ilustrativos não autorizam gerar horários agendáveis. As contagens são de pedidos de interesse, sem receita, ocupação, vagas ou planos vendidos. Os planos têm `priceMode: on_request`, `amount: null` e não incluem checkout, pagamento ou oferta gratuita.

## Campos e elegibilidade

Campos do pedido: `intent`, `serviceId`, `professionalId`, `preferredShift`, `name`, `phone`, `demoAcknowledged`. Aplicar os limites e as condições declarados em `interest.fields`:

- `intent` é obrigatório: `visita` ou `personal`.
- `name` é obrigatório, com trim e 2–80 caracteres Unicode após trim. Medir pontos de código tanto no cliente quanto no servidor; a restrição HTML pode ser mais estrita para caracteres fora do BMP, mas não deve permitir exceder o limite do contrato.
- `phone` é obrigatório: entrada de até 20 caracteres, normalização por remoção de caracteres não numéricos, resultado de 10 ou 11 dígitos com DDD. O limite bruto é validado antes da normalização. Não acrescentar `55` nem inventar validade ou titularidade de um número. Não fazer contato.
- `demoAcknowledged` deve ser booleano `true`.
- `serviceId` é opcional, com `null` para nenhuma preferência. `musculacao`, `funcional` e `mobilidade` pertencem a `visita`; `personal-individual` e `personal-dupla` pertencem a `personal`.
- `professionalId` é opcional somente em `personal`. Lia e Caio são elegíveis para ambos os formatos de personal. `null` significa sem preferência, sem atribuir profissional automaticamente. Em `visita`, o campo deve estar oculto e o payload deve conter `null`; valor não nulo é rejeitado pelo servidor.
- `preferredShift` é opcional: `manha`, `tarde`, `noite` ou `null`. Nunca converter preferência em horário reservado.

Todos os identificadores e enums vêm do catálogo canônico. Validar no servidor com uma cópia controlada do contrato; o catálogo enviado pelo cliente não é autoridade. Normalizar seleções vazias para `null` antes de gerar o payload. Rejeitar propriedades desconhecidas e tipos incorretos, sem coerção de booleanos ou enums. Não coletar sintomas, peso, medidas, BMI, CPF, e-mail, notas de saúde, texto livre ou arquivos. Os únicos dados de contato são nome e celular de teste. `brand.contact` permanece nulo, sem links comerciais, Maps ou WhatsApp.

## Registro local

O padrão é `demo_local`. Usar `pulsoDemo:demo-leads:v1` para registros e `pulsoDemo:demo-leads:v1:pending` para tentativas remotas pendentes. Não usar namespaces de outros portfólios. Um pedido recebe `recordId` aleatório, `requestId` aleatório, `createdAt` em ISO-8601 e status `received`; cancelamento produz `cancelled` e `cancelledAt`. Estados válidos são apenas `received` e `cancelled`.

Exemplo de registro local:

```json
{
  "schemaVersion": "1.1.0",
  "brandId": "pulso-academia-personal-demo",
  "recordId": "uuid-gerado",
  "requestId": "uuid-da-tentativa",
  "status": "received",
  "createdAt": "timestamp-ISO-gerado",
  "cancelledAt": null,
  "mode": "demo_local",
  "intent": "visita",
  "serviceId": "musculacao",
  "professionalId": null,
  "preferredShift": null,
  "name": "Pessoa de teste",
  "phone": "00000000000",
  "demoAcknowledged": true
}
```

Os marcadores de UUID/timestamp do exemplo são explicativos, não valores aceitos. Gerar IDs com `crypto.randomUUID()`. Gravar antes de confirmar sucesso. Falha de armazenamento mantém o formulário e apresenta `storageError`. Schema inválido ou JSON local corrompido não pode ser sobrescrito silenciosamente: apresentar erro e permitir exclusão local confirmada, desde que não exista envio pendente. Deduplicar por `brandId + requestId`, comparar o payload normalizado e devolver o registro original em retry. Não deduplicar pessoas por telefone: novos pedidos deliberados não são retries.

Coordenar mutações entre abas com Web Locks e releitura dentro do lock, anunciando mudanças pelo evento `storage`. Onde Web Locks não estiver disponível, tratar a persistência local como limitada e não prometer atomicidade entre abas; manter dedupe por nonce e releitura antes de gravar. Não há capacidade ou recurso escasso para bloquear. Uma segunda aba que detectar envio remoto pendente deve respeitar o bloqueio global desse namespace.

Em `/demo`, usar filtros de caminho e personal em conjunto. Visita não possui personal; filtro de pessoa selecionada retorna apenas pedidos `personal` dessa pessoa. “Sem preferência de personal” filtra `personal` com `professionalId: null`, não visitas. Ao filtrar só visitas, ocultar e limpar o filtro de pessoa. As três métricas contam todos os registros locais, independentemente dos filtros: visitas `received`, personal `received` e todos os `cancelled`. Indicar vazio com `operations.empty` e ausência global com `operations.noRecords`.

Cancelamento exige confirmação; repetir cancelamento mantém o mesmo registro cancelado. Reset exige confirmação e apaga somente dados locais. Uma tentativa remota sem resultado definitivo bloqueia reset, novo envio e fallback. Não apagar o único nonce que permite reconciliar um envio. `/demo` é a visão local deste navegador, não um painel público de clientes ou uma prova de operação real.

## GAS opcional e autorização

`integration.gasEndpoint` é nulo. `PUBLIC_DEMO_GAS_ENDPOINT` pode apontar para uma implantação demonstrativa autorizada; sua existência não comprova funcionamento. Não implementar GAS ou presumir que um endpoint está ativo apenas com base neste contrato. Mostrar o destino efetivo e `remoteNotice` antes do envio. O formulário tem escolha de modo explícita; jamais mudar de remoto para local silenciosamente.

O GET público permitido é `health`, que devolve apenas `{ok, schemaVersion, brandId}`. Não há GET público de disponibilidade, listagem de pedidos, contagens operacionais ou dados pessoais. Registro, consulta de tentativa, cancelamento, listagem e exclusão são privados. Exigir token de autorização validado pelo servidor para cada operação; mantê-lo somente em RAM no cliente, nunca em localStorage, sessionStorage, URL, variável `PUBLIC_*`, export ou logs. Guardar o segredo no servidor, por exemplo em Script Properties. Ao recarregar, solicitar novamente o token antes de reconciliar o pedido persistido. Não realizar envio sem token, nem gravar PII em logs de diagnóstico.

O endpoint deve produzir resposta legível no navegador ou ser consumido por um intermediário autenticado compatível com a política de origem. `no-cors`, resposta opaca, redirecionamento ilegível ou HTTP 200 isolado não confirmam registro. Restringir origens e taxa, validar tamanho da requisição e tratar todos os campos no servidor. Origem não substitui autorização.

## Envelope, nonce e deduplicação

Envelope privado para criação:

```json
{
  "op": "register_lead",
  "authToken": "somente-em-memoria",
  "payload": {
    "schemaVersion": "1.1.0",
    "brandId": "pulso-academia-personal-demo",
    "requestId": "uuid-da-tentativa",
    "kind": "lead",
    "intent": "personal",
    "serviceId": "personal-individual",
    "professionalId": null,
    "preferredShift": "noite",
    "name": "Pessoa de teste",
    "phone": "00000000000",
    "demoAcknowledged": true
  }
}
```

Whitelist do envelope: `op`, `authToken`, `payload`. Whitelist do payload: `schemaVersion`, `brandId`, `requestId`, `kind`, `intent`, `serviceId`, `professionalId`, `preferredShift`, `name`, `phone`, `demoAcknowledged`. Validar marca, versão, UUID, `kind: lead`, limites, catálogo e enums. Derivar IDs, status e timestamps no servidor. O cliente não pode enviar status, preço, propriedade do registro ou informações operacionais.

`requestId` é o nonce estável da tentativa. Gerar uma vez, persistir payload normalizado e nonce antes do primeiro envio e impedir sua edição durante a pendência. Se não for possível preservar a tentativa, não iniciar a chamada. O fingerprint é SHA-256 de JSON canônico dos campos whitelisted normalizados, em ordem fixa, excluindo token, metadados da resposta e timestamp do servidor. Não usar fingerprint de telefone como identidade de cliente.

No servidor, usar `LockService.getScriptLock()` com tempo de espera limitado. Dentro de um único trecho protegido: consultar o ledger por marca+nonce, comparar fingerprint, criar ou recuperar o resultado e persistir de forma recuperável. Registrar intenção durável no ledger antes de efeitos externos; se houver falha entre escrita do pedido e escrita do resultado, recuperar pelo mesmo nonce sem inserir nova linha. Sheets não oferece transação: não supor que duas gravações independentes são atômicas. Liberação do lock em `finally`; falha ao obter lock não é sucesso.

Mesmo nonce e mesmo fingerprint devolvem o resultado original. Mesmo nonce com payload diferente retorna `request_mismatch`, sem nova escrita. Manter o ledger pelo menos enquanto o pedido e qualquer retry forem válidos, com retenção documentada na implantação; não reutilizar nonce depois de exclusão. Nenhuma operação envia WhatsApp, SMS, e-mail ou cobrança.

Resposta de criação aceita:

```json
{"ok":true,"requestId":"uuid-da-tentativa","status":"received","recordId":"uuid-do-servidor","createdAt":"timestamp-ISO-gerado","cancelToken":"capacidade-imprevisivel"}
```

Resposta de erro definitivo contém `{ok:false, requestId, code, definitive:true, accepted:false}`. Códigos incluem `invalid_payload`, `unauthorized`, `invalid_selection` e `request_mismatch`; um mismatch exige reconciliação do pedido original, não autoriza descarte do nonce ou fallback automático. `busy`/falhas sem prova definitiva mantêm pendência. Validar o schema e a correspondência de nonce na resposta antes de anunciar sucesso.

## Resultado remoto desconhecido

Timeout, erro de rede ou resposta ilegível pode ocorrer depois da gravação. Manter a tentativa imutável e exibir `remoteUnknown`. Bloquear outro nonce, alteração do payload, exclusão da pendência e fallback local. A consulta privada `request_status` usa marca+nonce e autorização, nunca busca por nome ou celular. Pode devolver `pending`, `received`, `cancelled` ou `rejected` como estados da tentativa; apenas `received`/`cancelled` são estados de um pedido.

Um simples “não encontrado” não prova que não houve aceitação: a requisição original pode ainda estar em trânsito. Para declarar `rejected`/`definitive:true, accepted:false`, o servidor deve gravar, sob o mesmo lock, um tombstone final para o nonce, impedindo qualquer criação tardia. Uma chegada posterior desse nonce retorna a rejeição gravada. Não usar expiração de timeout do cliente como prova de não aceitação.

Resolver a pendência por consulta privada ou retry com o mesmo nonce e payload. Se recebido, devolver o registro original e sua capacidade de cancelamento. Se rejeitado definitivamente, oferecer a escolha explícita de modo local; iniciar uma nova tentativa local vinculada por `resolvedRemoteRequestId`, sem alterar o resultado remoto. Autorização expirada exige novo token em RAM, mas preserva nonce e payload. A indicação de pendência também deve sobreviver à recarga.

## Cancelamento, listagem e exclusão privados

`cancel_record` exige autorização e capacidade imprevisível por registro, vinculada à marca e ao `recordId`, não só um ID. Gerar a capacidade com fonte criptográfica segura no servidor, armazenar apenas seu hash para validação e devolver o segredo apenas ao cliente autorizado. Guardá-lo na memória; após recarga, recuperá-lo por consulta autorizada do nonce, sem colocá-lo em export público. Não usar UUID previsível como capacidade. Cancelamento tem `operationId` próprio e idempotente, com resposta `status: cancelled` e `cancelledAt`. Repetição retorna o mesmo resultado. Resultado de cancelamento desconhecido mantém o estado anterior na UI e bloqueia ações conflitantes até consulta privada; não anunciar cancelamento por timeout.

`list_records` e `delete_record` exigem autorização administrativa apropriada, distinta de capacidade de cancelamento. GET nunca lista PII. A página `/demo` não executa listagem remota automática. Exclusão remota não é reset local e não deve remover o ledger de dedupe de modo que um retry antigo recrie o pedido. A implantação precisa definir retenção dos dados de teste, expiração de capacidades, política de exclusão e proteção de acesso antes de habilitar envio remoto.

## Recursos e limites de publicação

`resources.links` define rótulos e destinos locais, todos desabilitados por padrão. Exibir somente arquivo existente no build com `enabled: true`. O recurso GAS é o contrato de integração, não uma alegação de endpoint implementado. Não mostrar download inexistente ou inventar URL de repositório.

Imagens e retratos são ilustrativos; seus textos alternativos descrevem a composição e devem ser ajustados na mesma fonte se a mídia entregue divergir. Não apresentam prescrição de exercícios ou prova de resultados. Contatos, credenciais/CREF e Maps são nulos. Endereço e horários são cenário fictício, sem encaminhar visitante para estabelecimento real. Nenhuma configuração técnica transforma estes pedidos de teste em atendimento real.
