【问题标题】:Stripe API requests being split sent across two Stripe accountsStripe API 请求被拆分发送到两个 Stripe 账户
【发布时间】:2020-04-13 18:23:31
【问题描述】:

更新:我实施了部分解决方案,并在下面修改了这篇文章。现在正在正确的 Stripe 帐户中创建客户对象。但是,仍然存在次要问题,即 Stripe Card obj 未在 Stripe Customer obj 上保存(更新)(但以前是)。

我们有一个 Django 项目需要使用两个不同的 Stripe 帐户(出于合规性原因)。一个 Stripe 帐户(“SA1”)用于 SaaS 计费,我们的第二个 Stripe 帐户(“SA2”)使用 Stripe Connect 处理特定的一次性付款。

设置完成后,我开始看到意外的行为,即请求在两个帐户之间拆分发送,而不是转到预期的 SA1 帐户。一些 API 请求被发送到 SA1(我们想要的),一些 API 请求被发送到 SA2(我们不想要的)。我会进一步解释:

我们有一个 admin_billing 视图,新客户保存他们的卡以创建和保存新的 Stripe 客户及其卡。

def admin_billing(request):
    """
    Query customer table and adds a new card.
    :param request:
    :return: Billing rendering with template
    """

    form = StripeAddCardForm(request.POST or None)
    if form.is_valid():
        customer = Customer.objects.get_or_create_for_user(request.user)

        token = form.cleaned_data['stripeToken']
        card = Card(customer=customer, stripe_card_id=token)
        try:
            card.save()
        except stripe.error.CardError as e:
            body = e.json_body
            err = body.get('error', {})
            messages.error(request, err.get('message'))
            log.error("Stripe card error: %s" % (e))
        except stripe.error.StripeError as e:
            messages.error(request, 'Please try again or report the problem')
            log.error("Stripe error: %s" % (e))
        except Exception as e:
            messages.error(request, 'Please try again or report the problem')
            log.error("Error while handling stripe: %s" % (e))
        finally:
            return redirect(reverse('admin-billing'))

    context = {
        'form': form,
        'stripe_pub_key': settings.STRIPE_LIVE_PUBLIC_KEY
    }
    return render(request, 'management/billing.html', context)

StripeAddCardForm 是一个 Django 表单:

class StripeAddCardForm(forms.Form):
    stripeToken = forms.CharField()

我还可以确认STRIPE_LIVE_PUBLIC_KEY 是我们 SA1 帐户的正确公钥。

billing.models 我们有:

from stripe import Customer as StripeCustomer, Subscription as StripeSubscription, Charge
from jsonfield import JSONField

class CustomerManager(models.Manager):
    def get_or_create_for_user(self, user):
        try:
            customer = user.customer
            return customer
        except AttributeError:
            pass

        stripe_customer = StripeCustomer.create(
            email=user.email,
            description=user.username
        )
        customer = Customer.objects.create(
            user=user,
            stripe_customer_id=stripe_customer.id,
            stripe_customer_data=stripe_customer,
        )
        return customer


class Customer(models.Model):
    user = models.OneToOneField('users.User', null=True, on_delete=models.SET_NULL, related_name='customer')
    stripe_customer_id = models.CharField(unique=True, max_length=255)
    stripe_customer_data = JSONField(blank=True, default=dict, editable=False)
    objects = CustomerManager()

    def __str__(self):
        return self.stripe_customer_id

    @property
    def stripe_customer(self):
        return StripeCustomer.retrieve(self.stripe_customer_id)

class Card(models.Model):
    STATUS_ACTIVE = 'active'
    STATUS_CANCELED = 'canceled'

    STATUS = (
        (STATUS_ACTIVE, 'Card active'),
        (STATUS_CANCELED, 'Card canceled'),
    )

    customer = models.ForeignKey('Customer', on_delete=models.PROTECT, related_name='cards')
    stripe_card_id = models.CharField(unique=True, max_length=255)
    stripe_card_data = JSONField(default=dict)

    is_primary = models.BooleanField(default=False)
    status = models.CharField(choices=STATUS, max_length=20, default=STATUS_ACTIVE)
    date_created = models.DateTimeField(auto_now_add=True)
    date_canceled = models.DateTimeField(null=True)

    objects = CardQuerySet.as_manager()

    class Meta:
        ordering = ['id']

    def __str__(self):
        return self.stripe_card_id

    @property
    def stripe_customer(self):
        if not self.customer:
            return None
        return self.customer.stripe_customer

    def save(self, *args, **kwargs):
        # tagging existing card as primary (only one card can be primary card)
        if self.pk and self.is_primary:
            if self.status == self.STATUS_CANCELED:
                raise ValueError("Primary card must be active")
            self.customer.cards.all().exclude(id=self.id).filter(is_primary=True).update(is_primary=False)
            self.customer.stripe_customer_data = StripeCustomer.modify(
                self.customer.stripe_customer_id,
                default_source=self.stripe_card_id,
            )
            self.customer.save()

        # new card
        if not self.pk:
            self.stripe_card_data = self.stripe_customer.sources.create(source=self.stripe_card_id)
            self.stripe_card_id = self.stripe_card_data['id']

            # if this is a first card for this customer
            if not self.customer.cards.get_active():
                self.is_primary = True
                self.customer.stripe_customer_data = StripeCustomer.modify(
                    self.customer.stripe_customer_id,
                    default_source=self.stripe_card_id,
                )
                self.customer.save()

        super(Card, self).save(*args, **kwargs)

然后我们在视图/模板中使用 stripe.js 来处理表单:

<script src="https://js.stripe.com/v3/"></script>
<script>

// Create a Stripe client. This is SA1 key passed in from view ctx
var stripe = Stripe('{{ stripe_pub_key }}');

// Create an instance of Elements.
var elements = stripe.elements();

// Create an instance of the card Element.
var card = elements.create('card', {style: style});

// Add an instance of the card Element into the `card-element` <div>.
card.mount('#card-element');

// Handle real-time validation errors from the card Element.
card.addEventListener('change', function(event) {
    var displayError = document.getElementById('card-errors');
    if (event.error) {
        displayError.textContent = event.error.message;
    } else {
        displayError.textContent = '';
    }
});

// Handle form submission.
var stripeCardForm = document.getElementById('payment-form');
stripeCardForm.addEventListener('submit', function(event) {
    event.preventDefault();

    stripe.createToken(card).then(function(result) {
        if (result.error) {
            // Inform the user if there was an error.
            var errorElement = document.getElementById('card-errors');
            errorElement.textContent = result.error.message;
        } else {
            // Send the token to your server.
            stripeTokenHandler(result.token);
        }
    });
});

// Submit the form with the token ID.
function stripeTokenHandler(token) {
    // Insert the token ID into the form so it gets submitted to the server
    var form = document.getElementById('payment-form');
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', 'stripeToken');
    hiddenInput.setAttribute('value', token.id);
    form.appendChild(hiddenInput);

    // Submit the form
    form.submit();
}

</script>

在表单保存时,会发生什么:

  1. POST/v1/tokens 的请求已成功发送到 SA1(良好/预期)
  2. 卡实例保存到数据库(良好/预期)
  3. POST req to /v1/customers 成功发送到SA2(坏/???)
  4. 客户实例保存到数据库(良好/预期)

    { “错误”: { “代码”:“resource_missing”, "doc_url": "https://stripe.com/docs/error-codes/resource-missing", "message": "没有这样的令牌:{{ token }}", “参数”:“来源”, “类型”:“invalid_request_error” } }

应用程序登录表单提交:

2019-12-21T18:14:52.154021+00:00: at=info method=POST path="/manage/billing/" host=app.com 
2019-12-21T18:14:52.134957+00:00: [4] [INFO] pathname=/app/python/lib/python3.6/site-packages/stripe/util.py lineno=63 funcname=log_info message='Stripe API response' path=https://api.stripe.com/v1/customers/cus_foo response_code=404
2019-12-21T18:14:52.136422+00:00: [4] [INFO] pathname=/app/python/lib/python3.6/site-packages/stripe/util.py lineno=63 funcname=log_info error_code=resource_missing error_message='No such customer: cus_foo' error_param=id error_type=invalid_request_error message='Stripe API error received'
2019-12-21T18:14:52.136791+00:00: [4] [ERROR] pathname=./management/views.py lineno=1755 funcname=admin_billing Stripe error: Request req_bar: No such customer: cus_foo
2019-12-21T18:14:52.153639+00:00: [4] [INFO] pathname=/app/python/lib/python3.6/site-packages/uvicorn/protocols/http/httptools_impl.py lineno=443 funcname=send ('10.45.113.223', 12377) - "POST /manage/billing/ HTTP/1.1" 302

我通过在 models.py 中的客户计费管理器模型中显式设置 StripeCustomer.create() 调用上的正确 SA1 密钥解决了 API 请求路由问题(请参阅下面的新行):

class CustomerManager(models.Manager):
    def get_or_create_for_user(self, user):
        try:
            customer = user.customer
            return customer
        except AttributeError:
            pass

        stripe_customer = StripeCustomer.create(
            email=user.email,
            description=user.username,
            # NEW LINE
            **api_key=settings.STRIPE_LIVE_SECRET_KEY**
        )
        customer = Customer.objects.create(
            user=user,
            stripe_customer_id=stripe_customer.id,
            stripe_customer_data=stripe_customer,
        )
        return customer

现在两个请求(包括/v1/customers)都路由到正确的 Stripe 帐户 SA1,我可以看到 Customer obj 正在创建。然而,一个 Card obj 也应该被保存并附加到那个 Stripe Customer,但不是。我可以在之前的 Stripe 日志中看到,在创建客户时会有 customer.updatedpayment_method.attached 调用,目前还没有发生)。现在需要调试这个问题,我目前的假设是在我们的 Card 模型声明的 save() 方法中:

    # new card
    if not self.pk:
            self.stripe_card_data = self.stripe_customer.sources.create(source=self.stripe_card_id)
            self.stripe_card_id = self.stripe_card_data['id']

            # if this is a first card for this customer
            if not self.customer.cards.get_active():
                self.is_primary = True
                self.customer.stripe_customer_data = StripeCustomer.modify(
                    self.customer.stripe_customer_id,
                    default_source=self.stripe_card_id,
                )
                self.customer.save()

【问题讨论】:

  • 您可能会混淆两个帐户的密钥。
  • 我认为情况并非如此。我已经确认这个秘密也是正确的
  • 您为某些请求使用了错误的 secret_key,或者您为某些请求使用了 Connect 并设置了 Stripe-Header (docs)。我建议写信给support.stripe.com/contact,Stripe 可以深入了解您的日志,以查看您发送给他们的确切内容。
  • 谢谢。将进一步研究Stripe-Header,看看它是否可以设置在/v1/customers req 上。我们正在使用带有 SA2 的 Stripe Connect

标签: python django stripe-payments


【解决方案1】:

经过更多调试、日志记录和测试后,我得出结论,这仍然是 SA1 和 SA2 之间的 API 请求级路由问题 - 一些请​​求仍会发送到 SA2 并使用错误的密钥进行签名。如果您将 Stripe Connect 与其他非 Connect Stripe 帐户一起使用,则对于引用此内容的其他人,它会出现在请求网络层次结构/优先级 Stripe 默认为对 Connect 帐户的请求(在本例中为 SA2)。我最终通过使用 Stripe 请求身份验证对我们发送的 API 请求进行请求级覆盖来解决这个问题。

所以对于完整的请求链:

  1. 创建 Stripe 客户
  2. 检索已创建的 Stripe 客户
  3. 使用新来源(卡)修改 Stripe 客户

我们将正确的 API 密钥分配给 stripe.api_key 并将其作为 kwarg 传递给调用,例如:

self.customer.stripe_customer_data = StripeCustomer.modify(
     self.customer.stripe_customer_id,
     default_source=self.stripe_card_id,
     api_key=settings.STRIPE_LIVE_SECRET_KEY
)
self.customer.save() 

确保 api_key 为链中的所有请求设置了 SA1 密钥,覆盖默认的 SA2 请求并将它们正确路由到 SA1

https://stripe.com/docs/api/authentication

【讨论】:

    猜你喜欢
    • 2021-12-27
    • 1970-01-01
    • 2019-08-05
    • 1970-01-01
    • 2016-12-09
    • 1970-01-01
    • 1970-01-01
    • 2018-08-06
    • 1970-01-01
    相关资源
    最近更新 更多