feat: check Tiny products and add a supervised order test
All checks were successful
Build and deploy / Validate source (push) Successful in 11s
Build and deploy / Integration suite on a real stack (push) Successful in 2m39s
Build and deploy / Secret scan and release gate (push) Successful in 7s
Build and deploy / Publish images (push) Successful in 1m43s
All checks were successful
Build and deploy / Validate source (push) Successful in 11s
Build and deploy / Integration suite on a real stack (push) Successful in 2m39s
Build and deploy / Secret scan and release gate (push) Successful in 7s
Build and deploy / Publish images (push) Successful in 1m43s
"Testar conexão" now also reads the four configured Tiny products and requires each to be active. app/tiny_probe.py runs from the worker console to list products, confirm the configured ids, and create one marked test order through the worker's own delivery path, proving the duplicate guard by search before a second delivery. Nothing is sent without --confirmar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
33
app/tiny.py
33
app/tiny.py
@@ -223,16 +223,35 @@ def order_payload(payload, contact_id, today=None):
|
||||
return pedido
|
||||
|
||||
|
||||
def check(auth=None, transport=None):
|
||||
def configured_product(orders, mode):
|
||||
"""The Tiny product a mode's setting points at, or a reason it cannot be used."""
|
||||
setting = PRODUCT_SETTINGS[mode]
|
||||
value = os.environ.get(setting, '')
|
||||
if not value.isdigit():
|
||||
return None, f'{setting} sem id'
|
||||
found = orders.request('GET', f'/produtos/{value}')
|
||||
if found.get('situacao') != 'A':
|
||||
return found, f"produto {value} não está ativo no Tiny ({found.get('situacao')})"
|
||||
return found, 'ok'
|
||||
|
||||
|
||||
def check(auth=None, transport=None, orders=None):
|
||||
"""Read-only proof that the connection and permissions work: one order and
|
||||
one contact listed, nothing created. Raises TinyNotConnected when there is
|
||||
no usable connection; otherwise reports each read separately."""
|
||||
orders = TinyOrders(auth=auth or TinyAuth(), transport=transport)
|
||||
one contact listed and each configured product found active, nothing
|
||||
created. Raises TinyNotConnected when there is no usable connection;
|
||||
otherwise reports each read separately."""
|
||||
orders = orders or TinyOrders(auth=auth or TinyAuth(), transport=transport)
|
||||
|
||||
def listed(path):
|
||||
orders.request('GET', path, params={'limit': 1})
|
||||
return 'ok'
|
||||
|
||||
reads = [('pedidos', lambda: listed('/pedidos')), ('contatos', lambda: listed('/contatos'))]
|
||||
reads += [(PRODUCTS[mode], lambda mode=mode: configured_product(orders, mode)[1]) for mode in PRODUCT_SETTINGS]
|
||||
results = {}
|
||||
for name, path in (('pedidos', '/pedidos'), ('contatos', '/contatos')):
|
||||
for name, read in reads:
|
||||
try:
|
||||
orders.request('GET', path, params={'limit': 1})
|
||||
results[name] = 'ok'
|
||||
results[name] = read()
|
||||
except TinyNotConnected:
|
||||
raise
|
||||
except TinyError as exc:
|
||||
|
||||
132
app/tiny_probe.py
Normal file
132
app/tiny_probe.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""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 conferir connection and the four product ids (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 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())
|
||||
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='')
|
||||
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 == '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())
|
||||
Reference in New Issue
Block a user