Skip to content

Módulo 1 - Orçamento Público & Captura de Leads (PublicQuote)

Módulo 1: Orçamento Público & Captura de Leads (PublicQuote)

Section titled “Módulo 1: Orçamento Público & Captura de Leads (PublicQuote)”

O módulo PublicQuote é a porta de entrada de novos clientes para o Prime Crown. Trata-se de um wizard público interativo que calcula estimativas de limpeza residencial com precisão e converte visitantes em leads qualificados no sistema.

O indicador de progresso preserva contraste AA também nos passos inativos. O link da marca possui nome acessível alinhado ao texto visível “Prime Crown Cleaning Services”, mantendo toque, reconhecimento de voz e leitores de tela equivalentes em navegador, PWA e WebViews nativas.


sequenceDiagram
    autonumber
    actor Client as Cliente (Web)
    participant Step1 as Passo 1: Endereço & Cobertura
    participant Step2 as Passo 2: Detalhes da Casa
    participant Step3 as Passo 3: Resultado & Frequência
    participant API as /api/leads/chat-initiate
    participant D1 as Cloudflare D1

    Client->>Step1: Digita Postcode e seleciona endereço
    Step1->>Step2: Valida área de cobertura (ServiceArea)
    Client->>Step2: Seleciona quartos, banheiros e adicionais (Addons)
    Step2->>Step3: Calcula preço e exibe descontos de frequência
    Client->>Step3: Preenche Nome + E-mail/WhatsApp
    Step3->>API: POST com quote + issueGuestToken false
    API->>D1: Lead + canal + lead_quote OPEN
    Note over API,D1: Não cria cliente nem service_request
    API-->>Client: Confirma recebimento sem criar sessão JWT

🧮 2. Regras e Fórmulas de Cálculo de Preço

Section titled “🧮 2. Regras e Fórmulas de Cálculo de Preço”

O cálculo de estimativa utiliza a seguinte lógica de formação de preço:

  1. Preço Base do Serviço: $$\text{Custo Base} = \text{Preço Fixo da Categoria} + (\text{Quartos} \times \text{Taxa Quarto}) + (\text{Banheiros} \times \text{Taxa Banheiro})$$

  2. Horas Estimadas: $$\text{Horas} = \text{Horas Base} + (\text{Metragem} \times 0.015)$$

  3. Desconto por Frequência:

    • Semanal (Weekly): 20% de desconto recorrente.
    • Quinzenal (Bi-weekly): 10% de desconto recorrente.
    • Mensal (Monthly) / Avulsa (One-off): Preço padrão sem desconto.
  4. Addons (Serviços Adicionais):

    • Limpeza de Forno, Geladeira, Passadoria de Roupas ou Limpeza de Janelas Internas são somados ao valor total.

  • Página Principal: src/pages/PublicQuote/index.tsx
  • Componente de Resultado: src/pages/PublicQuote/StepQuoteResult.tsx
  • Endereço e Cobertura: src/pages/PublicQuote/StepAddressForm.tsx
  • Backend Handler: functions/api/leads/chat-initiate.ts
  • Contrato de Cotação: functions/api/leads/leadQuote.ts
  • Confirmação: functions/api/leads/confirmLeadQuote.ts
  • Fila Administrativa: src/pages/Quotes.tsx
  • Pedido Autenticado: src/pages/RequestService.tsx

🔄 4. Lead, cotação e cliente são estados distintos

Section titled “🔄 4. Lead, cotação e cliente são estados distintos”

O visitante permanece em leads enquanto apenas solicita um preço. A intenção comercial vive em lead_quotes, com estado OPEN, CONFIRMED ou DECLINED. Reenviar o formulário atualiza a única cotação aberta daquele lead; decisões anteriores continuam como histórico.

Somente a confirmação administrativa cria ou reutiliza uma linha em clients e materializa o service_request. O gestor escolhe a entidade fiscal e pode ajustar o preço final; VAT é calculado nesse momento porque depende da entidade que faturará. A confirmação também marca o lead como convertido e move contas cujo reference_type era LEAD para o cliente resultante. A recusa não cria cliente nem atendimento e cancela follow-ups comerciais pendentes.

Na promoção manual pela tela Leads, o cadastro do cliente é confirmado primeiro e seu ID exato segue para a conversão. Na promoção legada disparada por alocação, usa-se o cliente que já pertence à tarefa. A store nunca fabrica um segundo cliente: o backend move/consolida o canal no destino informado. Uma única operação por lead é aceita em cada instante; repetições compartilham a promise e destinos conflitantes são denunciados. Se a conversão falhar, somente o objeto otimista daquela tentativa é revertido, sem apagar edições paralelas em outros leads.

Na revisão, o preço final é um campo decimal obrigatório e editável — não um snapshot somente leitura. Total, VAT e mensagem de confirmação acompanham a edição. Confirmar e recusar aguardam a resposta autoritativa antes do feedback e atualizam a cotação local para que voltar à fila não ressuscite uma linha já decidida. A identificação de recorrência usa createUuid, sem Math.random, mantendo o mesmo contrato criptográfico em navegador, PWA e WebViews iOS/Android.

Confirmar e recusar compartilham um lock síncrono adquirido antes da primeira chamada remota, impedindo dois comandos ou ações cruzadas antes do próximo render. O composer de WhatsApp usa CFInput e Button, bloqueia edição/restauração durante o envio e adquire seu próprio lock antes de abrir a confirmação. A mensagem é aparada e capturada naquele momento; somente o texto efetivamente aprovado segue ao canal do lead, sem dois diálogos ou payload que muda durante a decisão.

A página autenticada Request Service usa POST /api/leads/my-quote. O leadId nunca é aceito do corpo: ele é derivado do cookie de uma conta ligada a lead ou do bearer de convidado usado pelo widget. Website e aplicativo alimentam, portanto, a mesma fila Quotes.

As telas de lead/cotação usam os primitives oficiais (PageHeader, Card, Button, CFInput, CFPillSelector, CFEmptyState e IconBadge) e superfícies claras responsivas. Envio de OTP e cotação têm locks síncronos contra toque duplo; Request Service limita o POST a 15 segundos, sair da tela aborta a requisição pendente e timeout devolve o formulário ao estado recuperável.

No wizard público, a preferência de semana usa CFSelect sem perder o picker nativo do sistema e as observações usam CFInput multilinha com o limite autoritativo de 500 caracteres. Manhã, tarde e horário flexível formam um grupo nomeado e publicam o estado pressionado, tornando a mesma seleção compreensível por teclado, TalkBack e VoiceOver em PWA, Android, iOS e desktop.

Os incrementos de quartos, duração de adicionais e ajuste final de tempo preservam os ícones compactos no desktop, mas usam touch-target para atingir 44×44 px em telas coarse/touch. Cada ação possui nome contextual (“Increase bedroom quantity”, “Decrease oven duration”, por exemplo), e o seletor de adicional publica aria-pressed; o mesmo controle permanece operável por polegar, teclado, TalkBack e VoiceOver.

As escolhas de tipo de serviço e frequência também publicam aria-pressed em cada opção. Assim, gradiente, borda e círculo continuam compondo a aparência visual, mas leitores de tela recebem a mesma seleção que usuários de mouse ou toque enxergam.

Catálogos, configuração e rascunho podem resolver em ordens diferentes, sobretudo em rede móvel. O corpo do wizard sincroniza novamente as durações mínimas dos adicionais quando o catálogo chega depois da seleção e recalcula o piso de progresso quando o serviço chega depois do rascunho. A sincronização só eleva mínimos e progresso: não reduz uma duração maior escolhida pelo visitante nem substitui sua escolha atual. quoteAsyncSync.test.ts protege essas condições para navegador, PWA e WebViews.

A revelação progressiva usa um único timeout substituível para scroll. Seleções rápidas cancelam o salto anterior antes de apontar para a etapa atual, e desmontagem cancela a navegação pendente; o wizard não volta visualmente para uma pergunta antiga depois que a pessoa já avançou em touch, teclado ou mouse.

Todas as chamadas do wizard usam o transporte limitado compartilhado por /quote, /pay, /statement, /proposal, /rate e pelas operações autenticadas que adotam a mesma primitiva. Leituras encerram em 12 segundos e mutações em 15 segundos. Trocar de tela ou desmontar a WebView aborta catálogo, cálculo, rascunho e envio final; locks síncronos impedem duplo toque antes que o estado React seja renderizado. Uma falha de catálogo oferece Try again, enquanto cálculo e confirmação retornam ao estado interativo com mensagem de erro. O mesmo contrato vale para navegador, PWA, desktop e WebViews Android/iOS.

O catálogo administrativo de preços de carpetes segue o mesmo contrato. A leitura mais nova cancela a anterior e somente ela pode publicar a lista; carregamento, falha com snapshot preservado e vazio confirmado são estados distintos. Salvar e excluir usam limite de 15 segundos, cancelamento no teardown e um lock síncrono único adquirido antes da confirmação de exclusão. Enquanto uma mutação está em voo, edição, exclusão e fechamento do formulário ficam indisponíveis e anunciam o estado ocupado; falha mantém o formulário ou item autoritativo e oferece retry explícito, sem falso sucesso em nenhuma das quatro superfícies.

O seletor compartilhado de endereço também reconcilia cobertura nos dois sentidos quando a lista de districts resolve: um postcode pode passar de fora da área para coberto sem nova digitação, e os indicadores de validação/completude acompanham a decisão atual. O Reset possui uma única fronteira de callback, evitando desmontar e limpar o wizard duas vezes em uma ativação por toque, teclado ou leitor de tela.

Lookup de postcode e refinamento geográfico são operações latest-wins: uma nova busca, Reset ou desmontagem aborta a anterior e invalida qualquer resposta tardia. O postcode possui teto de 12 segundos e o refinamento, 8 segundos; timeout retorna o formulário ao estado de erro recuperável em vez de deixá-lo preso em loading. O cleanup reabre o gate da busca automática durante a simulação do React StrictMode, preservando o mesmo comportamento em desenvolvimento e produção.

O passo de confirmação não abre o chat de convidado. Por isso envia issueGuestToken: false: o backend conclui a captura, cria/atualiza o lead, o canal operacional e a cotação, mas não gera nem devolve um JWT desnecessário. A interface também não grava token ou channelId em sessionStorage. Outros consumidores de chat-initiate continuam recebendo o token por padrão quando realmente usam chat-messages.

Quote e Contact Us no widget do site gravam o lead (e a cotação, quando houver) mais a mensagem inicial no canal interno. No telemóvel abrem sempre wa.me no WhatsApp do visitante (número e texto em Automatic replies). O interruptor On nessas duas opções só liga a confirmação automática pelo Evolution, com texto editável (widgetQuoteAutoReplyMessage / widgetContactAutoReplyMessage); Off continua a abrir o WhatsApp do utilizador sem envio Evolution, e a caixa dessa resposta automática fica desativada (cinzenta).

chat-initiate e check-number usam a mesma allowlist CORS. Navegadores são aceitos somente nos domínios oficiais, previews HTTPS controlados do Pages, desenvolvimento local e origins padrão do Capacitor; uma origem desconhecida recebe 403 antes de criar registros ou consultar o provedor de WhatsApp. Chamadas backend sem cabeçalho Origin continuam disponíveis para integrações confiáveis, e Vary: Origin evita que caches misturem decisões.

Clientes HTTP diretos passam ainda pelo rate limiter D1 antes do payload: chat-initiate permite 5 tentativas em 15 minutos e check-number, 30 em 10 minutos por marcador de cliente e endpoint. Excesso retorna 429 com Retry-After, sem criar lead, canal ou mensagem nem consultar WhatsApp. O endereço recebido do edge é persistido somente como SHA-256; entradas expiradas são removidas dentro da própria janela.

Falhas internas desses endpoints não revelam mensagens do D1, Evolution ou stack ao formulário, PWA ou shells. chat-initiate grava a exceção pelo helper sanitizado e retorna uma orientação estável para revisar/tentar novamente; check-number faz o mesmo sem incluir o erro do provedor.

Depois da captura, os CRUDs autenticados de leads, serviços, adicionais, descontos por frequência e área de atendimento usam o contrato atômico seguro. Uma falha inesperada de persistência é investigável em system_logs, mas não muda de forma entre desktop, PWA e shells nativos nem revela detalhes do D1.

Toda a superfície /api/public/* usada pelo orçamento aplica também publicServerError. Carregamento de serviços/adicionais, criação de lead e cálculo usam saídas protegidas: falhas do D1 e ausência de configuração fiscal ficam na telemetria sanitizada, enquanto o visitante recebe somente uma orientação estável de nova tentativa. Validações 400 e conflitos 409 continuam específicos. As antigas rotas de conversão pública direta foram removidas; somente a confirmação autenticada da cotação converte lead em cliente. Respostas bem-sucedidas de catálogo preservam seu cache público; erros não são armazenados.