"""Jadlog freight quotes (Embarcador API, "Simulador de Frete"). Written from Jadlog's API manual v2.3 (August 2025) and exercised only against a fake HTTP transport until app.jadlog_probe has run on the client's account. A quote is read-only: it creates no shipment and costs nothing, so 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. """ import os from decimal import ROUND_HALF_UP, Decimal import httpx QUOTE_URL = 'https://www.jadlog.com.br/embarcador/api/frete/valor' PACKAGE = 3 ORIGIN = '14402310' class JadlogError(Exception): pass def digits(value): return ''.join(ch for ch in str(value or '') if ch.isdigit()) class JadlogQuotes: def __init__(self, token=None, cnpj=None, conta=None, contrato=None, origin=None, modalidade=None, transport=None): self.token = (token or os.environ.get('JADLOG_TOKEN', '')).strip() self.cnpj = digits(cnpj or os.environ.get('JADLOG_CNPJ')) self.conta = digits(conta if conta is not None else os.environ.get('JADLOG_CONTA')) # Only when Jadlog issued one; the manual says to send null otherwise. self.contrato = (contrato if contrato is not None else os.environ.get('JADLOG_CONTRATO', '')).strip() or None self.origin = digits(origin or os.environ.get('JADLOG_ORIGEM_CEP') or ORIGIN) self.modalidade = int(modalidade or os.environ.get('JADLOG_MODALIDADE') or PACKAGE) if not self.token or len(self.cnpj) != 14: raise RuntimeError('Jadlog needs JADLOG_TOKEN and a 14-digit JADLOG_CNPJ') self.http = httpx.Client(timeout=20, transport=transport) def item(self, postal_code, weight_kg, declared_cents): return {'cepori': self.origin, 'cepdes': digits(postal_code), 'frap': 'N', 'peso': float(weight_kg), 'cnpj': self.cnpj, 'conta': self.conta, 'contrato': self.contrato, 'modalidade': self.modalidade, 'tpentrega': 'D', 'tpseguro': 'N', 'vldeclarado': declared_cents / 100, 'vlcoleta': 0} def quote(self, postal_code, weight_kg, declared_cents): """Price in centavos and delivery days for one package to one CEP.""" response = self.http.post(QUOTE_URL, json={'frete': [self.item(postal_code, weight_kg, declared_cents)]}, headers={'Authorization': self.token}) if response.status_code == 401: raise JadlogError('Jadlog refused the token (HTTP 401)') try: data = response.json() except ValueError: raise JadlogError(f'HTTP {response.status_code}: the response is not JSON') from None items = data.get('frete') or [] # The manual names the group "erro" in its tables and "error" in its # examples, at the top level and per item. problem = data.get('error') or data.get('erro') if not problem and items: problem = items[0].get('erro') or items[0].get('error') if problem: raise JadlogError(problem.get('descricao') or str(problem) if isinstance(problem, dict) else str(problem)) if response.status_code >= 400 or not items or items[0].get('vltotal') is None: raise JadlogError(f'HTTP {response.status_code}: no freight value in the response') 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]}