1
votes

UPDATE: I implemented a partial solution and have revised this post below. Customer objects are now being created in the correct Stripe account. However, there remains a secondary issue where the Stripe Card obj is not saving (updating) on the Stripe Customer obj (but was previously).

We have a Django project that needs to use two different Stripe accounts (for compliance reasons). One Stripe account ("SA1") is for SaaS billing and our second Stripe account ("SA2") processes specific one-time payments using Stripe Connect.

After we set this up, I started seeing unexpected behavior where requests are being split sent between both accounts, rather than going to the intended SA1 account. Some API requests get sent to SA1 (what we want), some API requests are sent to SA2 (what we do not want). I will explain further:

we have a view admin_billing where new customers save their card to create and save a new Stripe Customer and their Card.

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 is a Django form:

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

I can also confirm STRIPE_LIVE_PUBLIC_KEY is the correct public key for our SA1 account.

In billing.models we have:

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)

and then we use stripe.js in the view/template to handle the form:

<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>

On form save, what would happen:

  1. POST req to /v1/tokens is sent successfully to SA1 (good/expected)
  2. Card instance saved to db (good/expected)
  3. POST req to /v1/customers is sent successfully to SA2 (bad/???)
  4. Customer instance saved to db (good/expected)

    { "error": { "code": "resource_missing", "doc_url": "https://stripe.com/docs/error-codes/resource-missing", "message": "No such token: {{ token }}", "param": "source", "type": "invalid_request_error" } }

app logs on form submit:

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

I resolved the API request routing issue by explicitly setting the correct SA1 secrete key on the StripeCustomer.create() call in our customer billing manager model in models.py (see new line below):

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

Now both requests (including /v1/customers) are routing to the correct Stripe account, SA1, and I can see Customer obj's being created. However, a Card obj should also be getting saved and attached to that Stripe Customer, but isn't. I can see earlier in our Stripe logs there would be customer.updated and payment_method.attached calls on Customer creation, currently that's not happening). Now need to debug this issue, which my current hypothesis is here in the save() method on our Card model declaration:

    # 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()
1
You may mixed-up the secret keys of both accounts.JPG
I don't believe that's the case. I've confirmed the secret is correct as wellChris B.
You're either using the wrong secret_key for some of the requests, or you are using Connect and setting Stripe-Header (docs) for some of the requests. I'd suggest writing in to support.stripe.com/contact and Stripe can dive into your logs to see exactly what you're sending them.taintedzodiac
thanks. will research Stripe-Header further and see if it could be getting set on that /v1/customers req. we are using Stripe Connect with SA2Chris B.

1 Answers

1
votes

after more debugging, logging, and testing I concluded that this was still an API request-level routing issue between SA1 and SA2 - some requests were still going to SA2 and being signed with the wrong key. for anyone else referencing this if you're using Stripe Connect alongside other non-Connect Stripe accounts it appears in the request networking hierarchy/priority Stripe defaults to requests for the Connect account (in this case SA2). I ultimately resolved this by using Stripe request authentication to do req-level overrides for the API reqs we send.

so for the full chain of requests:

  1. create Stripe customer
  2. retrieve created Stripe customer
  3. modify Stripe customer with new source (card)

we're assigning the correct API key to stripe.api_key and passing it as a kwarg to the call, ex:

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() 

ensuring api_key was set with SA1 key for all requests in the chain overrode the default SA2 requests and routed them correctly to SA1

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