feat: price home delivery with Jadlog
All checks were successful
Build and deploy / Validate source (push) Successful in 1m18s
Build and deploy / Integration suite on a real stack (push) Successful in 2m33s
Build and deploy / Secret scan and release gate (push) Successful in 9s
Build and deploy / Publish images (push) Successful in 1m42s
All checks were successful
Build and deploy / Validate source (push) Successful in 1m18s
Build and deploy / Integration suite on a real stack (push) Successful in 2m33s
Build and deploy / Secret scan and release gate (push) Successful in 9s
Build and deploy / Publish images (push) Successful in 1m42s
FREIGHT_ADAPTER=jadlog prices "Receber em casa" through Jadlog's Simulador de Frete from the order's billed metres and value, adding production days to Jadlog's delivery time. The package weight is a base plus a weight per metre from the client, with no default: the adapter refuses to start without it and without the credentials. The cart re-quotes when the package changes, and approval quotes again from the server-priced items. The production stack takes the Jadlog settings, so the read-only probe runs from the worker's console. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,16 @@ def require_runtime():
|
||||
checkout until their audited implementations are added.
|
||||
"""
|
||||
environment = os.environ.get('APP_ENV', 'local')
|
||||
for name in ('FREIGHT', 'WHATSAPP'):
|
||||
if os.environ.get(f'{name}_ADAPTER') != 'fake':
|
||||
raise RuntimeError(f'{name} must use the currently supported fake adapter')
|
||||
if os.environ.get('WHATSAPP_ADAPTER') != 'fake':
|
||||
raise RuntimeError('WHATSAPP must use the currently supported fake adapter')
|
||||
freight = os.environ.get('FREIGHT_ADAPTER')
|
||||
if freight == 'jadlog':
|
||||
from .jadlog import required_settings as jadlog_settings
|
||||
for name in jadlog_settings():
|
||||
if not os.environ.get(name):
|
||||
raise RuntimeError(f'{name} is required for the Jadlog adapter')
|
||||
elif freight != 'fake':
|
||||
raise RuntimeError('FREIGHT must use the fake or jadlog adapter')
|
||||
tiny = os.environ.get('TINY_ADAPTER')
|
||||
if tiny == 'tiny':
|
||||
from .tiny import required_settings
|
||||
@@ -148,12 +155,12 @@ class FakePayment:
|
||||
'status': 'paid', 'total_cents': total_cents}
|
||||
|
||||
class FreightAdapter(Protocol):
|
||||
def quote(self, service: str, postal_code: str) -> dict: ...
|
||||
def quote(self, service: str, postal_code: str, metres=None, declared_cents: int = 0) -> dict: ...
|
||||
|
||||
class FakeFreight:
|
||||
name = 'fake'
|
||||
|
||||
def quote(self, service: str, postal_code: str) -> dict:
|
||||
def quote(self, service: str, postal_code: str, metres=None, declared_cents: int = 0) -> dict:
|
||||
if service == 'pickup':
|
||||
return {'provider': 'fake', 'service': 'pickup', 'total_cents': 0, 'postal_code': ''}
|
||||
if service != 'mock-standard' or len(postal_code) != 8 or not postal_code.isascii() or not postal_code.isdigit():
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from ..core import db
|
||||
from ..core.auth import client_ip, owner, new_session, rate_limit
|
||||
from ..core.limits import upload_limit_bytes
|
||||
from ..core.models import Freight
|
||||
from ..core.models import FreightEstimate
|
||||
from ..runtime import (ENVIRONMENT, GUEST_SESSION_LIMIT, PART_BYTES, freight, payment,
|
||||
require_delivery_available, storage)
|
||||
|
||||
@@ -37,12 +37,14 @@ def session(request: Request, response: Response):
|
||||
return {'environment': ENVIRONMENT, 'cart_scope': str(session_id), 'part_bytes': PART_BYTES,
|
||||
'max_upload_bytes': upload_limit_bytes(), 'payment_provider': payment.name,
|
||||
# Public by design: Mercado Pago's card form needs it in the browser.
|
||||
'payment_public_key': os.environ.get('MP_PUBLIC_KEY', '') if payment.name == 'mercadopago' else ''}
|
||||
'payment_public_key': os.environ.get('MP_PUBLIC_KEY', '') if payment.name == 'mercadopago' else '',
|
||||
# The delivery service the cart quotes, or none: pickup only.
|
||||
'freight_service': 'jadlog' if freight.name == 'jadlog' else 'mock-standard' if ENVIRONMENT == 'local' else None}
|
||||
|
||||
@router.post('/api/freight')
|
||||
def quote_freight(body: Freight):
|
||||
def quote_freight(body: FreightEstimate):
|
||||
require_delivery_available(body.service)
|
||||
try:
|
||||
return freight.quote(body.service, body.postal_code)
|
||||
return freight.quote(body.service, body.postal_code, body.metres, body.declared_cents)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, str(exc))
|
||||
|
||||
@@ -10,6 +10,7 @@ from psycopg.types.json import Jsonb
|
||||
from ..core import db
|
||||
from ..core.auth import owner
|
||||
from ..core.models import QuoteRequest
|
||||
from ..core.pricing import price
|
||||
from .. import quote_review
|
||||
from ..runtime import freight, quote_view, require_delivery_available, upload_row
|
||||
from ..scanning import require_clean
|
||||
@@ -24,8 +25,13 @@ def create_quote(body: QuoteRequest, session_id=Depends(owner)):
|
||||
require_delivery_available(body.freight.service)
|
||||
draft = body.model_dump(mode='json', exclude={'request_key'})
|
||||
digest = hashlib.sha256(json.dumps(draft, sort_keys=True).encode()).hexdigest()
|
||||
reason = quote_review.review_reason(draft)
|
||||
if reason is not None:
|
||||
# Waits for review: check the delivery now, priced at approval.
|
||||
try:
|
||||
freight.quote(body.freight.service, body.freight.postal_code)
|
||||
priced = [price(item.mode, str(item.metres), item.grade) for item in body.items]
|
||||
freight.quote(body.freight.service, body.freight.postal_code,
|
||||
sum(Decimal(i['billed_metres']) for i in priced), sum(i['total_cents'] for i in priced))
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, str(exc))
|
||||
with db.connect() as c:
|
||||
@@ -41,7 +47,6 @@ def create_quote(body: QuoteRequest, session_id=Depends(owner)):
|
||||
row = c.execute('SELECT * FROM dtf_local.quotes WHERE owner=%s AND request_key=%s FOR UPDATE', (session_id, body.request_key)).fetchone()
|
||||
if row['request_hash'] != digest:
|
||||
raise HTTPException(409, 'Request key already used for a different cart')
|
||||
reason = quote_review.review_reason(draft)
|
||||
if row['id'] == uid and reason is None:
|
||||
quote_review.approve(c, row, body.items, quote_review.AUTO)
|
||||
return {'id': row['id'], 'status': 'approved'}
|
||||
|
||||
@@ -41,9 +41,15 @@ class Customer(StrictModel):
|
||||
return value.strip()
|
||||
|
||||
class Freight(StrictModel):
|
||||
service: Literal['pickup','mock-standard'] = 'pickup'
|
||||
service: Literal['pickup','mock-standard','jadlog'] = 'pickup'
|
||||
postal_code: str = Field(default='', max_length=8)
|
||||
|
||||
class FreightEstimate(Freight):
|
||||
"""The cart's estimate. The charged freight is quoted again by the server
|
||||
from the approved items, never from these numbers."""
|
||||
metres: Decimal | None = Field(default=None, gt=0, le=12000)
|
||||
declared_cents: int = Field(default=0, ge=0, le=100_000_000, strict=True)
|
||||
|
||||
UF = Literal['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB',
|
||||
'PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO']
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ unlike Tiny it can be tried on the real account as often as needed.
|
||||
|
||||
The client's contract: origin CEP 14402-310, service .PACKAGE (modalidade 3),
|
||||
home delivery. Jadlog prices by weight in kg and expects the larger of the
|
||||
real and the cubed weight; the weight per metre of film is still to come from
|
||||
the client, so nothing here decides it.
|
||||
real and the cubed weight. The package weight comes from the client (a base
|
||||
plus a weight per billed metre) and has no default: FREIGHT_ADAPTER=jadlog
|
||||
refuses to start without it, so a guessed weight never prices a customer's
|
||||
freight.
|
||||
"""
|
||||
import os
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
@@ -18,6 +20,8 @@ import httpx
|
||||
QUOTE_URL = 'https://www.jadlog.com.br/embarcador/api/frete/valor'
|
||||
PACKAGE = 3
|
||||
ORIGIN = '14402310'
|
||||
SERVICE = 'jadlog'
|
||||
WEIGHT_SETTINGS = ('JADLOG_PESO_BASE_KG', 'JADLOG_PESO_POR_METRO_KG')
|
||||
|
||||
|
||||
class JadlogError(Exception):
|
||||
@@ -72,3 +76,42 @@ class JadlogQuotes:
|
||||
total = Decimal(str(items[0]['vltotal']))
|
||||
return {'total_cents': int((total * 100).quantize(Decimal('1'), ROUND_HALF_UP)),
|
||||
'days': items[0].get('prazo'), 'raw': items[0]}
|
||||
|
||||
|
||||
def required_settings():
|
||||
return ('JADLOG_TOKEN', 'JADLOG_CNPJ') + WEIGHT_SETTINGS
|
||||
|
||||
|
||||
class JadlogFreight:
|
||||
"""The Site's delivery option: Jadlog's price for the order's package."""
|
||||
name = 'jadlog'
|
||||
|
||||
def __init__(self, quotes=None):
|
||||
self.quotes = quotes or JadlogQuotes()
|
||||
self.base_kg = Decimal(os.environ['JADLOG_PESO_BASE_KG'])
|
||||
self.per_metre_kg = Decimal(os.environ['JADLOG_PESO_POR_METRO_KG'])
|
||||
# Jadlog's time counts from collection; the order is printed first.
|
||||
self.production_days = int(os.environ.get('FREIGHT_PRODUCTION_DAYS') or 0)
|
||||
if self.base_kg < 0 or self.per_metre_kg <= 0 or self.production_days < 0:
|
||||
raise RuntimeError('Jadlog package weight and production days must be positive')
|
||||
|
||||
def weight_kg(self, metres):
|
||||
return (self.base_kg + self.per_metre_kg * Decimal(str(metres))).quantize(Decimal('0.001'))
|
||||
|
||||
def quote(self, service, postal_code, metres=None, declared_cents=0):
|
||||
if service == 'pickup':
|
||||
return {'provider': self.name, 'service': 'pickup', 'total_cents': 0, 'postal_code': ''}
|
||||
if service != SERVICE or len(postal_code) != 8 or not postal_code.isascii() or not postal_code.isdigit():
|
||||
raise ValueError('Select pickup or Jadlog delivery with an eight-digit CEP')
|
||||
if metres is None or Decimal(str(metres)) <= 0:
|
||||
raise ValueError('The order length is required to quote freight')
|
||||
weight = self.weight_kg(metres)
|
||||
try:
|
||||
result = self.quotes.quote(postal_code, weight, int(declared_cents))
|
||||
except (JadlogError, httpx.HTTPError) as error:
|
||||
raise ValueError(f'Não foi possível cotar o frete na Jadlog agora: {error}') from None
|
||||
days = result['days']
|
||||
return {'provider': self.name, 'service': SERVICE, 'postal_code': postal_code,
|
||||
'total_cents': result['total_cents'], 'weight_kg': str(weight),
|
||||
'days': None if days is None else int(days) + self.production_days,
|
||||
'description': 'Jadlog .PACKAGE'}
|
||||
|
||||
@@ -9,6 +9,11 @@ number is required (Jadlog names it in the error), and what the contract
|
||||
prices and delivery times are. The first failure stops the run: an account
|
||||
problem would repeat on every line.
|
||||
|
||||
In production, from the worker's console (Portainer > Containers > worker >
|
||||
Console), with JADLOG_TOKEN, JADLOG_CNPJ and JADLOG_CONTA in the stack:
|
||||
|
||||
python -m app.jadlog_probe
|
||||
|
||||
Locally, with the credentials in jadlog.env (ignored by git):
|
||||
|
||||
docker compose -f compose.local.yaml -f compose.providers.yaml run --rm --no-deps --build \\
|
||||
|
||||
@@ -72,7 +72,12 @@ def approve(c, row, items, reviewer):
|
||||
'uploads': original['uploads'], 'production': original['production'],
|
||||
'quality_status': original['quality_status'],
|
||||
'quality_acknowledged': original['quality_acknowledged']})
|
||||
quoted_freight = freight.quote(**draft['freight'])
|
||||
metres = sum(Decimal(i['billed_metres']) for i in priced)
|
||||
try:
|
||||
quoted_freight = freight.quote(draft['freight']['service'], draft['freight'].get('postal_code', ''),
|
||||
metres, sum(i['total_cents'] for i in priced))
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, str(exc))
|
||||
approved = {'customer': draft['customer'], 'items': priced, 'freight': quoted_freight,
|
||||
'destination': draft.get('destination'),
|
||||
'total_cents': sum(i['total_cents'] for i in priced) + quoted_freight['total_cents']}
|
||||
|
||||
@@ -25,6 +25,10 @@ if os.environ.get('PAYMENT_ADAPTER') == 'mercadopago':
|
||||
payment = MercadoPagoPayment()
|
||||
else:
|
||||
payment = FakePayment()
|
||||
if os.environ.get('FREIGHT_ADAPTER') == 'jadlog':
|
||||
from .jadlog import JadlogFreight
|
||||
freight = JadlogFreight()
|
||||
else:
|
||||
freight = FakeFreight()
|
||||
ENVIRONMENT = os.environ.get('APP_ENV', 'local')
|
||||
PUBLIC_ORIGIN = os.environ.get('PUBLIC_ORIGIN', 'http://localhost')
|
||||
|
||||
@@ -36,7 +36,14 @@ x-app: &app
|
||||
MP_WEBHOOK_SECRET: ${MP_WEBHOOK_SECRET:-}
|
||||
MP_NOTIFICATION_URL: ${MP_NOTIFICATION_URL:-}
|
||||
MP_PUBLIC_KEY: ${MP_PUBLIC_KEY:-}
|
||||
FREIGHT_ADAPTER: fake
|
||||
FREIGHT_ADAPTER: ${FREIGHT_ADAPTER:-fake}
|
||||
JADLOG_TOKEN: ${JADLOG_TOKEN:-}
|
||||
JADLOG_CNPJ: ${JADLOG_CNPJ:-}
|
||||
JADLOG_CONTA: ${JADLOG_CONTA:-}
|
||||
JADLOG_CONTRATO: ${JADLOG_CONTRATO:-}
|
||||
JADLOG_PESO_BASE_KG: ${JADLOG_PESO_BASE_KG:-}
|
||||
JADLOG_PESO_POR_METRO_KG: ${JADLOG_PESO_POR_METRO_KG:-}
|
||||
FREIGHT_PRODUCTION_DAYS: ${FREIGHT_PRODUCTION_DAYS:-0}
|
||||
# Carts the Site priced are approved at checkout; larger ones wait for review.
|
||||
QUOTE_AUTO_APPROVE: ${QUOTE_AUTO_APPROVE:-true}
|
||||
QUOTE_AUTO_MAX_METRES: ${QUOTE_AUTO_MAX_METRES:-50}
|
||||
|
||||
@@ -34,7 +34,21 @@ x-app-environment: &app-environment
|
||||
# nothing and therefore accepts nothing, which is the correct state until a
|
||||
# provider is connected. Never set it to a value anyone could guess.
|
||||
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-}
|
||||
FREIGHT_ADAPTER: fake
|
||||
# fake offers pickup only. jadlog prices delivery with Jadlog and needs the
|
||||
# credentials below and the package weight from the client; it refuses to
|
||||
# start without them. The credentials alone are enough for the console
|
||||
# check, python -m app.jadlog_probe, on the worker.
|
||||
FREIGHT_ADAPTER: ${FREIGHT_ADAPTER:-fake}
|
||||
JADLOG_TOKEN: ${JADLOG_TOKEN:-}
|
||||
# The "Usuário" Jadlog issued: the CNPJ that contracts the freight.
|
||||
JADLOG_CNPJ: ${JADLOG_CNPJ:-}
|
||||
JADLOG_CONTA: ${JADLOG_CONTA:-}
|
||||
JADLOG_CONTRATO: ${JADLOG_CONTRATO:-}
|
||||
# Package weight in kg: a base plus each billed metre of film.
|
||||
JADLOG_PESO_BASE_KG: ${JADLOG_PESO_BASE_KG:-}
|
||||
JADLOG_PESO_POR_METRO_KG: ${JADLOG_PESO_POR_METRO_KG:-}
|
||||
# Working days of production added to Jadlog's delivery time.
|
||||
FREIGHT_PRODUCTION_DAYS: ${FREIGHT_PRODUCTION_DAYS:-0}
|
||||
# A cart the Site priced is approved at checkout and can be paid at once;
|
||||
# orders above QUOTE_AUTO_MAX_METRES, or with a grade the Site did not
|
||||
# compute, wait for an operator on the Kanban (app/quote_review.py).
|
||||
|
||||
@@ -265,6 +265,15 @@ From the report already sent. These are dated promises, not backlog.
|
||||
2026-09-28). `app/jadlog.py` prices one package from the manual (v2.3);
|
||||
`python -m app.jadlog_probe` prices test weights to six regions, read-only,
|
||||
to confirm token, account and contract on the client's account. Not yet run.
|
||||
**Adapter (2026-09-29):** `FREIGHT_ADAPTER=jadlog` prices "Receber em casa"
|
||||
with Jadlog from the order's billed metres (`JADLOG_PESO_BASE_KG` plus
|
||||
`JADLOG_PESO_POR_METRO_KG` per metre) and value, adds
|
||||
`FREIGHT_PRODUCTION_DAYS` to Jadlog's time, re-quotes the cart when it
|
||||
changes, and quotes again at approval from the priced items. It refuses to
|
||||
start without the credentials and the weights, which have no default. The
|
||||
production stack now takes the Jadlog settings, so the probe runs from the
|
||||
worker's console. Cubed weight is not computed: the client's box sizes will
|
||||
tell whether it is needed.
|
||||
- `[~]` 1.3 — Idempotent Tiny/Olist order creation with order-number traceability.
|
||||
Confirm endpoints, tag behaviour and rate limits first.
|
||||
**Groundwork (2026-09-24), API v3 by decision:** OAuth2 against Tiny's
|
||||
|
||||
@@ -4,11 +4,13 @@ This proves the manual's contract only; app.jadlog_probe must still confirm
|
||||
it on the client's account. Runs where httpx is installed.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import httpx
|
||||
|
||||
from app.jadlog import QUOTE_URL, JadlogError, JadlogQuotes
|
||||
from app.jadlog import QUOTE_URL, JadlogError, JadlogFreight, JadlogQuotes
|
||||
from app.jadlog_probe import run
|
||||
|
||||
CNPJ = '11.222.333/0001-81'
|
||||
@@ -66,6 +68,44 @@ class JadlogQuoteTest(unittest.TestCase):
|
||||
JadlogQuotes(token='tok', cnpj='123')
|
||||
|
||||
|
||||
class JadlogFreightTest(unittest.TestCase):
|
||||
WEIGHTS = {'JADLOG_PESO_BASE_KG': '0.2', 'JADLOG_PESO_POR_METRO_KG': '0.15', 'FREIGHT_PRODUCTION_DAYS': '2'}
|
||||
|
||||
def freight(self, handler, **env):
|
||||
with mock.patch.dict(os.environ, {**self.WEIGHTS, **env}):
|
||||
return JadlogFreight(client(handler))
|
||||
|
||||
def test_package_weight_value_and_days(self):
|
||||
seen = []
|
||||
|
||||
def handler(request):
|
||||
seen.append(json.loads(request.content)['frete'][0])
|
||||
return httpx.Response(200, json={'frete': [{'vltotal': 18.4, 'prazo': 3}]})
|
||||
|
||||
quote = self.freight(handler).quote('jadlog', '01310100', '2.8', 6972)
|
||||
# 0.2 kg of packaging plus 0.15 kg for each of the 2.8 billed metres.
|
||||
self.assertEqual((seen[0]['peso'], seen[0]['vldeclarado'], seen[0]['cepdes']), (0.62, 69.72, '01310100'))
|
||||
self.assertEqual((quote['total_cents'], quote['days'], quote['service']), (1840, 5, 'jadlog'))
|
||||
|
||||
def test_pickup_is_free_and_bad_requests_are_refused(self):
|
||||
freight = self.freight(lambda request: httpx.Response(500))
|
||||
self.assertEqual(freight.quote('pickup', '')['total_cents'], 0)
|
||||
for args in (('mock-standard', '01310100', '1'), ('jadlog', '0131', '1'), ('jadlog', '01310100', None)):
|
||||
with self.subTest(args=args), self.assertRaises(ValueError):
|
||||
freight.quote(*args)
|
||||
|
||||
def test_a_jadlog_failure_is_a_clear_refusal(self):
|
||||
freight = self.freight(lambda request: httpx.Response(200, json={'frete': [{}], 'error': {'id': -1, 'descricao': 'CEP destino invalido'}}))
|
||||
with self.assertRaisesRegex(ValueError, 'CEP destino invalido'):
|
||||
freight.quote('jadlog', '01310100', '1', 100)
|
||||
|
||||
def test_the_weight_has_no_default(self):
|
||||
with mock.patch.dict(os.environ, {}, clear=True), self.assertRaises(KeyError):
|
||||
JadlogFreight(client(lambda request: httpx.Response(200)))
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.freight(lambda request: httpx.Response(200), JADLOG_PESO_POR_METRO_KG='0')
|
||||
|
||||
|
||||
class JadlogProbeTest(unittest.TestCase):
|
||||
def test_bearer_is_tried_once_after_a_401_and_kept(self):
|
||||
headers = []
|
||||
|
||||
@@ -81,7 +81,8 @@
|
||||
const rows = approved.items.map(item => linha(
|
||||
(MODOS[item.mode]?.tit || item.mode)+' · '+fmtM(Number(item.billed_metres))+' m', rs(item.total_cents/100)));
|
||||
const pickup = approved.freight.service === 'pickup';
|
||||
rows.push(linha(pickup ? 'Retirada em Franca' : 'Frete',
|
||||
const prazo = approved.freight.days ? ' · até '+approved.freight.days+' dias úteis' : '';
|
||||
rows.push(linha(pickup ? 'Retirada em Franca' : 'Frete'+prazo,
|
||||
approved.freight.total_cents ? rs(approved.freight.total_cents/100) : 'Grátis'));
|
||||
rows.push(linha('Total', rs(approved.total_cents/100), 'tot'));
|
||||
resumo.replaceChildren(node('h4','Resumo do pedido'), ...rows);
|
||||
@@ -96,17 +97,40 @@
|
||||
const session=await ready;
|
||||
return window.dtfUpload(file,{api,progress:message,scope:session.cart_scope});
|
||||
}
|
||||
// The cart's package: billed metres and value. The charged freight is
|
||||
// quoted again by the server from the approved items.
|
||||
const pacote = () => {
|
||||
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
return {metres:items.reduce((t,it)=>t+(it.cob||0),0), cents:Math.round(items.reduce((t,it)=>t+(it.total||0),0)*100)};
|
||||
};
|
||||
let freightQuoted = null;
|
||||
window.dtfFreight = async () => {
|
||||
const cep = entrega.cep;
|
||||
const service = (await ready).freight_service;
|
||||
if (!service) {
|
||||
$('cepMsg').textContent = 'A entrega ainda não está disponível. Escolha a retirada em Franca.';
|
||||
return;
|
||||
}
|
||||
const {metres, cents} = pacote();
|
||||
if (!metres) { $('cepMsg').textContent = 'Adicione um item ao pedido para cotar o frete.'; return; }
|
||||
$('cepMsg').textContent = 'Cotando o frete…';
|
||||
try {
|
||||
const result = await api('/freight',{service:'mock-standard',postal_code:cep});
|
||||
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.cotado = true;
|
||||
$('cepMsg').textContent = 'Frete estimado: '+rs(entrega.valor)+'.';
|
||||
freightQuoted = JSON.stringify(pacote());
|
||||
$('cepMsg').textContent = 'Frete: '+rs(entrega.valor)+
|
||||
(result.days ? ' · entrega em até '+result.days+' dias úteis.' : '.');
|
||||
pintaEntrega();
|
||||
} catch(error) { $('cepMsg').textContent = error.message; }
|
||||
};
|
||||
// A different package is a different freight: quote it again.
|
||||
window.addEventListener('dtf-cart-changed',()=>{
|
||||
if (entrega.tipo==='frete' && entrega.cotado && freightQuoted && freightQuoted!==JSON.stringify(pacote())) {
|
||||
entrega.cotado=false; freightQuoted=null; window.dtfFreight();
|
||||
}
|
||||
});
|
||||
window.dtfCheckout = async () => {
|
||||
if (busy) return;
|
||||
// The cart that was sent goes straight to its payment; a changed cart is sent again.
|
||||
@@ -135,7 +159,9 @@
|
||||
upload_id:uploads[index],...source}))},
|
||||
quality_status:item.qualityStatus,quality_acknowledged:item.qualityAcknowledged});
|
||||
}
|
||||
const content = {customer:{...cliente},items,freight:{service:entrega.tipo==='retira'?'pickup':'mock-standard',postal_code:entrega.tipo==='retira'?'':entrega.cep}};
|
||||
const service = entrega.tipo==='retira' ? 'pickup' : (await ready).freight_service;
|
||||
if (!service) throw new Error('A entrega ainda não está disponível. Escolha a retirada em Franca.');
|
||||
const content = {customer:{...cliente},items,freight:{service,postal_code:entrega.tipo==='retira'?'':entrega.cep}};
|
||||
const destination = destinoApi();
|
||||
if (destination) content.destination = destination;
|
||||
if (cartSnapshot()!==initialCart) throw new Error('O carrinho mudou durante o envio. Confira os itens e envie de novo.');
|
||||
|
||||
Reference in New Issue
Block a user