mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 14:35:27 +00:00
webhooks: Update Stripe integration.
This commit is contained in:
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user