5

I am using Stripe in my Django application. I have the following test case: incorrect_cvc leads to an card_error. Now when correcting the CVC and using 4242 4242 4242 4242 what I except is a successful charge. However what I get pack is the following error message:

Request req_auTSTGSGoUVNUa: Keys for idempotent requests can only be used with the same parameters they were first used with. Try using a key other than 'k1qjchgqjw' if you meant to execute a different request.

I am not aware of which parameters I changed. But I think it's not the idea that the checkout process basically doesn't work anymore after an card_error. Does anyone understand which parameters I "changed" that leads to this error message?

def checkout_page(request):
"""
* Check if session and ReservedItem exist.
* Generate order_item dict for every ReservedItem entry, that belongs
  to order_reference.
If request.method is 'POST':
    * Check if ticket reservation is still valid.
    * Create entries in models OrderItem, Order & ReservedItem.
"""
session_order_reference = request.session.get('order_reference')
if request.session.get('order_reference'):
    reserved_items = ReservedItem.objects.filter(
        order_reference=session_order_reference
    )
    if not reserved_items:
        return redirect('website:index')
else:
    return redirect('website:index')

taxes_dict = {}
total_gross = total_tax_amount = 0
order_items_list = []
for item in reserved_items:
    event = item.ticket.event
    timestamp_of_reservation = item.created
    total_gross += item.subtotal
    order_item = {
        'ticket': item.ticket,
        'ticket_name': item.ticket.name,
        'quantity': item.quantity,
        'subtotal': item.subtotal,
        'type': OrderType.ORDER,
    }
    total_tax_amount += add_tax(
        item=item,
        taxes_dict=taxes_dict,
        order_item=order_item,
    )
    order_items_list.append(dict(order_item))
total_net = total_gross - total_tax_amount  # TODO Marc: Calculate in add_vat func?

if request.method == 'POST':

    # TODO Marc: Should live in forms.py or just models?
    reservation_expired_redirect = check_if_reservation_expired(
        request=request,
        timestamp_of_reservation=timestamp_of_reservation,
        organizer=event.organizer.slug,
        event=event.slug,
    )
    if reservation_expired_redirect:
        return reservation_expired_redirect

    # TODO Marc: Should live in forms.py or just models?
    ticket_is_on_sale = check_if_ticket_is_on_sale(
        order_items_list=order_items_list,
        request=request,
        organizer=event.organizer.slug,
        event=event.slug,
    )
    if ticket_is_on_sale:
        return ticket_is_on_sale

    billing = BillingForm(request.POST, prefix='billing')
    order = OrderForm(request.POST, prefix='order')
    if order.is_valid() and billing.is_valid():

        # Charge via Stripe
        stripe.api_key = "ABC" # TODO Marc: Change to env
        token = request.POST.get('stripeToken')
        # https://stripe.com/docs/api#error_handling

        paid = False
        try:
            # Compare with transactions > models copy.py > class ChargeManager(models.Manager):
            # Use Stripe's library to make requests...
            total_gross_amount_in_smallest_unit = smallest_currency_unit(total_gross, 'eur') #TODO Marc: Replace eur
            charge = stripe.Charge.create(
                amount=total_gross_amount_in_smallest_unit, # TODO Marc > https://stripe.com/docs/currencies#zero-decimal
                application_fee=100, # TODO Marc: Which currency?
                currency='eur',  # TODO Marc
                source=token,
                stripe_account="ABC",  # TODO Marc: Replace with organizer stripe account
                idempotency_key=session_order_reference,
            )

            new_charge_obj = Charge.objects.create(
                amount=charge.amount,
                charge_id=charge.id,
                livemode=charge.livemode,
                paid=charge.paid,
                refunded=charge.refunded,
                currency=charge.currency,
                failure_code=charge.failure_code,
                failure_message=charge.failure_message,
                fraud_details=charge.fraud_details,
                outcome=charge.outcome,
                status=charge.status,
                application_fee=charge.application_fee,
                captured=charge.captured,
                created=charge.created,
                # TODO Marc: Add refunds:
                # amount_refunded=charge.amount_refunded,
                # etc.
            )
            application_fee = stripe.ApplicationFee.retrieve(charge.application_fee)
            Fee.objects.create(
                fee_id=application_fee.id,
                livemode=application_fee.livemode,
                currency=application_fee.currency,
                amount=application_fee.amount,
                charge=new_charge_obj,
                # TODO Marc: Add refunds
            )
            paid = new_charge_obj.paid
        except stripe.error.CardError as e:
            # Since it's a decline, stripe.error.CardError will be caught
            body = e.json_body
            err = body.get('error', {})

            messages.add_message(
                request,
                messages.ERROR,
                err.get('message')
            )

            # return redirect(
            #     'orders:order-list',
            #     order_reference=new_order.order_reference,
            #     access_key=new_order.access_key,
            # )

            # print("Type is: %s") % err.get('type')
            # print("Code is: %s") % err.get('code')
            # # param is '' in this case
            # print("Param is: %s") % err.get('param')
            # print("Message is: %s") % err.get('message')
        except stripe.error.RateLimitError as e:
            # Too many requests made to the API too quickly
            pass
        except stripe.error.InvalidRequestError as e:
            # Invalid parameters were supplied to Stripe's API
            pass
        except stripe.error.AuthenticationError as e:
            # Authentication with Stripe's API failed
            # (maybe you changed API keys recently)
            pass
        except stripe.error.APIConnectionError as e:
            # Network communication with Stripe failed
            pass
        except stripe.error.StripeError as e:
            # Display a very generic error to the user, and maybe send
            # yourself an email
            pass
        except Exception as e:
            # Something else happened, completely unrelated to Stripe
            pass
        if paid:
            # Create new attendee
            i = 1
            attendee_list = []
            for item in reserved_items:
                for _ in range(item.quantity):  # noqa
                    new_attendee_dict = {
                        'event': item.ticket.event,
                        'ticket': item.ticket,
                        'ticket_name': item.ticket.name,
                        'ticket_reference': session_order_reference + "-" + str(i),
                        'ticket_code': get_random_string(length=10),
                    }
                    i += 1
                    attendee_list.append(dict(new_attendee_dict))

            # Create new order
            new_order_dict = {
                'total_gross': total_gross,
                'total_tax': total_tax_amount,
                'total_net': total_net,
                'total_gross_converted': total_gross,  # TODO Marc
                'event': event,
                'order_reference': session_order_reference,
                'status': OrderStatus.PENDING,
                'access_key': get_random_string(length=10),
            }

            new_order = order.save(commit=False)
            [setattr(new_order, k, v) for k, v in new_order_dict.items()]
            new_order.save()

            # Create order items
            for item in order_items_list:
                OrderItem.objects.create(order=new_order, **item)

            # Create attendees
            for item in attendee_list:
                Attendee.objects.create(order=new_order, **item)

            # Create billing profile
            billing_profile = billing.save(commit=False)
            billing_profile.order = new_order
            billing_profile.save()

            # Delete order_reference session
            del request.session['order_reference']

            return redirect(
                'orders:order-list',
                order_reference=new_order.order_reference,
                access_key=new_order.access_key,
            )
else:
    billing = BillingForm(prefix='billing')
    order = OrderForm(prefix='order')

context = {
    'reserved_items': reserved_items,
    'taxes': taxes_dict,
    'total_net': total_net,
    'total_gross': total_gross,
    'currency': event.currency,
    'order': order,
    'billing': billing,
}

return render(request, 'checkout/checkout.html', context)

3 Answers3

10

The problem is not with anything that you've changed, but rather what you haven't changed :)

On this line you are passing an idempotency_key:

charge = stripe.Charge.create(
    ...
    idempotency_key=session_order_reference,
)

As described in the Stripe docs, you can pass an idempotency key with a request, which allows you to make the same request again in the future, using the same key, and you will get the same result as the first request. This is useful in case you didn't recieve the first response because of a network issue.

In this case, you have changed the CVC, which creates a new token variable. This means that your request is not identical to the previous request that used the same idempotency key. That doesn't make sense as you can only use the same idempotency key with identical requests, so you get this error from Stripe.

To resolve this, you should retry the charge creation using a freshly generated idempotency key. Generally, the key should be generated on each unique request that your application creates.

karllekko
  • 5,648
  • 1
  • 15
  • 19
  • thank you so much for the answer. That actually helped a lot. Do you have a recommended way to generate this unique id? –  Jul 11 '18 at 11:02
  • 1
    It should just be a random string with no sensitive information. Often people would use a [UUID](https://stackoverflow.com/questions/534839/how-to-create-a-guid-uuid-in-python) but it's up to you. The important thing is that you regenerate on it each unique request that you make, and only re-use a key if you are explicitly retrying a failed request after an error. – karllekko Jul 11 '18 at 11:14
  • I created a random string generator for my order_ids. The probability that the same 10-digit-string is generated twice in a row is highly unlikely, however, it 'could' happen. Would you mind about that or should I just use that approach? –  Jul 11 '18 at 13:47
  • Note that idempotent keys are only valid for 24 hours, so your generated values only need to be unique for that long. If you feel that your approach is sufficiently random, go with it! – karllekko Jul 11 '18 at 14:32
  • Your answer makes total sense also the Stripe docs - except 1 thing I am not sure imagine this case scenario - someone has a card all info valid also funds and it failed - you call your bank and bank approves it that you can try again - you try again with not data change it is literally same request and the key and it fails again but not on idempotent key anymore but on the same issue as 1st attempt like it was cached or something and when I forcibly change the idempotent key it goes through - have no idea why is not working even bank approved it and request has not changed at all. – Radek Dec 08 '20 at 19:00
2

Had a similar issue where I was passing an indempotency_key and clients were not able to pay after their card was declined because the data that was sent was unique to the charge but not to the card. If for example their CVC was incorrect the subsequent charge will get created with the exact same idempotency key because data pertinent to the actually card was not taken into account.

The fix for this is to make sure your key is unique to the charge AND the card in this case including the card token can fix this.

Some other things to think about are things like partial payments, refunds, same/different ip, other metadata.

0

Stripe handles this case when you are submitting wrong cvv

I tested with stripe test credit cards https://stripe.com/docs/testing#cards use that one which fails with cvv code than use valid card.

Davit Huroyan
  • 302
  • 4
  • 16