Skip to content

Instantly share code, notes, and snippets.

@philfreo
Created September 11, 2013 01:37
Show Gist options
  • Save philfreo/6518326 to your computer and use it in GitHub Desktop.
Save philfreo/6518326 to your computer and use it in GitHub Desktop.
Stripe invoice.created webhook handling for subscription billing
def stripe_invoice_created_webhook(stripe_event):
"""Called by Stripe when an invoice is created so we can log it and tell Stripe about any charges to add."""
stripe_event_invoice = stripe_event['data']['object']
stripe_invoice = stripe.Invoice.retrieve(stripe_event_invoice['id'])
orgs = Organization.objects.filter(status=OrganizationStatus.Paying, stripe_customer_id=stripe_invoice.customer)
for org in orgs:
if org.manual_billing:
continue
"""
We can only add Stripe invoice items to an "open" (non-closed) Stripe invoice.
- https://support.stripe.com/questions/metered-subscription-billing
- https://support.stripe.com/questions/when-is-an-invoice-open-for-modification
An invoice will be sent to us as "closed" for different reasons. Various cases to consider...
"""
# 1) A not-yet-expired trial just added a subscription. A $0 paid/closed invoice is then created.
# => don't log this invoice at all
if stripe_invoice.closed and stripe_invoice.paid and stripe_invoice.total == 0:
return
# 2) Upon the renewal date of a subscription that was previously considered 'unpaid' the invoice
# the invoice will be 'closed' (this is dumb to me) an unpaid.
# => tell stripe to reopen it, then log invoice, generate and send stripe invoice items
if stripe_invoice.closed and not stripe_invoice.paid:
stripe_invoice.closed = False
stripe_invoice.save()
# no return here because we want to fall through to 'open' case
# 3) A subscription's trial ends, causing another invoice created, this time *open*, OR
# 4) A regular renewal event occurs creating an *open* invoice.
# => log invoice, generate and send to stripe invoice items based on current memberships
if not stripe_invoice.closed:
invoice = Invoice.objects.create(organization=org, stripe_id=stripe_invoice.id)
org.generate_invoice_items(invoice)
return
# 5) An already-expired trial just added a subscription and got charged immediately upon
# the subscription creation, causing the webhook notification to come after the invoice is
# already paid/closed. Amount > $0.
# => log invoice, update our own local pending invoiceitems. don't send stripe anything.
if stripe_invoice.closed and stripe_invoice.paid and stripe_invoice.total > 0:
pending_items = InvoiceItem.objects.filter(organization=org, invoice=None)
if pending_items:
# link the initial InvoiceItems (generated during the subscription creation) to the created Invoice
invoice = Invoice.objects.create(organization=org, stripe_id=stripe_invoice.id)
pending_items.update(set__invoice=invoice)
return
mail_exception(subject='Unexpected Stripe Invoice Webhook', context={'stripe_event_invoice': stripe_event_invoice})
@app.route('/stripe_webhook/', methods=['POST'])
def stripe_webhook():
"""Called by Stripe on various events"""
stripe_event = request.json
if stripe_event['type'] == 'invoice.created':
stripe_invoice_created_webhook(stripe_event)
# Careful that we return a 200
return ''
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment