webhooks: Update Stripe integration.

This commit is contained in:
Rishi Gupta
2018-11-20 16:39:37 -08:00
parent 882cf224f8
commit 9f471a3e7d
8 changed files with 214 additions and 548 deletions

View File

@@ -11,28 +11,13 @@ Get Zulip notifications for Stripe events!
the event types you would like to be notified about, and click
**Add endpoint**.
Zulip currently supports the following Stripe events:
1. [Optional] In Zulip, add a
[linkification filter](/help/add-a-custom-linkification-filter) with
**Pattern** `(?P<id>cus_[0-9a-zA-Z]+)` and **URL format string**
`https://dashboard.stripe.com/customers/%(id)s`.
* Charge Dispute Closed
* Charge Dispute Created
* Charge Failed
* Charge Succeeded
* Customer Created
* Customer Deleted
* Customer Subscription Created
* Customer Subsciption Deleted
* Customer Subscription Trial Will End
* Invoice Payment Failed
* Order Payment Failed
* Order Payment Succeeded
* Order Updated
* Transfer Failed
* Transfer Paid
!!! tip ""
To set up different topics for different events, create separate
webhooks for those events, customizing the URL stream and topic
for each.
Zulip currently supports Stripe events for Charges, Customers, Discounts,
Sources, Subscriptions, Files, Invoices and Invoice items.
{% if 'http:' in external_uri_scheme %}

View File

@@ -1,64 +0,0 @@
{
"created": 1326853478,
"livemode": false,
"id": "evt_00000000000000",
"type": "order.payment_failed",
"object": "event",
"request": null,
"pending_webhooks": 1,
"api_version": "2016-07-06",
"data": {
"object": {
"id": "or_00000000000000",
"object": "order",
"amount": 1500,
"amount_returned": null,
"application": null,
"application_fee": null,
"charge": null,
"created": 1480672065,
"currency": "aud",
"customer": null,
"email": null,
"items": [
{
"object": "order_item",
"amount": 1500,
"currency": "aud",
"description": "T-shirt",
"parent": "sk_19MDVPCV4wXizEw4hEI0KH4Q",
"quantity": null,
"type": "sku"
}
],
"livemode": false,
"metadata": {},
"returns": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/order_returns?order=or_19MDdtCV4wXizEw4j5wLMEYP"
},
"selected_shipping_method": null,
"shipping": {
"address": {
"city": "Anytown",
"country": "US",
"line1": "1234 Main street",
"line2": null,
"postal_code": "123456",
"state": null
},
"carrier": null,
"name": "Jenny Rosen",
"phone": null,
"tracking_number": null
},
"shipping_methods": null,
"status": "created",
"status_transitions": null,
"updated": 1480672065
}
}
}

View File

@@ -1,64 +0,0 @@
{
"created": 1326853478,
"livemode": false,
"id": "evt_00000000000000",
"type": "order.payment_succeeded",
"object": "event",
"request": null,
"pending_webhooks": 1,
"api_version": "2016-07-06",
"data": {
"object": {
"id": "or_00000000000000",
"object": "order",
"amount": 1500,
"amount_returned": null,
"application": null,
"application_fee": null,
"charge": null,
"created": 1480672071,
"currency": "aud",
"customer": null,
"email": null,
"items": [
{
"object": "order_item",
"amount": 1500,
"currency": "aud",
"description": "T-shirt",
"parent": "sk_19MDVPCV4wXizEw4hEI0KH4Q",
"quantity": null,
"type": "sku"
}
],
"livemode": false,
"metadata": {},
"returns": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/order_returns?order=or_19MDdzCV4wXizEw4tadCnKNn"
},
"selected_shipping_method": null,
"shipping": {
"address": {
"city": "Anytown",
"country": "US",
"line1": "1234 Main street",
"line2": null,
"postal_code": "123456",
"state": null
},
"carrier": null,
"name": "Jenny Rosen",
"phone": null,
"tracking_number": null
},
"shipping_methods": null,
"status": "created",
"status_transitions": null,
"updated": 1480672071
}
}
}

View File

@@ -1,65 +0,0 @@
{
"created": 1326853478,
"livemode": false,
"id": "evt_00000000000000",
"type": "order.updated",
"object": "event",
"request": null,
"pending_webhooks": 1,
"api_version": "2016-07-06",
"data": {
"object": {
"id": "or_00000000000000",
"object": "order",
"amount": 1500,
"amount_returned": null,
"application": null,
"application_fee": null,
"charge": null,
"created": 1480672076,
"currency": "aud",
"customer": null,
"email": null,
"items": [
{
"object": "order_item",
"amount": 1500,
"currency": "aud",
"description": "T-shirt",
"parent": "sk_19MDVPCV4wXizEw4hEI0KH4Q",
"quantity": null,
"type": "sku"
}
],
"livemode": false,
"metadata": {},
"returns": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/order_returns?order=or_19MDe4CV4wXizEw4hV0NcMsD"
},
"selected_shipping_method": null,
"shipping": {
"address": {
"city": "Anytown",
"country": "US",
"line1": "1234 Main street",
"line2": null,
"postal_code": "123456",
"state": null
},
"carrier": null,
"name": "Jenny Rosen",
"phone": null,
"tracking_number": null
},
"shipping_methods": null,
"status": "created",
"status_transitions": null,
"updated": 1480672076
},
"previous_attributes": {}
}
}

View File

@@ -1,44 +0,0 @@
{
"created": 1326853478,
"livemode": false,
"id": "evt_00000000000000",
"type": "transfer.failed",
"object": "event",
"request": null,
"pending_webhooks": 1,
"api_version": "2016-07-06",
"data": {
"object": {
"id": "tr_00000000000000",
"object": "transfer",
"amount": 1100,
"amount_reversed": 0,
"application_fee": null,
"balance_transaction": "txn_00000000000000",
"created": 1480672150,
"currency": "aud",
"date": 1480672150,
"description": "Transfer to test@example.com",
"destination": "ba_19MDfGCV4wXizEw4Yp5cFspJ",
"failure_code": null,
"failure_message": null,
"livemode": false,
"metadata": {},
"method": "standard",
"recipient": "rp_00000000000000",
"reversals": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/transfers/tr_19MDfGCV4wXizEw4tj8SREbU/reversals"
},
"reversed": false,
"source_transaction": null,
"source_type": "card",
"statement_descriptor": null,
"status": "failed",
"type": "bank_account"
}
}
}

View File

@@ -1,44 +0,0 @@
{
"created": 1326853478,
"livemode": false,
"id": "evt_00000000000000",
"type": "transfer.paid",
"object": "event",
"request": null,
"pending_webhooks": 1,
"api_version": "2016-07-06",
"data": {
"object": {
"id": "tr_00000000000000",
"object": "transfer",
"amount": 1100,
"amount_reversed": 0,
"application_fee": null,
"balance_transaction": "txn_00000000000000",
"created": 1480672156,
"currency": "aud",
"date": 1480672156,
"description": "Transfer to test@example.com",
"destination": "ba_19MDfMCV4wXizEw4jOWU4wu8",
"failure_code": null,
"failure_message": null,
"livemode": false,
"metadata": {},
"method": "standard",
"recipient": "rp_00000000000000",
"reversals": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/transfers/tr_19MDfMCV4wXizEw4GpK8rGHB/reversals"
},
"reversed": false,
"source_transaction": null,
"source_type": "card",
"statement_descriptor": null,
"status": "paid",
"type": "bank_account"
}
}
}

View File

@@ -10,88 +10,65 @@ class StripeHookTests(WebhookTestCase):
FIXTURE_DIR_NAME = 'stripe'
def test_charge_dispute_closed(self) -> None:
expected_topic = u"Charge ch_00000000000000"
expected_message = u"A charge dispute for **10.01aud** has been closed as **won**.\nThe charge in dispute was **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)**."
# use fixture named stripe_charge_dispute_closed
expected_topic = u"disputes"
expected_message = u"[Dispute](https://dashboard.stripe.com/disputes/dp_00000000000000) closed. Current status: won."
self.send_and_test_stream_message('charge_dispute_closed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_charge_dispute_created(self) -> None:
expected_topic = u"Charge ch_00000000000000"
expected_message = u"A charge dispute for **1000jpy** has been created.\nThe charge in dispute is **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)**."
# use fixture named stripe_charge_dispute_created
expected_topic = u"disputes"
expected_message = u"[Dispute](https://dashboard.stripe.com/disputes/dp_00000000000000) created. Current status: needs response."
self.send_and_test_stream_message('charge_dispute_created', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_charge_failed(self) -> None:
expected_topic = u"Charge ch_00000000000000"
expected_message = u"A charge with id **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)** for **1.00aud** has failed."
# use fixture named stripe_charge_failed
expected_topic = u"charges"
expected_message = u"[Charge](https://dashboard.stripe.com/charges/ch_00000000000000) for 1.00 AUD failed"
self.send_and_test_stream_message('charge_failed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_charge_succeeded(self) -> None:
expected_topic = u"Charge ch_00000000000000"
expected_message = u"A charge with id **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)** for **1.00aud** has succeeded."
# use fixture named stripe_charge_succeeded
expected_topic = u"charges"
expected_message = u"[Charge](https://dashboard.stripe.com/charges/ch_00000000000000) for 1.00 AUD succeeded"
self.send_and_test_stream_message('charge_succeeded', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_customer_created_email(self) -> None:
expected_topic = u"Customer cus_00000000000000"
expected_message = u"A new customer with id **[cus_00000000000000](https://dashboard.stripe.com/customers/cus_00000000000000)** and email **example@abc.com** has been created."
# use fixture named stripe_customer_created_email
self.send_and_test_stream_message('customer_created_email', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_customer_created(self) -> None:
expected_topic = u"Customer cus_00000000000000"
expected_message = u"A new customer with id **[cus_00000000000000](https://dashboard.stripe.com/customers/cus_00000000000000)** has been created."
# use fixture named stripe_customer_created
expected_topic = u"cus_00000000000000"
expected_message = u"[Customer](https://dashboard.stripe.com/customers/cus_00000000000000) created"
self.send_and_test_stream_message('customer_created', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_customer_deleted(self) -> None:
expected_topic = u"Customer cus_00000000000000"
expected_message = u"A customer with id **[cus_00000000000000](https://dashboard.stripe.com/customers/cus_00000000000000)** has been deleted."
def test_customer_created_email(self) -> None:
expected_topic = u"cus_00000000000000"
expected_message = u"[Customer](https://dashboard.stripe.com/customers/cus_00000000000000) created\nEmail: example@abc.com"
self.send_and_test_stream_message('customer_created_email', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
# use fixture named stripe_customer_deleted
def test_customer_deleted(self) -> None:
expected_topic = u"cus_00000000000000"
expected_message = u"[Customer](https://dashboard.stripe.com/customers/cus_00000000000000) deleted"
self.send_and_test_stream_message('customer_deleted', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_customer_subscription_created(self) -> None:
expected_topic = u"Customer sub_00000000000000"
expected_message = u"A new customer subscription for **20.00aud** every **month** has been created.\nThe subscription has id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)**."
# use fixture named stripe_customer_subscription_created
expected_topic = u"cus_00000000000000"
expected_message = u"""\
[Subscription](https://dashboard.stripe.com/subscriptions/sub_00000000000000) created
Plan: [Gold Special](https://dashboard.stripe.com/plans/gold_00000000000000)
Quantity: 1"""
self.send_and_test_stream_message('customer_subscription_created', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_customer_subscription_deleted(self) -> None:
expected_topic = u"Customer sub_00000000000000"
expected_message = u"The customer subscription with id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)** was deleted."
# use fixture named stripe_customer_subscription_deleted
expected_topic = u"cus_00000000000000"
expected_message = u"[Subscription](https://dashboard.stripe.com/subscriptions/sub_00000000000000) deleted"
self.send_and_test_stream_message('customer_subscription_deleted', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_customer_subscription_updated(self) -> None:
expected_topic = u"Customer sub_00000000000000"
expected_message = u"The customer subscription with id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)** was updated."
self.send_and_test_stream_message('customer_subscription_updated',
expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_customer_subscription_trial_will_end(self) -> None:
expected_topic = u"Customer sub_00000000000000"
expected_message = u"The customer subscription trial with id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)** will end in 3 days."
expected_topic = u"cus_00000000000000"
expected_message = u"[Subscription](https://dashboard.stripe.com/subscriptions/sub_00000000000000) trial will end in 3 days"
# 3 days before the end of the trial, plus a little bit to make sure the rounding is working
with mock.patch('time.time', return_value=1480892861 - 3*3600*24 + 100):
# use fixture named stripe_customer_subscription_trial_will_end
@@ -100,52 +77,7 @@ class StripeHookTests(WebhookTestCase):
content_type="application/x-www-form-urlencoded")
def test_invoice_payment_failed(self) -> None:
expected_topic = u"Invoice in_00000000000000"
expected_message = u"An invoice payment on invoice with id **[in_00000000000000](https://dashboard.stripe.com/invoices/in_00000000000000)** and with **0.00aud** due has failed."
# use fixture named stripe_invoice_payment_failed
expected_topic = u"cus_00000000000000"
expected_message = u"[Invoice](https://dashboard.stripe.com/invoices/in_00000000000000) payment failed"
self.send_and_test_stream_message('invoice_payment_failed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_order_payment_failed(self) -> None:
expected_topic = u"Order or_00000000000000"
expected_message = u"An order payment on order with id **[or_00000000000000](https://dashboard.stripe.com/orders/or_00000000000000)** for **15.00aud** has failed."
# use fixture named stripe_order_payment_failed
self.send_and_test_stream_message('order_payment_failed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_order_payment_succeeded(self) -> None:
expected_topic = u"Order or_00000000000000"
expected_message = u"An order payment on order with id **[or_00000000000000](https://dashboard.stripe.com/orders/or_00000000000000)** for **15.00aud** has succeeded."
# use fixture named stripe_order_payment_succeeded
self.send_and_test_stream_message('order_payment_succeeded', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_order_updated(self) -> None:
expected_topic = u"Order or_00000000000000"
expected_message = u"The order with id **[or_00000000000000](https://dashboard.stripe.com/orders/or_00000000000000)** for **15.00aud** has been updated."
# use fixture named stripe_order_updated
self.send_and_test_stream_message('order_updated', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_transfer_failed(self) -> None:
expected_topic = u"Transfer tr_00000000000000"
expected_message = u"The transfer with description **Transfer to test@example.com** and id **[tr_00000000000000](https://dashboard.stripe.com/transfers/tr_00000000000000)** for amount **11.00aud** has failed."
# use fixture named stripe_transfer_failed
self.send_and_test_stream_message('transfer_failed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def test_transfer_paid(self) -> None:
expected_topic = u"Transfer tr_00000000000000"
expected_message = u"The transfer with description **Transfer to test@example.com** and id **[tr_00000000000000](https://dashboard.stripe.com/transfers/tr_00000000000000)** for amount **11.00aud** has been paid."
# use fixture named stripe_transfer_paid
self.send_and_test_stream_message('transfer_paid', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
def get_body(self, fixture_name: str) -> str:
return self.webhook_fixture_data("stripe", fixture_name, file_type="json")

View File

@@ -1,7 +1,7 @@
# Webhooks for external integrations.
import time
from datetime import datetime
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Tuple
from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
@@ -18,168 +18,198 @@ from zerver.models import UserProfile
def api_stripe_webhook(request: HttpRequest, user_profile: UserProfile,
payload: Dict[str, Any]=REQ(argument_type='body'),
stream: str=REQ(default='test')) -> HttpResponse:
event_type = payload["type"] # invoice.created, customer.subscription.created, etc
if len(event_type.split('.')) == 3:
category, resource, event = event_type.split('.')
else:
resource, event = event_type.split('.')
category = resource
object_ = payload["data"]["object"] # The full, updated Stripe object
# Set the topic to the customer_id when we can
topic = ''
customer_id = object_.get("customer", None)
if customer_id is not None:
# Running into the 60 character topic limit.
# topic = '[{}](https://dashboard.stripe.com/customers/{})' % (customer_id, customer_id)
topic = customer_id
body = None
event_type = payload["type"]
data_object = payload["data"]["object"]
if event_type.startswith('charge'):
charge_url = "https://dashboard.stripe.com/payments/{}"
amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"])
def default_body() -> str:
return '{resource} {verbed}'.format(
resource=linkified_id(object_['id']), verbed=event.replace('_', ' '))
if event_type.startswith('charge.dispute'):
charge_id = data_object["charge"]
link = charge_url.format(charge_id)
body_template = "A charge dispute for **{amount}** has been {rest}.\n"\
"The charge in dispute {verb} **[{charge}]({link})**."
if event_type == "charge.dispute.closed":
rest = "closed as **{}**".format(data_object['status'])
verb = 'was'
if category == 'account': # nocoverage
if event == 'updated':
topic = "account updates"
body = ''
else:
rest = "created"
verb = 'is'
body = body_template.format(amount=amount_string,
rest=rest,
verb=verb,
charge=charge_id,
link=link)
else:
charge_id = data_object["id"]
link = charge_url.format(charge_id)
body_template = "A charge with id **[{charge_id}]({link})** for **{amount}** has {verb}."
if event_type == "charge.failed":
verb = "failed"
else:
verb = "succeeded"
body = body_template.format(charge_id=charge_id, link=link, amount=amount_string, verb=verb)
topic = "Charge {}".format(charge_id)
elif event_type.startswith('customer'):
object_id = data_object["id"]
if event_type.startswith('customer.subscription'):
link = "https://dashboard.stripe.com/subscriptions/{}".format(object_id)
if event_type == "customer.subscription.created":
amount_string = amount(data_object["plan"]["amount"], data_object["plan"]["currency"])
body_template = "A new customer subscription for **{amount}** " \
"every **{interval}** has been created.\n" \
"The subscription has id **[{id}]({link})**."
body = body_template.format(
amount=amount_string,
interval=data_object['plan']['interval'],
id=object_id,
link=link
)
elif event_type == "customer.subscription.deleted":
body_template = "The customer subscription with id **[{id}]({link})** was deleted."
body = body_template.format(id=object_id, link=link)
elif event_type == "customer.subscription.trial_will_end":
# Part of Stripe Connect
return json_success()
if category == 'application_fee': # nocoverage
# Part of Stripe Connect
return json_success()
if category == 'balance': # nocoverage
# Not that interesting to most businesses, I think
return json_success()
if category == 'charge':
if resource == 'charge':
if not topic:
topic = 'charges'
body = "{resource} for {amount} {verbed}".format(
resource=linkified_id(object_['id']),
amount=amount_string(object_['amount'], object_['currency']), verbed=event)
if object_['failure_code']: # nocoverage
body += '. Failure code: {}'.format(object_['failure_code'])
if resource == 'dispute':
topic = 'disputes'
body = default_body() + '. Current status: {status}.'.format(
status=object_['status'].replace('_', ' '))
if resource == 'refund': # nocoverage
topic = 'refunds'
body = 'A {resource} for a {charge} of {amount} was updated.'.format(
resource=linkified_id(object_['id'], lower=True),
charge=linkified_id(object_['charge'], lower=True), amount=object_['amount'])
if category == 'checkout_beta': # nocoverage
# Not sure what this is
return json_success()
if category == 'coupon': # nocoverage
# Not something that likely happens programmatically
return json_success()
if category == 'customer':
if resource == 'customer':
# Running into the 60 character topic limit.
# topic = '[{}](https://dashboard.stripe.com/customers/{})' % (object_['id'], object_['id'])
topic = object_['id']
body = default_body()
if event == 'created':
if object_['email']:
body += '\nEmail: {}'.format(object_['email'])
if object_['metadata']: # nocoverage
for key, value in object_['metadata'].items():
body += '\n{}: {}'.format(key, value)
if resource == 'discount': # nocoverage
body = default_body() + '. ({coupon})'.format(
coupon=linkified_id(object_['coupon'], lower=True))
if resource == 'source': # nocoverage
body = default_body()
if resource == 'subscription':
body = default_body()
if event == 'trial_will_end':
DAY = 60 * 60 * 24 # seconds in a day
# days_left should always be three according to
# https://stripe.com/docs/api/python#event_types, but do the
# computation just to be safe.
days_left = int((data_object["trial_end"] - time.time() + DAY//2) // DAY)
body_template = ("The customer subscription trial with id"
" **[{id}]({link})** will end in {days} days.")
body = body_template.format(id=object_id, link=link, days=days_left)
elif event_type == "customer.subscription.updated":
body_template = "The customer subscription with id **[{id}]({link})** was updated."
body = body_template.format(id=object_id, link=link)
# Basically always three: https://stripe.com/docs/api/python#event_types
body += ' in {days} days'.format(
days=int((object_["trial_end"] - time.time() + DAY//2) // DAY))
if event == 'created':
if object_['plan']:
body += '\nPlan: [{plan_name}](https://dashboard.stripe.com/plans/{plan_id})'.format(
plan_name=object_['plan']['name'], plan_id=object_['plan']['id'])
if object_['quantity']:
body += '\nQuantity: {}'.format(object_['quantity'])
if 'billing' in object_: # nocoverage
body += '\nBilling method: {}'.format(object_['billing'].replace('_', ' '))
if category == 'file': # nocoverage
topic = 'files'
body = default_body() + ' ({purpose}). \nTitle: {title}'.format(
purpose=object_['purpose'].replace('_', ' '), title=object_['title'])
if category == 'invoice':
if event == 'upcoming': # nocoverage
body = 'Upcoming invoice created'
else:
raise UnexpectedWebhookEventType("Stripe", event_type)
else:
link = "https://dashboard.stripe.com/customers/{}".format(object_id)
body_template = "{beginning} customer with id **[{id}]({link})** {rest}."
if event_type == "customer.created":
beginning = "A new"
if data_object["email"] is None:
rest = "has been created"
else:
rest = "and email **{}** has been created".format(data_object['email'])
elif event_type == "customer.deleted":
beginning = "A"
rest = "has been deleted"
else:
raise UnexpectedWebhookEventType("Stripe", event_type)
body = body_template.format(beginning=beginning, id=object_id, link=link, rest=rest)
topic = "Customer {}".format(object_id)
elif event_type == "invoice.payment_failed":
object_id = data_object['id']
link = "https://dashboard.stripe.com/invoices/{}".format(object_id)
amount_string = amount(data_object["amount_due"], data_object["currency"])
body_template = "An invoice payment on invoice with id **[{id}]({link})** and "\
"with **{amount}** due has failed."
body = body_template.format(id=object_id, amount=amount_string, link=link)
topic = "Invoice {}".format(object_id)
elif event_type.startswith('order'):
object_id = data_object['id']
link = "https://dashboard.stripe.com/orders/{}".format(object_id)
amount_string = amount(data_object["amount"], data_object["currency"])
body_template = "{beginning} order with id **[{id}]({link})** for **{amount}** has {end}."
if event_type == "order.payment_failed":
beginning = "An order payment on"
end = "failed"
elif event_type == "order.payment_succeeded":
beginning = "An order payment on"
end = "succeeded"
elif event_type == "order.updated":
beginning = "The"
end = "been updated"
else:
raise UnexpectedWebhookEventType("Stripe", event_type)
body = body_template.format(beginning=beginning,
id=object_id,
link=link,
amount=amount_string,
end=end)
topic = "Order {}".format(object_id)
elif event_type.startswith('transfer'):
object_id = data_object['id']
link = "https://dashboard.stripe.com/transfers/{}".format(object_id)
amount_string = amount(data_object["amount"], data_object["currency"])
body_template = "The transfer with description **{description}** and id **[{id}]({link})** " \
"for amount **{amount}** has {end}."
if event_type == "transfer.failed":
end = 'failed'
elif event_type == "transfer.paid":
end = "been paid"
else:
raise UnexpectedWebhookEventType('Stripe', event_type)
body = body_template.format(
description=data_object['description'],
id=object_id,
link=link,
amount=amount_string,
end=end
)
topic = "Transfer {}".format(object_id)
body = default_body()
if event == 'created': # nocoverage
# Could potentially add link to invoice PDF here
body += ' ({reason})\nBilling method: {method}\nTotal: {total}\nAmount due: {due}'.format(
reason=object_['billing_reason'].replace('_', ' '),
method=object_['billing'].replace('_', ' '),
total=amount_string(object_['total'], object_['currency']),
due=amount_string(object_['amount_due'], object_['currency']))
if category == 'invoiceitem': # nocoverage
body = default_body()
if event == 'created':
body += ' for {amount}'.format(amount=amount_string(object_['amount'], object_['currency']))
if category.startswith('issuing'): # nocoverage
# Not implemented
return json_success()
if category.startswith('order'): # nocoverage
# Not implemented
return json_success()
if category in ['payment_intent', 'payout', 'plan', 'product', 'recipient',
'reporting', 'review', 'sigma', 'sku', 'source', 'subscription_schedule',
'topup', 'transfer']: # nocoverage
# Not implemented. In theory doing something like
# body = default_body()
# may not be hard for some of these
return json_success()
if body is None:
raise UnexpectedWebhookEventType('Stripe', event_type)
check_send_webhook_message(request, user_profile, topic, body)
if 'previous_attributes' in payload['data']: # nocoverage
previous_attributes = payload['data']['previous_attributes']
else:
previous_attributes = {}
body += '\n' + update_string(previous_attributes)
body = body.strip()
check_send_webhook_message(request, user_profile, topic, body)
return json_success()
def amount(amount: int, currency: str) -> str:
# zero-decimal currencies
def amount_string(amount: int, currency: str) -> str:
zero_decimal_currencies = ["bif", "djf", "jpy", "krw", "pyg", "vnd", "xaf",
"xpf", "clp", "gnf", "kmf", "mga", "rwf", "vuv", "xof"]
if currency in zero_decimal_currencies:
return str(amount) + currency
decimal_amount = str(amount) # nocoverage
else:
return '{0:.02f}'.format(float(amount) * 0.01) + currency
decimal_amount = '{0:.02f}'.format(float(amount) * 0.01)
if currency == 'usd': # nocoverage
return '$' + decimal_amount
return decimal_amount + ' {}'.format(currency.upper())
def update_string(previous_attributes: Dict[str, Any]) -> str:
return '\n'.join(attribute.replace('_', ' ').capitalize() + ' updated'
for attribute in previous_attributes)
def linkified_id(object_id: str, lower: bool=False) -> str:
names_and_urls = {
# Core resources
'ch': ('Charge', 'charges'),
'cus': ('Customer', 'customers'),
'dp': ('Dispute', 'disputes'),
'file': ('File', 'files'),
'link': ('File link', 'file_links'),
'pi': ('Payment intent', 'payment_intents'),
'po': ('Payout', 'payouts'),
'prod': ('Product', 'products'),
're': ('Refund', 'refunds'),
'tok': ('Token', 'tokens'),
# Payment methods
# payment methods have URL prefixes like /customers/cus_id/sources
'ba': ('Bank account', None),
'card': ('Card', None),
'src': ('Source', None),
# Billing
# coupons have a configurable id, but the URL prefix is /coupons
# discounts don't have a URL, I think
'in': ('Invoice', 'invoices'),
'ii': ('Invoice item', 'invoiceitems'),
# products are covered in core resources
# plans have a configurable id, though by default they are created with this pattern
# 'plan': ('Plan', 'plans'),
'sub': ('Subscription', 'subscriptions'),
'si': ('Subscription item', 'subscription_items'),
# I think usage records have URL prefixes like /subscription_items/si_id/usage_record_summaries
'mbur': ('Usage record', None),
# Connect, Fraud, Orders, etc not implemented
} # type: Dict[str, Tuple[str, Optional[str]]]
name, url_prefix = names_and_urls[object_id.split('_')[0]]
if lower: # nocoverage
name = name.lower()
if url_prefix is None: # nocoverage
return name
return '[{}](https://dashboard.stripe.com/{}/{})'.format(name, url_prefix, object_id)