# MORA Imóveis — contrato de solicitação demonstrativa

## Fontes e finalidade

`brand.json` define identidade, voz, limites e decisões fictícias. `site.json` schemaVersion 1.1.0 é a fonte canônica de textos, catálogo, CTAs, estados e regras. `copy.md` reproduz os dois contratos integralmente. A versão preserva o formato local da arquitetura; não implica compatibilidade com qualquer sistema externo. Payload/registro usam JSON Schema Draft 2020-12.

MORA Imóveis é fictícia. Cidade Modelo e bairros são narrativos. Quatro imóveis: Apartamento Alameda (82 m², 2 quartos, 1 vaga, compra R$ 680.000); Casa Pátio (148 m², 3 quartos, 2 vagas, compra R$ 1.190.000); Studio Jardim (36 m², 1 quarto, 0 vaga, aluguel R$ 2.250/mês); Apartamento Varanda (96 m², 3 quartos, 1 vaga, aluguel R$ 3.900/mês). Todos os números são ilustrativos. Condomínio, IPTU, taxas e custo total mensal são null, não zero. Sem contato externo, endereço exato, CRECI, corretor real, valorização ou negócio efetuado. Horário segunda a sexta, 9h–18h, é ilustrativo e não promete retorno.

Conversão `property_visit_request`: pedido de CONTATO de teste vinculado a imóvel, com preferência de turno opcional. Não agenda visita, reserva imóvel ou horário, promete disponibilidade, recebe anúncio de proprietário ou garante resposta.

## Página, catálogo e detalhes

Âncoras: inicio, imoveis, processo, informacoes, empresa, duvidas, visita, primeiro-passo, privacidade. Home contém navegação, hero, catálogo, processo, informações, marca, FAQ, solicitação, CTA final e rodapé. Painel `/demo` e quatro detalhes estáticos: `/imoveis/apartamento-alameda/`, `/imoveis/casa-patio/`, `/imoveis/studio-jardim/`, `/imoveis/apartamento-varanda/`. Imóveis são `properties`; referências de shape e chaves estão em site.json, não services genéricos.

Busca por nome/bairro/código, filtros AND de finalidade/tipo/quartos (quantidade exata), contagem e reset. Ordenação por destaque ou área compara todos; preço só fica habilitado dentro de compra OU aluguel. Voltar a finalidade all restaura featured se o sort era preço. Empates por id. Favoritos podem compor filtro AND. Sem resultado não é erro e permite limpar filtros. Zero vagas significa Sem vaga; encargos null mostram Não definido, nunca gratuito.

Favoritos em `moraDemo:property-favorites:v1`: array de IDs exatos, únicos, no máximo quatro. Validar estrutura e catálogo; estado corrompido é preservado e só pode ser limpo por confirmação específica. Falha de quota/storage informa erro sem confirmar a alteração e sem bloquear navegação, catálogo ou formulário. Eventos storage revalidam entre abas. Favoritar não cria lead ou reserva. Não ler namespaces dos demais portfólios.

CTA genérico `#visita` mantém escolha; CTA de detalhe `/?property=<ID>#visita` define uma referência exata dentre alameda/patio/jardim/varanda, aceitando apenas uma ocorrência. Validar, consumir uma vez e remover property com history.replaceState mantendo caminho, hash e outras queries não sensíveis. Não colocar PII na URL. Duplicado/inválido é removido e anunciado; não atribuir imóvel por default. Pending válido sempre vence a URL, que não pode alterar snapshot; pending ilegível bloqueia seleção. Foco request_heading é distinto da âncora visita. CTA não envia dados nem muda modo. Badge persistente em todas as rotas, aviso no catálogo/detalhe/formulário e contexto fictício na revisão e resultado. Texto deve vir dos JSONs.

Quatro fotos próprias, uma por imóvel. Casa Pátio usa a MESMA foto para hero/card/detalhe; demais usam card/detalhe. Sem quinto imóvel, galeria inventada, pessoas necessárias, vídeo ou áudio. Alts são propostas a conferir contra as imagens finais. Recortes não indicam outros cômodos. PNG original fica privado, WebP responsivo é público. Mídia de referências não pode ser reutilizada.

## Guard síncrono compartilhado

Antes de qualquer `await`, SHA-256, Web Lock ou rede, verificar e adquirir um guard síncrono compartilhado por formulário, cartões, gestão e controles remotos. Um `setState` de loading não protege handlers no mesmo tick. CTA genérico ou de imóvel, select, edição, revisão, voltar, novo teste, fallback, cancelamento, exclusão, reset e mudança de modo/destino consultam o mesmo guard e pending. Durante guard/pending, nenhuma mutação ou edição é permitida; somente leitura/navegação e reconciliação controlada da tentativa original.

Capturar snapshot normalizado imutável dentro do guard. Ao esperar lock, anunciar `waitingLock`. No lock `moraDemo:property-visit-requests:v1`, reler registros, ledger e pending antes de mutar. Em `finally`, liberar guard temporário apenas após término verificável ou instalação do bloqueio durável de pending. Este bloqueio sobrevive à liberação, unmount e reload. Reconciliação pode adquirir o guard apesar de pending exclusivamente para resolver a mesma operação; não contornar bloqueios para outra ação.

Eventos `storage` atualizam lista, métricas e bloqueio entre abas. Sem Web Locks, anunciar `noWebLocks`, reler e deduplicar antes da escrita e recomendar uma aba, sem alegar atomicidade equivalente. Guard da instância não substitui lock, ledger e releitura. Invalidade síncrona sem efeito pode liberar guard e permitir correção.

## Campos e normalização

O payload contém exatamente os nove campos obrigatórios de `integration.contract.payloadFields`: `schemaVersion,brandId,requestId,kind,propertyId,turn,name,phone,demoAcknowledged`. Constantes: versão `1.1.0`, marca `mora-imoveis-demo`, kind `property_visit_request`. `propertyId` é exatamente `alameda|patio|jardim|varanda`; `turn` é `manha|tarde|null`. O turno é opcional na interface, mas sempre presente no objeto. Aceite é booleano true, inicialmente false no controle. `requestId` é UUID v4 minúsculo imutável. Extras são rejeitados.

Nome: string, rejeitar controles Unicode Cc e substitutos isolados antes de trim, normalizar NFC e medir 2 a 80 pontos de código com `Array.from(value).length`. Sem quebras de linha. Telefone: string de teste, rejeitar controles/substitutos e medir entrada bruta até 20 pontos de código antes de remover tudo fora de `[0-9]`; resultado 10 ou 11 dígitos. Não truncar, adicionar 55 ou afirmar titularidade. `00000000000` é aceito para teste, sem contato. Revalidar no servidor tipos, enums, constantes e limites semânticos; JSON Schema não substitui essas verificações.

Não há data, hora, calendário, CPF, endereço, upload ou texto livre. Finalidade, código e preço não são campos do cliente: derivam do imóvel validado pelo escritor local e servidor. O registro adiciona `purpose,propertyCode,price,pricePeriod,priceIllustrative`; validar a tupla completa contra propertyId, não apenas enums isolados. Não permitir que o cliente forneça um catálogo ou preço. Características e valores continuam ilustrativos nos resumos de revisão e confirmação.

Exemplo normalizado:

```json
{"schemaVersion":"1.1.0","brandId":"mora-imoveis-demo","requestId":"123e4567-e89b-42d3-a456-426614174000","kind":"property_visit_request","propertyId":"patio","turn":null,"name":"Pessoa de teste","phone":"00000000000","demoAcknowledged":true}
```

## Persistência e idempotência locais

Padrão `demo_local`, endpoint vazio. Registros em `moraDemo:property-visit-requests:v1`; intenção no sufixo `:pending`; ledger no sufixo `:ledger`. Não ler ou apagar outros namespaces. Array de registros válidos: payload completo e campos derivados do catálogo mais `recordId`, `createdAt`, `status:received|cancelled`, `cancelledAt`, `mode:demo_local|demo_remote`. `recordId` UUID distinto de `requestId`, vinculado permanentemente à tentativa. Timestamps ISO-8601 UTC; received exige cancelledAt null, cancelled exige timestamp. Tokens e capabilities nunca entram no registro.

Guard → validação/snapshot → lock → releitura de registros/ledger/pending → intenção durável → escrita → releitura/validação → finalização do ledger → confirmação. Ledger guarda marca, requestId, fingerprint, estado e resultado/ponteiro; recupera interrupção entre escritas. Mesmo nonce/payload retorna resultado anterior; nonce com outros dados é conflito sem mutação. Não deduplicar por nome ou telefone. Cancelamento não reativa por replay; exclusão individual conserva tombstone.

Sucesso local somente após escrita e releitura válida. Erro de quota usa `quotaError`, preservando dados e tentativa recuperável. Falha de storage não confirma registro. JSON ou schema corrompido é preservado, sem sobrescrita silenciosa; pending ilegível bloqueia mutações e limpeza até recuperação verificável. Interrupção local exige recuperar a mesma intenção pelo ledger/registro antes de outro ID.

Sem pending, reset confirmado pode apagar registros e ledger exclusivamente locais. Isso encerra a proteção de replay do histórico local; descartar retries locais antigos antes de reset. Não remove prova pendente remota, não chama reset remoto e não recria dados apagados. Exclusão de cópia remota mantém proteção de replay e não apaga remoto automaticamente.

## Revisão, estados e gestão

Dados → validação → revisão → operação → resultado verificável. Cinco controles de dados: imóvel obrigatório, turno opcional, nome fictício, telefone fictício e aceite desmarcado inicialmente. Revisão mostra imóvel, finalidade, preço ilustrativo, turno (Sem preferência para null), nome, telefone e destino. Modo remoto, se configurado e escolhido, adiciona controles de autorização separados, sem inflar o formulário local. Antes da intenção, editar é permitido. Invalid preserva campos e associa erros ao resumo/controles. Loading, waitingLock, pending e reconciling não são sucesso. SuccessLocal exige persistência local verificada e diz que só houve registro neste navegador; nenhum estado confirma visita, reserva ou retorno futuro. SuccessRemote exige persistência remota verificada. Rejected precisa de prova terminal, nunca vira sucesso. Resposta sem prova mantém pending. Cache local falhando após aceitação remota mantém a mesma tentativa bloqueada para recuperação.

Painel `/demo`: somente solicitações efetivamente persistidas neste navegador, sem exemplos pré-carregados. Origem visível. Totais globais não filtrados: total = received + cancelled; por finalidade e imóvel usar recebidos válidos e incluir zeros. Favoritos e as quatro listagens nunca entram como contatos, receita, venda ou ocupação. Base inválida não gera zero falso. Filtros AND `propertyId,purpose,status,turn,query`; all significa sem filtro e unset representa turno null, sem pertencer ao payload. Buscar nome ou código de tentativa/registro, nunca telefone, com trim/NFC/casefold até 80 pontos de código. Vazio global e vazio filtrado têm mensagens distintas. Limpar filtros restaura defaults; Desfazer restaura um snapshot anterior válido em RAM, sem desfazer operações destrutivas.

Consulta remota administrativa é privada e separada, sem somar conjunto remoto e cópia local. Falha de leitura não vira lista vazia. Storage inválido não gera métricas parciais apresentadas como total válido; informar corrupção até recuperação ou limpeza permitida.

Cancelar exige confirmação, muda received para cancelled com timestamp e é idempotente. Cancelled não volta a received. Enquanto servidor responder received a uma tentativa de cancelamento, usar `cancelStillReceived`, preservar operationId/pending e o estado anterior; isso não confirma cancelamento. Excluir cópia e reset exigem confirmação. Durante guard ou pending, todas as mutações ficam bloqueadas. Exclusão remota exige admin e confirmação, preserva ledger e não oferece reset público em lote.

Erros, operações e resultados usam região viva e texto, não só cor. Foco visível; diálogos devolvem foco ao acionador. Não injetar payload como HTML. O badge não cobre controles. Sem JS não simular envio.

## GAS opcional: destino e autorização

`PUBLIC_DEMO_GAS_ENDPOINT` começa vazio e `integration.gasEndpoint` é `""`. Configurar URL não prova serviço ativo. Remoto exige endpoint efetivo visível, escolha explícita de modo, aceite de demo, opt-in remoto separado desmarcado por padrão e autorização válida. Opt-in é condição da UI, não propriedade do payload. Alterar modo/destino antes de uma tentativa invalida opt-in anterior; durante pending é proibido. Não enviar automaticamente nem migrar registros locais. Sem endpoint, remoto indisponível e local acessível.

POST direto com JSON em `Content-Type:text/plain;charset=UTF-8`; não criar proxy extra. Resposta deve ser legível e validada no navegador. `no-cors`, corpo opaco, timeout, HTML, falha de parse ou HTTP 200 isolado deixam resultado desconhecido. CORS, Origin e Content-Type não são autorização; não alegar CORS arbitrariamente configurável no GAS. Health positivo não comprova criação, idempotência ou persistência externa.

Único `doGet` público: `?op=health` retorna exatamente `{ok,schemaVersion,brandId}`, sem PII, IDs, contagens, catálogo pessoal ou disponibilidade. Operações privadas via `doPost` autenticado. Envelope exato `{op,authToken,payload}`; extras são rejeitados. Token de usuário/admin e capability de cancelamento somente em RAM, nunca URL, storage, export, log, ZIP ou variável `PUBLIC_*`. Segredos de servidor em Script Properties ou armazenamento privado. Reload pede nova autorização, preservando intenção. Remover autorização da página não elimina pending.

Corpo máximo 8.192 bytes UTF-8; token/capability não vazios até 512 pontos de código, sem controles ou substitutos. Fingerprint com 64 caracteres hex minúsculos; UUIDs de tentativa/operação v4 minúsculos. Revalidar payload no servidor e neutralizar interpretação de fórmula em células Sheets ao gravar strings, preservando a representação canônica para comparação. Rate limit, retenção, rotação/revogação e permissões de usuário/admin pertencem ao operador. Autorização não depende apenas de UUID ou conhecimento da URL. Não registrar PII ou segredos em logs. O contrato não certifica segurança ou autenticação comercial.

## Operações e respostas

| Operação | Campos exatos em payload | Autoridade |
| --- | --- | --- |
| `register_request` | Nove campos de `interest.payloadSchema` | Usuário autorizado |
| `request_status` | `brandId,requestId,fingerprint` | Dono da tentativa ou admin |
| `cancel_record` | `brandId,requestId,recordId,operationId,cancelCapability` | Dono autorizado com capability, ou admin com capability privada |
| `list_records` | `brandId` | Admin |
| `delete_record` | `brandId,recordId,operationId` | Admin e confirmação |

`operationId` é UUID estável por cancelamento/exclusão; retry usa o mesmo, nunca outro alvo. Capability vincula marca, tentativa e registro. Servidor calcula fingerprint da criação; cancelamento/status usam o original. Todas as respostas privadas incluem `schemaVersion,brandId,op,ok,outcome`. `outcome` é `received|cancelled|pending|rejected`. Criação/status também incluem `requestId,fingerprint`; cancelamento/exclusão incluem `operationId,recordId`. Validar correspondência exata com a operação e tentativa esperadas, incluindo payload completo do registro normalizado e seus metadados. `ok:true` sozinho não confirma nada.

Criação/status accepted retornam `ok:true,outcome:received,record` válido após persistência verificada, com `record.status:received`. Resultado já cancelado retorna `outcome:cancelled,record.status:cancelled`; não ressuscitar. Capability só em resposta privada, recuperável por status autorizado após reload e guardada em RAM. Pending retorna `ok:false,outcome:pending`. Resultado inválido mantém unknown, sem assumir rejeição.

Liturnm admin concluída: `ok:true,outcome:received,records:[...]`; received significa leitura concluída, não novas solicitações. Validar todos os registros, não converter falha em lista vazia. Exclusão admin concluída: `ok:true,outcome:cancelled,deleted:true,recordId,operationId`; esse outcome encerra a operação, não cria solicitação cancelada. Ledger conserva tombstone. Status de tentativa anteriormente aceita e excluída retorna `outcome:cancelled,deleted:true,accepted:true`, IDs e fingerprint correspondentes, sem PII do registro removido. Interface confirma exclusão e nunca oferece fallback de não aceitação.

Rejeição definitiva de criação exige `ok:false,outcome:rejected,definitive:true,accepted:false,tombstoned:true`, código `VALIDATION|UNAUTHORIZED|NONCE_CONFLICT|REJECTED_FINAL`, marca/requestId/fingerprint correspondentes e ausência comprovada de registro dessa tentativa. Tombstone gravado antes da resposta. Sem essa prova, pending permanece. Falta de autorização, conflito com outro fingerprint ou not-found isolado não encerram a tentativa original. Um conflito não autoriza tombstone que destrua nonce já aceito ou pendente com outro payload. Rejeição sem identidade válida explica erro de entrada, mas não prova terminalidade de intenção enviada.

## Fingerprint, intenção durável e reconciliação

Normalizar e validar → gerar requestId uma vez → serializar JSON em ordem `integration.contract.payloadFields`, com null explícito e sem espaços → UTF-8 → SHA-256 hex minúsculo. Excluir credenciais, opt-in e metadados. Antes da primeira rede, gravar e reler intenção:

```text
{schemaVersion,brandId,op:"register_request",requestId,fingerprint,payload,
 mode:"demo_remote",endpoint,state:"pending",createdAt}
```

Payload, destino e identidade imutáveis; não iniciar rede sem intenção preservada. Reload, fechamento, retry e timeout conservam nonce. Pending bloqueia editar, nova tentativa, CTAs que alteram seleção, reset, delete, mudança de modo/endpoint e fallback. Pending ilegível nunca é apagado para destravar. Reconciliar com POST status autorizado ou repetir criação com os mesmos dados/nonce. Not-found sozinho mantém pending porque requisição anterior ainda pode concluir.

Servidor usa `LockService.getScriptLock()` com espera limitada e liberação em `finally`. Sob lock: ler ledger durável por marca+nonce → comparar fingerprint → gravar intenção antes do efeito → procurar registro por chave única → escrever no máximo uma vez → verificar registro → finalizar ledger antes de responder. Sheets não tem transação multiaba; recuperação repara interrupção entre linha e ledger pelo mesmo vínculo, sem duplicação. Ledger não pode ser só cache/RAM. Mesmo nonce/dados devolve resultado anterior; dados diferentes são conflito sem mutação. Rejeição terminal impede aceitação tardia; exclusão impede replay; cancelamento impede reativação. Retenção conserva proteções enquanto retries/replays forem possíveis.

Após received/cancelled verificado, atualizar e reler cópia/ledger local antes de encerrar pending. Se cache falhar após aceitação remota, usar `remoteAcceptedLocalCacheError`, recuperar o mesmo registro e manter bloqueio; nunca criar novo. Deleted terminal remove cópia com ledger preservado e sem fallback. Rejeição validada não é sucesso: conservar tombstone e resolver intenção. Só depois oferecer escolha explícita de novo teste local, com NOVO nonce e ledger próprio, ou novo envio corrigido. Nunca transformar nonce rejeitado em recebido.

Cancelamento remoto prepara e relê `{schemaVersion,brandId,op:"cancel_record",requestId,recordId,operationId,fingerprint,mode,endpoint,state,createdAt}`, sem capability/token. Resultado desconhecido preserva status anterior e bloqueia mutações. Reload recupera capability por status privado e repete o mesmo operationId. Ledger de operação associa marca+operationId a tentativa/registro e conserva resultado. Exclusão remota usa intenção equivalente `op:delete_record`, com requestId/fingerprint vinculados ao registro conhecido na intenção e operationId estável; seu payload de rede segue a tabela. Não reutilizar operationId nem apagar ledger com registro.

## Downloads e limites

Recursos em `resources.links` possuem `enabled:false`. Regra: `enabled && arquivoExisteNoBuild`. Não exibir botão ativo para arquivo ausente ou desabilitado. Fonte ZIP, Code.gs, guia e contrato são recursos demonstrativos; não incluem storage, credenciais ou dados e não comprovam endpoint ativo. O código-fonte inclui somente artefatos públicos necessários, nunca segredos.

Este contrato descreve comportamento requerido, sem atestar execução de GAS/Sheets, contato, disponibilidade, autenticação comercial ou conformidade normativa ampla. Uso real exige dados e identidade verificáveis, conteúdo autorizado, operação e avaliação próprias; não preencher requisitos reais com credenciais fictícias.

## Banco e configuração do servidor opcional

Script Properties privadas: `MORA_SPREADSHEET_ID`, `MORA_USER_TOKEN_HASH`, `MORA_ADMIN_TOKEN_HASH`. Comparar hash de token autorizado; separar usuário demonstrativo de operador/admin e verificar escopo de propriedade da tentativa no servidor. Se um token de usuário for compartilhado, ele não estabelece identidade individual: consulta multiusuário exige autorização própria, sem fingir login. Token de operador e capability ficam somente em RAM no navegador. Hashes, permissões, rate limit, retenção e revogação são responsabilidade do operador. Catálogo autorizado é configuração controlada do servidor, nunca payload do cliente.

Abas e colunas exatas, em ordem (strings escritas de modo literal, sem execução de fórmula):

- `Records`: `schemaVersion, brandId, requestId, kind, propertyId, turn, name, phone, demoAcknowledged, recordId, createdAt, status, cancelledAt, mode, purpose, propertyCode, price, pricePeriod, priceIllustrative, fingerprint`.
- `Ledger`: `brandId, requestId, fingerprint, state, recordId, accepted, tombstoned, createdAt, updatedAt`.
- `Operations`: `brandId, operationId, op, requestId, recordId, fingerprint, state, createdAt, updatedAt`.
- `Capabilities`: `brandId, requestId, recordId, capabilityHash, revoked, createdAt`.

`Records` contém o payload normalizado e snapshot derivado do catálogo mais metadados; Ledger conserva nonce/fingerprint/estado e tombstones; Operations conserva o mesmo operationId para retry; Capabilities conserva apenas hash privado da capability aleatória, vinculada à marca/tentativa/registro, com revogação. Essas proteções são duráveis, nunca só cache. O health não revela IDs, métricas, clientes ou colunas. Sem endpoint testado, não afirmar integração ativa.

## Referências editoriais

As referências profissionais são registradas com URLs, data e limites no tracker de pesquisa. Foram consultadas para distinguir finalidade, exploração e contato; não constituem captura visual, auditoria normativa, credenciamento, parceria ou licença para copiar materiais.

