feat: undo mistaken moves, numbered pagination, and quieter Kanban messages
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m33s
Build and deploy / Secret scan and release gate (push) Successful in 7s
Build and deploy / Publish images (push) Successful in 1m48s
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m33s
Build and deploy / Secret scan and release gate (push) Successful in 7s
Build and deploy / Publish images (push) Successful in 1m48s
Moves: an order can go back one stage (BACK in app/runtime.py) with an internal reason, flagged in the history as movements.back. The customer is not notified and approved finals stay; "production started" and "ready" are now enqueued once per order, so undoing and redoing a move sends nothing twice. Dragging only goes forward and highlights the allowed column. Move errors are in Portuguese. Lists: the send log, payments (open, resolved as history, all) and quotes are paged on the server with a total, 20 rows by default (10/20/50/100), first/previous/page/next/last. The send log filters by destination, status, event and order. Older finished orders load on demand. The board no longer carries the send log or payment rows, only the open-payment count. Kanban: Pagamentos and Integrações are separate tabs; messages are brief, bottom notifications that clear themselves; wording is shorter. Full CI integration sequence passes locally, with new checks for undo, paging and filters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,15 @@ try{
|
||||
await kanban.click(`[data-order="${oid}"] [data-move="${state}"]`);
|
||||
await waitFor(()=>kanban.eval(`board.orders.find(o=>o.id==='${oid}').state==='${state}'`),'transition '+state);
|
||||
}
|
||||
// Undo a mistaken move from the panel: one stage back with an internal reason.
|
||||
await waitFor(()=>kanban.eval(`!!document.querySelector('[data-order="${oid}"] [data-back="imp"]')`),'back button');
|
||||
await kanban.click(`[data-order="${oid}"] [data-back="imp"]`);
|
||||
await kanban.fill(`[data-order="${oid}"] form.reason input`,'Movido por engano (teste)');
|
||||
await kanban.eval(`document.querySelector('[data-order="${oid}"] form.reason input').closest('form').requestSubmit()`);
|
||||
await waitFor(()=>kanban.eval(`board.orders.find(o=>o.id==='${oid}').state==='imp'`),'undo to Imprimindo');
|
||||
await waitFor(()=>kanban.eval(`!!document.querySelector('[data-order="${oid}"] [data-move="fin"]')`),'move to Finalizado again');
|
||||
await kanban.click(`[data-order="${oid}"] [data-move="fin"]`);
|
||||
await waitFor(()=>kanban.eval(`board.orders.find(o=>o.id==='${oid}').state==='fin'`),'finished again');
|
||||
// Reload proves the board is persisted on the backend.
|
||||
await kanban.call('Page.reload');
|
||||
await waitFor(()=>kanban.eval(`typeof board!=='undefined' && !!board && board.orders.some(o=>o.id==='${oid}'&&o.state==='fin')`),'persisted board');
|
||||
|
||||
@@ -109,7 +109,7 @@ def run():
|
||||
|
||||
# The customer is told once, not once per delivery.
|
||||
board = Client()
|
||||
events = board.call('/operator/board', operator=True)['events']
|
||||
events = board.call('/operator/events?order=' + str(order['number']), operator=True)['events']
|
||||
paid = [e for e in events if e['payload'].get('order_id') == order['id']
|
||||
and e['payload'].get('event') == 'payment_approved']
|
||||
assert len(paid) == 2, f'expected one tiny and one whatsapp event, got {len(paid)}'
|
||||
|
||||
@@ -141,17 +141,43 @@ def run():
|
||||
outcome = deliver({'event_id': event_id, 'reference': quote_id,
|
||||
'status': 'approved', 'amount_cents': total - 1})
|
||||
assert outcome['outcome'].startswith('refused'), outcome
|
||||
issues = client.call('/operator/board', operator=True)['payment_issues']
|
||||
issues = client.call('/operator/payment-events?state=open&limit=100', operator=True)['issues']
|
||||
issue = next(i for i in issues if i['event_id'] == event_id)
|
||||
assert client.call('/operator/board', operator=True)['payment_issues_total'] >= 1
|
||||
client.call('/operator/payment-events/' + issue['id'] + '/resolve', {'note': 'no'},
|
||||
operator=True, expected=422)
|
||||
client.call('/operator/payment-events/' + issue['id'] + '/resolve',
|
||||
{'note': 'Local test: refunded the underpayment'}, operator=True)
|
||||
client.call('/operator/payment-events/' + issue['id'] + '/resolve',
|
||||
{'note': 'Local test: second resolution'}, operator=True, expected=404)
|
||||
issues = client.call('/operator/board', operator=True)['payment_issues']
|
||||
issues = client.call('/operator/payment-events?state=open&limit=100', operator=True)['issues']
|
||||
assert not any(i['event_id'] == event_id for i in issues)
|
||||
print('PASS: refused paid notification is listed until an operator resolves it')
|
||||
resolved_page = client.call('/operator/payment-events?state=resolved&limit=100', operator=True)
|
||||
assert resolved_page['total'] >= 1
|
||||
resolved = resolved_page['issues']
|
||||
done = next(i for i in resolved if i['event_id'] == event_id)
|
||||
assert done['resolution'] == 'Local test: refunded the underpayment' and done['resolved_by']
|
||||
print('PASS: refused paid notification is listed until an operator resolves it, then kept as history')
|
||||
|
||||
# The send log pages by id and filters by destination, status, event and order.
|
||||
page = client.call('/operator/events?limit=2', operator=True)
|
||||
assert len(page['events']) == 2 and page['total'] > 2
|
||||
second = client.call('/operator/events?limit=2&offset=2', operator=True)
|
||||
assert all(e['id'] < page['events'][-1]['id'] for e in second['events'])
|
||||
assert second['total'] == page['total']
|
||||
tiny = client.call('/operator/events?provider=tiny&event=payment_approved&status=delivered&limit=100', operator=True)
|
||||
assert tiny['events'] and all(e['provider'] == 'tiny' and e['payload']['event'] == 'payment_approved'
|
||||
and e['delivered_at'] for e in tiny['events'])
|
||||
client.call('/operator/events?provider=email', operator=True, expected=422)
|
||||
client.call('/operator/events?limit=500', operator=True, expected=422)
|
||||
quotes = client.call('/operator/quotes?kind=approved&limit=1&offset=0', operator=True)
|
||||
assert 'total' in quotes and len(quotes['quotes']) <= 1
|
||||
finished = client.call('/operator/orders/finished?limit=1', operator=True)
|
||||
if finished['orders']:
|
||||
last = finished['orders'][-1]
|
||||
client.call('/operator/orders/finished?before_created_at=' + last['created_at'].replace('+', '%2B')
|
||||
+ '&before_id=' + last['id'], operator=True)
|
||||
print('PASS: send log, payment history and finished orders page and filter')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -185,10 +185,21 @@ def run():
|
||||
version+=1;assert moved['version']==version
|
||||
client.call('/operator/orders/'+oid+'/move',{'state':'rec','version':0},operator=True,expected=409)
|
||||
assert len(client.call('/operator/orders/'+oid+'/history',operator=True))==6
|
||||
# A mistaken move can be undone one stage at a time, with an internal
|
||||
# reason. The customer is not told again: going back and forward once more
|
||||
# adds no messages (the outbox count below stays 8).
|
||||
client.call('/operator/orders/'+oid+'/move',{'state':'imp','version':version},operator=True,expected=422)
|
||||
client.call('/operator/orders/'+oid+'/move',{'state':'tra','version':version},operator=True,expected=409)
|
||||
back=client.call('/operator/orders/'+oid+'/move',{'state':'imp','version':version,'reason':'Movido por engano'},operator=True)
|
||||
assert back['state']=='imp';version+=1
|
||||
client.call('/operator/orders/'+oid+'/move',{'state':'fin','version':version},operator=True);version+=1
|
||||
history=client.call('/operator/orders/'+oid+'/history',operator=True)
|
||||
assert len(history)==8 and history[-2]['back'] and history[-2]['reason']=='Movido por engano' and not history[-1]['back']
|
||||
print('PASS: a mistaken move is undone one stage back with a reason, without messaging the customer again')
|
||||
print('PASS: all modes, authoritative review/prices/freight, tamper rejection, concurrent payment idempotency, transitions and history')
|
||||
deadline=time.monotonic()+30
|
||||
while time.monotonic()<deadline:
|
||||
events=[e for e in client.call('/operator/board',operator=True)['events'] if e['payload']['order_id']==oid]
|
||||
events=[e for e in client.call('/operator/events?order='+str(order['number']),operator=True)['events'] if e['payload']['order_id']==oid]
|
||||
if len(events)==8 and all(e['delivered_at'] and e['receipt'] for e in events):break
|
||||
time.sleep(1)
|
||||
else:raise AssertionError('Mock outbox did not drain')
|
||||
|
||||
Reference in New Issue
Block a user