feat: PIX codes expire after 30 minutes, with a countdown
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 2m50s
Build and deploy / Secret scan and release gate (push) Successful in 8s
Build and deploy / Publish images (push) Successful in 1m54s
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 2m50s
Build and deploy / Secret scan and release gate (push) Successful in 8s
Build and deploy / Publish images (push) Successful in 1m54s
A PIX is created with a 30-minute date_of_expiration, and the PIX page counts down to it. When it runs out the page says the code expired and offers a new one. The API keeps one open code per quote: the same code until it expires, then exactly one new attempt (serialised per quote, with its own idempotency key), the old one marked expired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
from uuid import uuid4
|
||||
|
||||
from app.core import db
|
||||
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()
|
||||
@@ -60,7 +61,22 @@ def run():
|
||||
# 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 pix['expires_at']
|
||||
assert customer.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'pix'}})['id'] == pix['id']
|
||||
# Once the code has expired, asking again opens a new one, and only one.
|
||||
with db.connect() as c:
|
||||
c.execute('''UPDATE dtf_local.payment_intents
|
||||
SET response=jsonb_set(response,'{expires_at}',to_jsonb((now()-interval '1 minute')::text))
|
||||
WHERE provider_payment_id=%s''', (pix['id'],))
|
||||
renewed = customer.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'pix'}})
|
||||
assert renewed['id'] != pix['id'], 'an expired PIX was offered again'
|
||||
assert customer.call('/payments/intent', {'quote_id': quote_id, 'method': {'type': 'pix'}})['id'] == renewed['id']
|
||||
with db.connect() as c:
|
||||
statuses = {r['provider_payment_id']: r['status'] for r in c.execute(
|
||||
"SELECT provider_payment_id,status FROM dtf_local.payment_intents WHERE quote_id=%s AND method='pix'",
|
||||
(quote_id,)).fetchall()}
|
||||
assert statuses == {pix['id']: 'expired', renewed['id']: 'pending'}, statuses
|
||||
print('PASS: a PIX code expires after 30 minutes and is replaced by exactly one new code')
|
||||
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})
|
||||
|
||||
Reference in New Issue
Block a user