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 the event types you would like to be notified about, and click
**Add endpoint**. **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 Zulip currently supports Stripe events for Charges, Customers, Discounts,
* Charge Dispute Created Sources, Subscriptions, Files, Invoices and Invoice items.
* 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.
{% if 'http:' in external_uri_scheme %} {% 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' FIXTURE_DIR_NAME = 'stripe'
def test_charge_dispute_closed(self) -> None: def test_charge_dispute_closed(self) -> None:
expected_topic = u"Charge ch_00000000000000" expected_topic = u"disputes"
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)**." expected_message = u"[Dispute](https://dashboard.stripe.com/disputes/dp_00000000000000) closed. Current status: won."
# use fixture named stripe_charge_dispute_closed
self.send_and_test_stream_message('charge_dispute_closed', expected_topic, expected_message, self.send_and_test_stream_message('charge_dispute_closed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") content_type="application/x-www-form-urlencoded")
def test_charge_dispute_created(self) -> None: def test_charge_dispute_created(self) -> None:
expected_topic = u"Charge ch_00000000000000" expected_topic = u"disputes"
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)**." expected_message = u"[Dispute](https://dashboard.stripe.com/disputes/dp_00000000000000) created. Current status: needs response."
# use fixture named stripe_charge_dispute_created
self.send_and_test_stream_message('charge_dispute_created', expected_topic, expected_message, self.send_and_test_stream_message('charge_dispute_created', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") content_type="application/x-www-form-urlencoded")
def test_charge_failed(self) -> None: def test_charge_failed(self) -> None:
expected_topic = u"Charge ch_00000000000000" expected_topic = u"charges"
expected_message = u"A charge with id **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)** for **1.00aud** has failed." expected_message = u"[Charge](https://dashboard.stripe.com/charges/ch_00000000000000) for 1.00 AUD failed"
# use fixture named stripe_charge_failed
self.send_and_test_stream_message('charge_failed', expected_topic, expected_message, self.send_and_test_stream_message('charge_failed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") content_type="application/x-www-form-urlencoded")
def test_charge_succeeded(self) -> None: def test_charge_succeeded(self) -> None:
expected_topic = u"Charge ch_00000000000000" expected_topic = u"charges"
expected_message = u"A charge with id **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)** for **1.00aud** has succeeded." expected_message = u"[Charge](https://dashboard.stripe.com/charges/ch_00000000000000) for 1.00 AUD succeeded"
# use fixture named stripe_charge_succeeded
self.send_and_test_stream_message('charge_succeeded', expected_topic, expected_message, self.send_and_test_stream_message('charge_succeeded', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") 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: def test_customer_created(self) -> None:
expected_topic = u"Customer cus_00000000000000" expected_topic = u"cus_00000000000000"
expected_message = u"A new customer with id **[cus_00000000000000](https://dashboard.stripe.com/customers/cus_00000000000000)** has been created." expected_message = u"[Customer](https://dashboard.stripe.com/customers/cus_00000000000000) created"
# use fixture named stripe_customer_created
self.send_and_test_stream_message('customer_created', expected_topic, expected_message, self.send_and_test_stream_message('customer_created', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") content_type="application/x-www-form-urlencoded")
def test_customer_deleted(self) -> None: def test_customer_created_email(self) -> None:
expected_topic = u"Customer cus_00000000000000" expected_topic = u"cus_00000000000000"
expected_message = u"A customer with id **[cus_00000000000000](https://dashboard.stripe.com/customers/cus_00000000000000)** has been deleted." 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, self.send_and_test_stream_message('customer_deleted', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") content_type="application/x-www-form-urlencoded")
def test_customer_subscription_created(self) -> None: def test_customer_subscription_created(self) -> None:
expected_topic = u"Customer sub_00000000000000" expected_topic = u"cus_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)**." expected_message = u"""\
[Subscription](https://dashboard.stripe.com/subscriptions/sub_00000000000000) created
# use fixture named stripe_customer_subscription_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, self.send_and_test_stream_message('customer_subscription_created', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") content_type="application/x-www-form-urlencoded")
def test_customer_subscription_deleted(self) -> None: def test_customer_subscription_deleted(self) -> None:
expected_topic = u"Customer sub_00000000000000" expected_topic = u"cus_00000000000000"
expected_message = u"The customer subscription with id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)** was deleted." expected_message = u"[Subscription](https://dashboard.stripe.com/subscriptions/sub_00000000000000) deleted"
# use fixture named stripe_customer_subscription_deleted
self.send_and_test_stream_message('customer_subscription_deleted', expected_topic, expected_message, self.send_and_test_stream_message('customer_subscription_deleted', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") 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: def test_customer_subscription_trial_will_end(self) -> None:
expected_topic = u"Customer sub_00000000000000" expected_topic = u"cus_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_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 # 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): with mock.patch('time.time', return_value=1480892861 - 3*3600*24 + 100):
# use fixture named stripe_customer_subscription_trial_will_end # use fixture named stripe_customer_subscription_trial_will_end
@@ -100,52 +77,7 @@ class StripeHookTests(WebhookTestCase):
content_type="application/x-www-form-urlencoded") content_type="application/x-www-form-urlencoded")
def test_invoice_payment_failed(self) -> None: def test_invoice_payment_failed(self) -> None:
expected_topic = u"Invoice in_00000000000000" expected_topic = u"cus_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." expected_message = u"[Invoice](https://dashboard.stripe.com/invoices/in_00000000000000) payment failed"
# use fixture named stripe_invoice_payment_failed
self.send_and_test_stream_message('invoice_payment_failed', expected_topic, expected_message, self.send_and_test_stream_message('invoice_payment_failed', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded") 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. # Webhooks for external integrations.
import time import time
from datetime import datetime 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.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _ 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, def api_stripe_webhook(request: HttpRequest, user_profile: UserProfile,
payload: Dict[str, Any]=REQ(argument_type='body'), payload: Dict[str, Any]=REQ(argument_type='body'),
stream: str=REQ(default='test')) -> HttpResponse: 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 body = None
event_type = payload["type"]
data_object = payload["data"]["object"]
if event_type.startswith('charge'):
charge_url = "https://dashboard.stripe.com/payments/{}" def default_body() -> str:
amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) return '{resource} {verbed}'.format(
resource=linkified_id(object_['id']), verbed=event.replace('_', ' '))
if event_type.startswith('charge.dispute'): if category == 'account': # nocoverage
charge_id = data_object["charge"] if event == 'updated':
link = charge_url.format(charge_id) topic = "account updates"
body_template = "A charge dispute for **{amount}** has been {rest}.\n"\ body = ''
"The charge in dispute {verb} **[{charge}]({link})**."
if event_type == "charge.dispute.closed":
rest = "closed as **{}**".format(data_object['status'])
verb = 'was'
else: else:
rest = "created" # Part of Stripe Connect
verb = 'is' return json_success()
if category == 'application_fee': # nocoverage
body = body_template.format(amount=amount_string, # Part of Stripe Connect
rest=rest, return json_success()
verb=verb, if category == 'balance': # nocoverage
charge=charge_id, # Not that interesting to most businesses, I think
link=link) return json_success()
if category == 'charge':
else: if resource == 'charge':
charge_id = data_object["id"] if not topic:
link = charge_url.format(charge_id) topic = 'charges'
body_template = "A charge with id **[{charge_id}]({link})** for **{amount}** has {verb}." body = "{resource} for {amount} {verbed}".format(
resource=linkified_id(object_['id']),
if event_type == "charge.failed": amount=amount_string(object_['amount'], object_['currency']), verbed=event)
verb = "failed" if object_['failure_code']: # nocoverage
else: body += '. Failure code: {}'.format(object_['failure_code'])
verb = "succeeded" if resource == 'dispute':
body = body_template.format(charge_id=charge_id, link=link, amount=amount_string, verb=verb) topic = 'disputes'
body = default_body() + '. Current status: {status}.'.format(
topic = "Charge {}".format(charge_id) status=object_['status'].replace('_', ' '))
if resource == 'refund': # nocoverage
elif event_type.startswith('customer'): topic = 'refunds'
object_id = data_object["id"] body = 'A {resource} for a {charge} of {amount} was updated.'.format(
if event_type.startswith('customer.subscription'): resource=linkified_id(object_['id'], lower=True),
link = "https://dashboard.stripe.com/subscriptions/{}".format(object_id) charge=linkified_id(object_['charge'], lower=True), amount=object_['amount'])
if category == 'checkout_beta': # nocoverage
if event_type == "customer.subscription.created": # Not sure what this is
amount_string = amount(data_object["plan"]["amount"], data_object["plan"]["currency"]) return json_success()
if category == 'coupon': # nocoverage
body_template = "A new customer subscription for **{amount}** " \ # Not something that likely happens programmatically
"every **{interval}** has been created.\n" \ return json_success()
"The subscription has id **[{id}]({link})**." if category == 'customer':
body = body_template.format( if resource == 'customer':
amount=amount_string, # Running into the 60 character topic limit.
interval=data_object['plan']['interval'], # topic = '[{}](https://dashboard.stripe.com/customers/{})' % (object_['id'], object_['id'])
id=object_id, topic = object_['id']
link=link body = default_body()
) if event == 'created':
if object_['email']:
elif event_type == "customer.subscription.deleted": body += '\nEmail: {}'.format(object_['email'])
body_template = "The customer subscription with id **[{id}]({link})** was deleted." if object_['metadata']: # nocoverage
body = body_template.format(id=object_id, link=link) for key, value in object_['metadata'].items():
body += '\n{}: {}'.format(key, value)
elif event_type == "customer.subscription.trial_will_end": 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 DAY = 60 * 60 * 24 # seconds in a day
# days_left should always be three according to # Basically always three: https://stripe.com/docs/api/python#event_types
# https://stripe.com/docs/api/python#event_types, but do the body += ' in {days} days'.format(
# computation just to be safe. days=int((object_["trial_end"] - time.time() + DAY//2) // DAY))
days_left = int((data_object["trial_end"] - time.time() + DAY//2) // DAY) if event == 'created':
body_template = ("The customer subscription trial with id" if object_['plan']:
" **[{id}]({link})** will end in {days} days.") body += '\nPlan: [{plan_name}](https://dashboard.stripe.com/plans/{plan_id})'.format(
body = body_template.format(id=object_id, link=link, days=days_left) plan_name=object_['plan']['name'], plan_id=object_['plan']['id'])
elif event_type == "customer.subscription.updated": if object_['quantity']:
body_template = "The customer subscription with id **[{id}]({link})** was updated." body += '\nQuantity: {}'.format(object_['quantity'])
body = body_template.format(id=object_id, link=link) 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: else:
raise UnexpectedWebhookEventType("Stripe", event_type) body = default_body()
else: if event == 'created': # nocoverage
link = "https://dashboard.stripe.com/customers/{}".format(object_id) # Could potentially add link to invoice PDF here
body_template = "{beginning} customer with id **[{id}]({link})** {rest}." body += ' ({reason})\nBilling method: {method}\nTotal: {total}\nAmount due: {due}'.format(
reason=object_['billing_reason'].replace('_', ' '),
if event_type == "customer.created": method=object_['billing'].replace('_', ' '),
beginning = "A new" total=amount_string(object_['total'], object_['currency']),
if data_object["email"] is None: due=amount_string(object_['amount_due'], object_['currency']))
rest = "has been created" if category == 'invoiceitem': # nocoverage
else: body = default_body()
rest = "and email **{}** has been created".format(data_object['email']) if event == 'created':
elif event_type == "customer.deleted": body += ' for {amount}'.format(amount=amount_string(object_['amount'], object_['currency']))
beginning = "A" if category.startswith('issuing'): # nocoverage
rest = "has been deleted" # Not implemented
else: return json_success()
raise UnexpectedWebhookEventType("Stripe", event_type) if category.startswith('order'): # nocoverage
body = body_template.format(beginning=beginning, id=object_id, link=link, rest=rest) # Not implemented
return json_success()
topic = "Customer {}".format(object_id) if category in ['payment_intent', 'payout', 'plan', 'product', 'recipient',
'reporting', 'review', 'sigma', 'sku', 'source', 'subscription_schedule',
elif event_type == "invoice.payment_failed": 'topup', 'transfer']: # nocoverage
object_id = data_object['id'] # Not implemented. In theory doing something like
link = "https://dashboard.stripe.com/invoices/{}".format(object_id) # body = default_body()
amount_string = amount(data_object["amount_due"], data_object["currency"]) # may not be hard for some of these
body_template = "An invoice payment on invoice with id **[{id}]({link})** and "\ return json_success()
"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)
if body is None: if body is None:
raise UnexpectedWebhookEventType('Stripe', event_type) 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() return json_success()
def amount(amount: int, currency: str) -> str: def amount_string(amount: int, currency: str) -> str:
# zero-decimal currencies
zero_decimal_currencies = ["bif", "djf", "jpy", "krw", "pyg", "vnd", "xaf", zero_decimal_currencies = ["bif", "djf", "jpy", "krw", "pyg", "vnd", "xaf",
"xpf", "clp", "gnf", "kmf", "mga", "rwf", "vuv", "xof"] "xpf", "clp", "gnf", "kmf", "mga", "rwf", "vuv", "xof"]
if currency in zero_decimal_currencies: if currency in zero_decimal_currencies:
return str(amount) + currency decimal_amount = str(amount) # nocoverage
else: 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)