Django / Stripe:幂等请求只能与相同参数一起使用

时间:2018-07-11 06:07:11

标签: django stripe-payments

我在Django应用程序中使用Stripe。我有以下测试案例:incorrect_cvc导致card_error。现在,在更正CVC并使用4242 4242 4242 4242时,我除外,这是一次成功的充电。但是,我得到的是以下错误消息:

  

请求req_auTSTGSGoUVNUa:幂等请求的密钥只能是   与最初使用的参数相同。尝试使用   如果要执行其他操作,则除“ k1qjchgqjw”外的其他键   请求。

我不知道我更改了哪个parameters。但是我认为,在card_error之后,结帐过程基本上不再起作用的想法不是。有谁知道我“更改”了哪些参数导致出现此错误消息?

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 个答案:

答案 0 :(得分:1)

有一个类似的问题,我通过indempotency_key,并且客户的卡被拒后,客户无法付款,因为发送的数据是唯一的费用,而不是卡上的唯一数据。例如,如果他们的CVC错误,那么将使用完全相同的幂等性密钥来创建后续费用,因为未考虑与实际卡相关的数据。

此问题的解决方法是确保您的钥匙对费用唯一,并且在这种情况下,包括卡令牌在内的卡都可以解决此问题。

其他需要考虑的事情包括部分付款,退款,相同/不同的IP,其他元数据。

答案 1 :(得分:0)

问题不在于您所做的任何更改,而在于您未更改的内容:)

在这一行上,您正在传递idempotency_key

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

Stripe docs中所述,您可以将幂等密钥与请求一起传递,这样将来您可以使用相同的密钥再次提出相同的请求,并且获得与相同的结果。第一个请求。如果您由于网络问题而未收到第一响应,这很有用。

在这种情况下,您已经更改了CVC,它创建了一个新的token变量。这意味着您的请求与使用相同幂等键的先前请求不同。这是没有意义的,因为您只能对相同的请求使用相同的幂等性密钥,因此您会从Stripe中得到此错误。

要解决此问题,您应该使用新生成的幂等性密钥重试电荷创建。通常,应在应用程序创建的每个唯一请求上生成密钥。

答案 2 :(得分:0)

条带处理这种情况 当您提交错误的简历时

我使用条纹测试信用卡进行了测试 https://stripe.com/docs/testing#cards 使用失败的验证码而不是使用有效卡。