From 690b15ece100d7bafca064b159b958496f488250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 29 Sep 2026 10:31:41 -0300 Subject: [PATCH] feat: fill the delivery address from the CEP and quote freight as it is typed A complete CEP now looks up its address on the server (ViaCEP, rate limited, keeping the Site's CSP to its own origin) and fills street, district, city and state, moving the cursor to the first field left, and quotes the freight without pressing Calcular. The CEP keeps its mask when the cart is restored. Freight and its delivery time appear in the order summary instead of a line under the CEP, the delivery option says it ships with Jadlog, and the address reminder is a hint rather than an error. Co-Authored-By: Claude Opus 5.5 --- app/api/health.py | 24 +++++++++++++++++++++++- web/cart.js | 2 +- web/checkout.js | 4 ++-- web/index.html | 5 +++-- web/site-cart.js | 3 ++- web/site-flow.js | 28 +++++++++++++++++++++++++--- web/site-v2.css | 2 ++ 7 files changed, 58 insertions(+), 10 deletions(-) diff --git a/app/api/health.py b/app/api/health.py index 134d586..ca0527b 100644 --- a/app/api/health.py +++ b/app/api/health.py @@ -1,6 +1,8 @@ -"""Health, session bootstrap and freight quoting.""" +"""Health, session bootstrap, freight quoting and the address of a CEP.""" import os +import re +import httpx from fastapi import APIRouter, HTTPException, Request, Response from ..core import db @@ -48,3 +50,23 @@ def quote_freight(body: FreightEstimate): return freight.quote(body.service, body.postal_code, body.metres, body.declared_cents) except ValueError as exc: raise HTTPException(422, str(exc)) + +# The address of a CEP, so the cart fills it in. Looked up here rather than in +# the browser, which keeps the Site's CSP to its own origin. +VIACEP = 'https://viacep.com.br/ws/{}/json/' +CEP_LIMIT = 120 + +@router.get('/api/cep/{cep}') +def cep_address(cep: str, request: Request): + if not re.fullmatch(r'[0-9]{8}', cep): + raise HTTPException(422, 'CEP must have eight digits') + rate_limit('cep-lookup', client_ip(request), CEP_LIMIT, 900) + try: + response = httpx.get(VIACEP.format(cep), timeout=5) + data = response.json() if response.status_code == 200 else {} + except (httpx.HTTPError, ValueError): + raise HTTPException(503, 'Consulta de CEP indisponível') + if not data or data.get('erro'): + raise HTTPException(404, 'CEP não encontrado') + return {'street': data.get('logradouro') or '', 'district': data.get('bairro') or '', + 'city': data.get('localidade') or '', 'state': data.get('uf') or ''} diff --git a/web/cart.js b/web/cart.js index 8cee1da..0409a7c 100644 --- a/web/cart.js +++ b/web/cart.js @@ -34,7 +34,7 @@ // Freight must be quoted again; restored browser values are never final. if(entrega.tipo==='frete'){entrega.cotado=false;entrega.valor=0;} for(const [id,key] of [['fCnpj','cnpj'],['fZap','zap'],['fMail','mail']])$(id).value=cliente[key]; - $('cepIn').value=entrega.cep; + $('cepIn').value=fmtCep(entrega.cep); entrega.end=entrega.end||{}; for(const [id,key] of [['eNome','nome'],['eRua','rua'],['eNum','num'],['eComp','comp'], ['eBairro','bairro'],['eCidade','cidade'],['eUf','uf']])$(id).value=entrega.end[key]||''; diff --git a/web/checkout.js b/web/checkout.js index a77772e..b017735 100644 --- a/web/checkout.js +++ b/web/checkout.js @@ -118,10 +118,10 @@ const result = await api('/freight',{service,postal_code:cep,metres:metres.toFixed(2),declared_cents:cents}); if (entrega.cep !== cep || entrega.tipo !== 'frete') return; entrega.valor = result.total_cents/100; + entrega.dias = result.days || null; entrega.cotado = true; freightQuoted = JSON.stringify(pacote()); - $('cepMsg').textContent = 'Frete: '+rs(entrega.valor)+ - (result.days ? ' · entrega em até '+result.days+' dias úteis.' : '.'); + $('cepMsg').textContent = ''; pintaEntrega(); } catch(error) { $('cepMsg').textContent = error.message; } }; diff --git a/web/index.html b/web/index.html index 20ed7c4..683c054 100644 --- a/web/index.html +++ b/web/index.html @@ -644,6 +644,7 @@ h1 em{font-style:normal;color:var(--laranja)} font-family:"Inter",sans-serif;font-weight:600;font-size:12.5px} .cepL button:hover{border-color:var(--laranja);color:var(--laranja)} .cep p{font-size:11.5px;color:var(--fraco);margin-top:9px;line-height:1.5} +.cep p:empty{display:none} .cep .endereco{margin-top:12px} .cep .err{color:var(--vermelho)} .avisoE:empty{display:none} @@ -1162,7 +1163,7 @@ footer a:hover{color:var(--laranja2)}
Receber em casa - Cotamos com a transportadora pelo seu CEP + Entrega pela Jadlog em todo o Brasil. Informe o CEP para ver o valor e o prazo.
— @@ -1189,7 +1190,7 @@ footer a:hover{color:var(--laranja2)}
-

+

diff --git a/web/site-cart.js b/web/site-cart.js index 9297e79..accb66a 100644 --- a/web/site-cart.js +++ b/web/site-cart.js @@ -49,7 +49,8 @@ function pintaPedido(){ '
'+n+(n===1?' item':' itens')+''+rs(itens+eco)+'
'+ (eco>0.01? '
Desconto pela nota'+ '− '+rs(eco)+'
' : '')+ - '
Frete'+ + '
Frete'+(entrega.tipo==='frete' && entrega.cotado && entrega.dias + ? ' · até '+entrega.dias+' dias úteis' : '')+''+ (entrega.tipo==='retira' ? 'Grátis' : entrega.cotado ? rs(frete) : 'a cotar')+'
'+ '
Revisão e reencaixe'+ diff --git a/web/site-flow.js b/web/site-flow.js index c31e34f..911e2e3 100644 --- a/web/site-flow.js +++ b/web/site-flow.js @@ -106,7 +106,7 @@ function pintaEntrega(){ : entrega.cotado ? rs(entrega.valor) : 'a cotar'; $('bPagar').disabled = !entrega.cotado || !clienteOk() || !enderecoOk() || !cartPodeEnviar(); $('eEnd').textContent = entrega.tipo==='frete' && entrega.cotado && !enderecoOk() - ? 'Preencha o endereço de entrega para seguir.' : ''; + ? 'Complete o endereço de entrega para ir ao pagamento.' : ''; pintaPedido(); } @@ -158,10 +158,31 @@ document.querySelectorAll('.opE').forEach(b=>b.addEventListener('click',()=>{ if(entrega.tipo==='frete') $('cepMsg').innerHTML=''; pintaEntrega(); })); +function fmtCep(v){ v=String(v||'').replace(/\D/g,'').slice(0,8); return v.length>5 ? v.slice(0,5)+'-'+v.slice(5) : v; } +// The address of a complete CEP fills the form; only the number is left to type. +let cepBuscado=''; +async function buscaEndereco(cep){ + if(cep===cepBuscado || !window.dtfApi) return; + cepBuscado=cep; + try{ + const a=await window.dtfApi('/cep/'+cep); + if(entrega.cep!==cep) return; + for(const [id,chave,valor] of [['eRua','rua',a.street],['eBairro','bairro',a.district], + ['eCidade','cidade',a.city],['eUf','uf',a.state]]){ + if(!valor) continue; + $(id).value=valor; entrega.end={...(entrega.end||{}),[chave]:valor}; + } + pintaEntrega(); + const vazio=['eNome','eRua','eNum'].map($).find(el=>!el.value.trim()); + if(vazio && document.activeElement===$('cepIn')) vazio.focus(); + }catch(e){ /* sem consulta: o cliente preenche o endereço */ } +} $('cepIn').addEventListener('input',e=>{ const v=e.target.value.replace(/\D/g,'').slice(0,8); - e.target.value = v.length>5 ? v.slice(0,5)+'-'+v.slice(5) : v; - entrega.cep=v; entrega.cotado=false; entrega.valor=0; pintaEntrega(); + e.target.value = fmtCep(v); + if(v===entrega.cep) return; + entrega.cep=v; entrega.cotado=false; entrega.valor=0; entrega.dias=null; pintaEntrega(); + if(v.length===8){ buscaEndereco(v); if(window.dtfFreight) window.dtfFreight(); } }); for(const [id,chave] of [['eNome','nome'],['eRua','rua'],['eNum','num'],['eComp','comp'], ['eBairro','bairro'],['eCidade','cidade'],['eUf','uf']]){ @@ -173,6 +194,7 @@ for(const [id,chave] of [['eNome','nome'],['eRua','rua'],['eNum','num'],['eComp' } $('bCep').addEventListener('click',async()=>{ if(entrega.cep.length!==8){ $('cepMsg').innerHTML='CEP incompleto.'; return; } + buscaEndereco(entrega.cep); if(window.dtfFreight){ await window.dtfFreight(); return; } // A cotação vem da transportadora. Enquanto a integração não existir, o pedido não // fecha por frete: o valor precisa estar fechado antes do Mercado Pago. diff --git a/web/site-v2.css b/web/site-v2.css index 36615a0..767e797 100644 --- a/web/site-v2.css +++ b/web/site-v2.css @@ -228,6 +228,8 @@ section+section{border-top:0} /* The payment pages: paying on the left, the order summary on the right */ html:is([data-rota="pagamento"],[data-rota="pix"]) .checkout{max-width:1040px;margin-top:8px} html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:32px;align-items:start} +.dicaEnd{font-size:13px;color:var(--texto2);margin-top:12px} +.dicaEnd:empty{display:none} .pagCab{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:20px} .pagCab h3{font-size:26px;font-weight:800;letter-spacing:-.02em} .pagVolta{font-size:14px;color:var(--texto2)}