Files
dtf-system/tests/payment_test.py
Cauê Faleiros 275ebf72c4
All checks were successful
Build and deploy / Validate source (push) Successful in 8s
Build and deploy / Integration suite on a real stack (push) Successful in 2m22s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m34s
feat: approve priced carts at checkout so customers can pay at once
Every quote waited for an operator before it could be paid, so an order
placed at night waited for the morning. A cart the Site priced is now
approved when the quote is created, through the same server pricing the
operator's approval uses (app/quote_review.py). Orders above
QUOTE_AUTO_MAX_METRES (50 m) and items claiming a discount on art the Site
could not analyse still wait for review; the Kanban shows which quotes were
approved automatically and why the others wait.

The grade is still computed in the browser (roadmap 3.2, 3.9), so the
discount remains a customer-supplied value until the server computes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:02:56 -03:00

130 lines
6.7 KiB
Python

"""The webhook path, against a running stack.
A provider retries. It delivers out of order, twice, and late. None of that may
produce a second order or a second notification to the customer, and nothing
unsigned may produce one at all.
"""
import hashlib
import hmac
import json
import os
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from uuid import uuid4
from tests.smoke_test import BASE, Client, approved_quote, upload_bytes, item_spec, with_host
SECRET = os.environ.get('PAYMENT_WEBHOOK_SECRET', 'local-webhook-secret').encode()
def deliver(payload, expected=200, signature=None):
body = json.dumps(payload).encode()
sig = signature if signature is not None else hmac.new(SECRET, body, hashlib.sha256).hexdigest()
request = Request(BASE + '/api/payments/webhook', data=body,
headers=with_host({'Content-Type': 'application/json',
'x-payment-signature': sig}))
try:
with urlopen(request, timeout=30) as response:
assert response.status == expected, (response.status, expected)
return json.load(response)
except HTTPError as exc:
assert exc.code == expected, (exc.code, expected, exc.read().decode())
return {}
def reviewed_quote():
"""A quote an operator has approved, ready to be paid."""
customer = Client()
customer.call('/session')
uid = upload_bytes(customer, b'PAYMENT WEBHOOK TEST')
item = item_spec('file', '1.01', 0, uid)
profile = {'cnpj': '11222333000181', 'zap': '11999999999',
'mail': 'payment-' + uuid4().hex[:8] + '@example.test'}
quote = customer.call('/quotes', {'request_key': str(uuid4()), 'customer': profile,
'items': [item], 'freight': {'service': 'pickup'}})
approved = approved_quote(customer, quote, [item])
return customer, quote['id'], approved['total_cents']
def run():
customer, quote_id, total = reviewed_quote()
# Nothing unsigned creates an order, and a tampered body is not signed.
deliver({'event_id': 'unsigned-' + uuid4().hex, 'reference': quote_id,
'status': 'approved', 'amount_cents': total}, expected=403, signature='')
deliver({'event_id': 'tampered-' + uuid4().hex, 'reference': quote_id,
'status': 'approved', 'amount_cents': total}, expected=403, signature='0' * 64)
assert not customer.call('/quotes/' + quote_id)['order'], 'unsigned delivery created an order'
print('PASS: unsigned and tampered deliveries are refused and create nothing')
# Starting a PIX twice returns the same one. A card in review blocks every
# further attempt, so one quote can never be charged twice.
pix = customer.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'pix'}})
assert customer.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'pix'}})['id'] == pix['id']
customer.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'card'}}, expected=422)
card = {'type': 'card', 'token': 'tok-1', 'payment_method_id': 'visa', 'installments': 1}
customer.call('/payments/intent', {'quote_id': quote_id, 'method': card})
customer.call('/payments/intent', {'quote_id': quote_id, 'method': {**card, 'token': 'tok-2'}}, expected=409)
customer.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'pix'}}, expected=409)
stranger = Client()
stranger.call('/session')
stranger.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'pix'}}, expected=404)
print('PASS: payment start is idempotent for PIX and refuses a second charge')
# An approved payment for the wrong amount must not become an order.
deliver({'event_id': 'short-' + uuid4().hex, 'reference': quote_id,
'status': 'approved', 'amount_cents': total - 100})
assert not customer.call('/quotes/' + quote_id)['order'], 'underpayment created an order'
deliver({'event_id': 'missing-amount-' + uuid4().hex, 'reference': quote_id,
'status': 'approved'})
assert not customer.call('/quotes/' + quote_id)['order'], 'missing paid amount created an order'
deliver({'event_id': 'invalid-amount-' + uuid4().hex, 'reference': quote_id,
'status': 'approved', 'amount_cents': str(total)})
assert not customer.call('/quotes/' + quote_id)['order'], 'non-integer paid amount created an order'
print('PASS: a missing, invalid or mismatched paid amount is refused')
# The real thing, then the same delivery again, and a second event for the
# same quote: a provider does all three.
event = 'paid-' + uuid4().hex
payload = {'event_id': event, 'reference': quote_id, 'status': 'approved',
'amount_cents': total}
first = deliver(payload)
assert first['status'] == 'applied', first
order = customer.call('/quotes/' + quote_id)['order']
assert order, 'approved payment did not create an order'
again = deliver(payload)
assert again['status'] == 'duplicate', again
later = deliver({**payload, 'event_id': 'retry-' + uuid4().hex})
assert 'already existed' in later.get('outcome', ''), later
assert customer.call('/quotes/' + quote_id)['order']['id'] == order['id'], 'a second order appeared'
print('PASS: one order from a repeated and re-sent approval')
assert customer.call('/orders/dev-paid', {'quote_id': quote_id})['id'] == order['id']
other = Client()
other.call('/session')
other.call('/orders/dev-paid', {'quote_id': quote_id}, expected=404)
print('PASS: another customer cannot retrieve the paid order by quote id')
# The customer is told once, not once per delivery.
board = Client()
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)}'
assert {e['provider'] for e in paid} == {'tiny', 'whatsapp'}, paid
print('PASS: exactly one notification per provider for the order')
# A payment that was never reviewed, and one for something that is not a quote.
deliver({'event_id': 'nonsense-' + uuid4().hex, 'reference': 'not-a-uuid',
'status': 'approved', 'amount_cents': 100})
deliver({'event_id': 'missing-' + uuid4().hex, 'reference': str(uuid4()),
'status': 'approved', 'amount_cents': 100})
deliver({'event_id': 'pending-' + uuid4().hex, 'reference': quote_id,
'status': 'pending', 'amount_cents': total})
print('PASS: unknown references and non-approved statuses are recorded without acting')
if __name__ == '__main__':
run()