feat: let operators create test orders from approved quotes
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 2m28s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 47s

Without an approved card payment there was no way to try the board, the
files and the print flow in production. "Criar pedido de teste" on an
approved quote creates the order through the same path as a paid one, marked
TESTE on its card and panel and audited; neither it nor its stage moves
queue anything for Tiny or WhatsApp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-29 14:50:40 -03:00
parent 64c1260a9f
commit 9e69c48d2d
5 changed files with 62 additions and 5 deletions

View File

@@ -14,7 +14,7 @@ from ..core.auth import (COOKIE_SECURE, DUMMY_PASSWORD_HASH, audit, client_ip, o
login_failed, password_matches, throttle)
from ..core.models import Move, OperatorLogin, Resolution, Review
from ..printjobs import queue as queue_print_files
from .. import quote_review
from .. import payments, quote_review
from .. import tiny
from ..runtime import (BACK, BOARD_FINISHED_LIMIT, BOARD_QUOTE_LIMIT, ENVIRONMENT, STATES, TRANSITIONS, payment,
enqueue, freight, quote_view, storage)
@@ -206,6 +206,24 @@ def approve(uid: UUID, body: Review, user=Depends(operator)):
raise HTTPException(404, 'Quote not found')
return quote_review.approve(c, row, body.items, user)
@router.post('/api/operator/quotes/{uid}/test-order')
def test_order(uid: UUID, user=Depends(operator)):
"""An order for an approved quote without payment, to try the Kanban,
files and print flow in production. Marked TEST on the board, never sent
to Tiny or WhatsApp, and audited."""
with db.connect() as c:
try:
quote = payments.approved_quote(c, uid)
except payments.PaymentRefused as refusal:
raise HTTPException(404 if 'not found' in str(refusal) else 409, str(refusal))
receipt = {'provider': payments.TEST_PROVIDER, 'id': f'teste-{uid}', 'status': 'paid',
'total_cents': quote['approved']['total_cents'], 'operator': user}
order, created = payments.create_order(c, quote, receipt)
if created:
audit('test_order_created', order=str(order['id']), operator=user)
return order
@router.post('/api/operator/orders/{uid}/move')
def move(uid: UUID, body: Move, user=Depends(operator)):
with db.connect() as c:
@@ -233,7 +251,7 @@ def move(uid: UUID, body: Move, user=Depends(operator)):
(uid,row['state'],body.state,user,body.reason,back))
changed = c.execute('UPDATE dtf_local.orders SET state=%s, version=version+1, updated_at=now() WHERE id=%s RETURNING *', (body.state,uid)).fetchone()
events = {'imp':'production_started','cor':'correction_needed','fin':'ready'}
if body.state in events and not back:
if body.state in events and not back and not payments.is_test(row):
# "Production started" and "ready" reach the customer once per order,
# even if a mistaken move is undone and made again. Each correction
# is a new request, so it keeps one message per movement.

View File

@@ -41,6 +41,14 @@ def approved_quote(c, quote_id, owner=None):
return row
TEST_PROVIDER = 'teste'
def is_test(order):
"""An operator's test order: it goes through production and notifies no one."""
return (order.get('payment') or {}).get('provider') == TEST_PROVIDER
def create_order(c, quote, payment):
"""Create the order for a reviewed quote, or return the one already there.
@@ -61,6 +69,8 @@ def create_order(c, quote, payment):
'INSERT INTO dtf_local.orders(id,quote_id,owner,snapshot,payment) VALUES(%s,%s,%s,%s,%s) RETURNING *',
(uuid4(), quote['id'], quote['owner'], Jsonb(approved), Jsonb(payment))).fetchone()
queue_print_files(c, order['id'], len(approved['items']))
if is_test(order):
return order, True
for provider in ('tiny', 'whatsapp'):
enqueue(c, f"{order['id']}:paid:{provider}", provider,
{'order_id': str(order['id']), 'number': order['number'],