You: "With an Idempotency-Key.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | # ====================================================================== # Q: CTO: "The mobile app retries after a timeout and the customer gets charged # twice. How do you prevent it?" # # You: "With an Idempotency-Key. The client sends a unique key per payment # attempt. I store it with a unique constraint on (user, key) together with a # hash of the request body. If the same key arrives again, I return the stored # response instead of charging again. If the first call is still running, I # return 409. I also pass the same key to the PSP, so I am protected on its side # too. I do not make the PSP call inside an open DB transaction." # ====================================================================== def create_payment(request): key = request.headers.get('Idempotency-Key') if not key: return Response({'detail': 'Idempotency-Key required'}, status=400) body_hash = hashlib.sha256(request.body).hexdigest() record, created = IdempotencyRecord.objects.get_or_create( user=request.user, key=key, defaults={'request_hash': body_hash}) if not created: if record.request_hash != body_hash: return Response(status=422) # same key, different request if record.response is None: return Response(status=409) # still in progress return Response(record.response, status=record.status_code) payment = payments.charge(request.user, request.data, idempotency_key=key) record.response = PaymentSerializer(payment).data record.status_code = 201 record.save(update_fields=['response', 'status_code']) return Response(record.response, status=201) |