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)}