Files
dtf-system/app/tiny_probe.py
Cauê Faleiros aec5b1d054
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 2m5s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 1m45s
feat: send order situações to Tiny for the client's WhatsApp notices
The client already sends WhatsApp notices from Tiny's order situação
(Tiny webhook -> middleware -> n8n). With TINY_STATUS_UPDATES on, a paid
order is set to "Aprovada" once and a finished pickup order to "Pronto
para envio"; pickup orders carry the client's pickup forma de envio
(TINY_FORMA_ENVIO_RETIRADA). The ready event now carries the order and
the Tiny id from the sale's receipt. Off by default until go-live, when
n8n stops sending the DTFIMP designer message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:17:00 -03:00

154 lines
7.1 KiB
Python

"""Supervised checks against the client's real Tiny, run by hand from a
container console (docker exec, or Portainer > Containers > worker > Console).
Tiny has no sandbox, so this is how the adapter is proven on the real account.
python -m app.tiny_probe produtos [termo] list active products (read-only)
python -m app.tiny_probe formas-envio [termo] list formas de envio, to find pickup (read-only)
python -m app.tiny_probe conferir connection, the four product ids and pickup (read-only)
python -m app.tiny_probe pedido --cnpj ... --email ... --celular ... [--modo file]
show the test order; --confirmar creates it
The test order goes through TinyOrders.deliver, exactly as the worker sends a
paid order. The duplicate guard is then proven read-only first (the order must
be found by its purchase-order number) and only then by a second delivery,
which must return the existing order. If the search cannot find the order, the
second delivery is not attempted: it would create a duplicate. Cancel the test
order in Olist afterwards.
"""
import argparse
import json
import sys
import time
from datetime import datetime
from .core.pricing import TIERS
from .core.secrets import load as load_secret_files
from . import tiny
FIND_ATTEMPTS = 4
FIND_WAIT_SECONDS = 5
def test_order(cnpj, email, celular, mode, now=None):
number = 'TESTE-' + (now or datetime.now(tiny.BRASILIA)).strftime('%Y%m%d%H%M')
unit = TIERS[mode][0][1]
return {'order_id': 'teste-integracao', 'number': number, 'event': 'payment_approved',
'order': {'customer': {'cnpj': cnpj, 'mail': email, 'zap': celular},
'items': [{'mode': mode, 'grade': 100, 'billed_metres': '1', 'unit_cents': unit}],
'freight': {'service': 'pickup', 'total_cents': 0},
'destination': None, 'total_cents': unit}}
def produtos(orders, termo, out):
params = {'situacao': 'A', 'limit': 100}
if termo:
params['nome'] = termo
found = orders.request('GET', '/produtos', params=params).get('itens') or []
for item in found:
preco = (item.get('precos') or {}).get('preco')
out(f"{item['id']}\t{item.get('sku') or '-'}\t{item.get('descricao')}\tR$ {preco}")
out(f'{len(found)} produto(s) ativo(s)' + (f' com "{termo}"' if termo else '') + '.')
def formas_envio(orders, termo, out):
params = {'limit': 100}
if termo:
params['nome'] = termo
found = orders.request('GET', '/formas-envio', params=params).get('itens') or []
for item in found:
out(f"{item.get('id')}\t{item.get('tipo')}\t{item.get('nome')}")
out(f'{len(found)} forma(s) de envio' + (f' com "{termo}"' if termo else '') +
'. A retirada é a do tipo 6 (Customizado) que a equipe usa para retirar pessoalmente.')
def conferir(orders, out):
ok = True
for mode, setting in tiny.PRODUCT_SETTINGS.items():
product, result = tiny.configured_product(orders, mode)
ok &= result == 'ok'
described = f"{product.get('sku') or '-'} · {product.get('descricao')}" if product else ''
out(f'{setting} ({tiny.PRODUCTS[mode]}): {result} {described}'.rstrip())
method, result = tiny.configured_pickup(orders)
ok &= result == 'ok'
out(f"{tiny.PICKUP_SETTING} (retirada): {result} {method.get('nome') if method else ''}".rstrip())
out(f"{tiny.STATUS_UPDATES_SETTING}: {'ligado' if tiny.status_updates() else 'desligado'}")
for name, result in tiny.check(orders=orders).items():
if name in ('pedidos', 'contatos'):
ok &= result == 'ok'
out(f'{name}: {result}')
out('Tudo conferido.' if ok else 'Há pendências acima.')
return ok
def pedido(orders, payload, confirm, out, wait=time.sleep):
cnpj = payload['order']['customer']['cnpj']
contacts = orders.request('GET', '/contatos', params={'cpfCnpj': cnpj, 'limit': 5}).get('itens') or []
known = [c for c in contacts if ''.join(ch for ch in str(c.get('cpfCnpj') or '') if ch.isdigit()) == cnpj]
out(f"Contato {cnpj}: " + (f"já existe no Tiny (id {known[0]['id']})" if known
else 'não existe; será criado com o e-mail como nome'))
out('Pedido que será enviado:')
out(json.dumps(tiny.order_payload(payload, known[0]['id'] if known else 0), ensure_ascii=False, indent=2))
if not confirm:
out('Nada foi criado. Repita com --confirmar para criar este pedido no Tiny.')
return None
first = orders.deliver('teste-integracao', payload)
out(f"1º envio: {first['status']} · Tiny id {first['tiny_id']} · nº {first['tiny_number']}")
for attempt in range(FIND_ATTEMPTS):
found = orders.find(payload)
if found:
break
if attempt < FIND_ATTEMPTS - 1:
wait(FIND_WAIT_SECONDS)
else:
out(f'FALHA: a busca por {tiny.purchase_order(payload["number"])} não encontrou o pedido. '
'Um reenvio criaria um duplicado; o 2º envio não foi feito. Cancele o pedido no Olist.')
return False
second = orders.deliver('teste-integracao', payload)
out(f"2º envio: {second['status']} · Tiny id {second['tiny_id']}")
passed = second['status'] == 'already-created' and second['tiny_id'] == first['tiny_id']
out(('OK: o reenvio encontrou o mesmo pedido.' if passed else 'FALHA: o reenvio não devolveu o mesmo pedido.')
+ f" Cancele o pedido nº {first['tiny_number']} no Olist.")
return passed
def main(argv=None, orders=None, out=print):
parser = argparse.ArgumentParser(prog='python -m app.tiny_probe')
commands = parser.add_subparsers(dest='command', required=True)
listing = commands.add_parser('produtos')
listing.add_argument('termo', nargs='?', default='')
methods = commands.add_parser('formas-envio')
methods.add_argument('termo', nargs='?', default='')
commands.add_parser('conferir')
order = commands.add_parser('pedido')
order.add_argument('--cnpj', required=True)
order.add_argument('--email', required=True)
order.add_argument('--celular', required=True)
order.add_argument('--modo', choices=sorted(tiny.PRODUCT_SETTINGS), default='file')
order.add_argument('--confirmar', action='store_true')
args = parser.parse_args(argv)
if orders is None:
load_secret_files()
orders = tiny.TinyOrders(auth=tiny.TinyAuth())
try:
if args.command == 'produtos':
produtos(orders, args.termo, out)
return 0
if args.command == 'formas-envio':
formas_envio(orders, args.termo, out)
return 0
if args.command == 'conferir':
return 0 if conferir(orders, out) else 1
cnpj = ''.join(ch for ch in args.cnpj if ch.isdigit())
if len(cnpj) != 14:
out('Informe um CNPJ com 14 dígitos.')
return 2
payload = test_order(cnpj, args.email, args.celular, args.modo)
return 0 if pedido(orders, payload, args.confirmar, out) is not False else 1
except tiny.TinyError as exc:
out(f'Tiny: {exc}')
return 1
if __name__ == '__main__':
sys.exit(main())