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
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>
154 lines
7.1 KiB
Python
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())
|