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:
POST
req to/v1/tokens
is sent successfully to SA1 (good/expected)- Card instance saved to db (good/expected)
POST
req to/v1/customers
is sent successfully to SA2 (bad/???)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()
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. – taintedzodiacStripe-Header
further and see if it could be getting set on that/v1/customers
req. we are using Stripe Connect with SA2 – Chris B.