mirror of https://github.com/zulip/zulip.git
webhooks/stripe: Update how we handle updated events.
Also more explicitly ignores the Stripe Connect related events in the 'account' category.
This commit is contained in:
parent
5fb683e788
commit
027d5e90c5
|
@ -0,0 +1,188 @@
|
|||
{
|
||||
"id": "evt_1DbqCuDEQaroqDjsIkZIT6uP",
|
||||
"object": "event",
|
||||
"api_version": "2018-11-08",
|
||||
"created": 1543500572,
|
||||
"data": {
|
||||
"object": {
|
||||
"id": "cus_00000000000000",
|
||||
"object": "customer",
|
||||
"account_balance": 100,
|
||||
"created": 1543971209,
|
||||
"currency": "usd",
|
||||
"default_source": "src_1DdoduGh0CmXqmnwKJmq8lpv",
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"invoice_prefix": "6E2831C",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"shipping": null,
|
||||
"sources": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "src_1DdoduGh0CmXqmnwKJmq8lpv",
|
||||
"object": "source",
|
||||
"ach_credit_transfer": {
|
||||
"account_number": "test_a479e261d1bb",
|
||||
"bank_name": "TEST BANK",
|
||||
"fingerprint": "cAzkzdbKgZYS8NhC",
|
||||
"routing_number": "110000000",
|
||||
"swift_code": "TSTEZ122",
|
||||
"refund_routing_number": null,
|
||||
"refund_account_number": null,
|
||||
"refund_account_holder_type": null,
|
||||
"refund_account_holder_name": null
|
||||
},
|
||||
"amount": null,
|
||||
"client_secret": "src_client_secret_E60fLRPcrzhAJNmOjvsUHTli",
|
||||
"created": 1543971214,
|
||||
"currency": "usd",
|
||||
"customer": "cus_E60fF1CuG8K36D",
|
||||
"flow": "receiver",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"owner": {
|
||||
"address": null,
|
||||
"email": "amount_0@stripe.com",
|
||||
"name": null,
|
||||
"phone": null,
|
||||
"verified_address": null,
|
||||
"verified_email": null,
|
||||
"verified_name": null,
|
||||
"verified_phone": null
|
||||
},
|
||||
"receiver": {
|
||||
"address": "110000000-test_a479e261d1bb",
|
||||
"amount_charged": 0,
|
||||
"amount_received": 0,
|
||||
"amount_returned": 0,
|
||||
"refund_attributes_method": "email",
|
||||
"refund_attributes_status": "missing"
|
||||
},
|
||||
"statement_descriptor": null,
|
||||
"status": "pending",
|
||||
"type": "ach_credit_transfer",
|
||||
"usage": "reusable"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/customers/cus_E60fF1CuG8K36D/sources"
|
||||
},
|
||||
"subscriptions": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "sub_E60fxv28JMUWOG",
|
||||
"object": "subscription",
|
||||
"application_fee_percent": null,
|
||||
"billing": "send_invoice",
|
||||
"billing_cycle_anchor": 1543971209,
|
||||
"cancel_at_period_end": false,
|
||||
"canceled_at": null,
|
||||
"created": 1543971209,
|
||||
"current_period_end": 1575507209,
|
||||
"current_period_start": 1543971209,
|
||||
"customer": "cus_E60fF1CuG8K36D",
|
||||
"days_until_due": 30,
|
||||
"default_source": null,
|
||||
"discount": null,
|
||||
"ended_at": null,
|
||||
"items": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "si_E60fDuBL4e55ZF",
|
||||
"object": "subscription_item",
|
||||
"created": 1543971210,
|
||||
"metadata": {
|
||||
},
|
||||
"plan": {
|
||||
"id": "plan_Do3xCvbzO89OsR",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": 8000,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1539831971,
|
||||
"currency": "usd",
|
||||
"interval": "year",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"nickname": "annual",
|
||||
"product": "prod_Do3x494SetTDpx",
|
||||
"tiers": null,
|
||||
"tiers_mode": null,
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"quantity": 8,
|
||||
"subscription": "sub_E60fxv28JMUWOG"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/subscription_items?subscription=sub_E60fxv28JMUWOG"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"plan": {
|
||||
"id": "plan_Do3xCvbzO89OsR",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": 8000,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1539831971,
|
||||
"currency": "usd",
|
||||
"interval": "year",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"nickname": "annual",
|
||||
"product": "prod_Do3x494SetTDpx",
|
||||
"tiers": null,
|
||||
"tiers_mode": null,
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"quantity": 8,
|
||||
"start": 1543971210,
|
||||
"status": "active",
|
||||
"tax_percent": 0,
|
||||
"trial_end": null,
|
||||
"trial_start": null
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/customers/cus_E60fF1CuG8K36D/subscriptions"
|
||||
},
|
||||
"tax_info": null,
|
||||
"tax_info_verification": null
|
||||
},
|
||||
"previous_attributes": {
|
||||
"account_balance": 0
|
||||
}
|
||||
},
|
||||
"livemode": true,
|
||||
"pending_webhooks": 1,
|
||||
"request": {
|
||||
"id": null,
|
||||
"idempotency_key": null
|
||||
},
|
||||
"type": "customer.updated"
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
{
|
||||
"id": "evt_1DbqCuDEQaroqDjsIkZIT6uP",
|
||||
"object": "event",
|
||||
"api_version": "2018-11-08",
|
||||
"created": 1543500572,
|
||||
"data": {
|
||||
"object": {
|
||||
"id": "cus_00000000000000",
|
||||
"object": "customer",
|
||||
"account_balance": 0,
|
||||
"created": 1535559697,
|
||||
"currency": "usd",
|
||||
"default_source": "src_1DBQGZDEQaroqDjsfHSp24Hn",
|
||||
"delinquent": false,
|
||||
"description": "Acme corp",
|
||||
"discount": {
|
||||
"object": "discount",
|
||||
"coupon": {
|
||||
"id": "8nj6KZjp",
|
||||
"object": "coupon",
|
||||
"amount_off": null,
|
||||
"created": 1542077780,
|
||||
"currency": null,
|
||||
"duration": "forever",
|
||||
"duration_in_months": null,
|
||||
"livemode": true,
|
||||
"max_redemptions": null,
|
||||
"metadata": {
|
||||
},
|
||||
"name": "85% discount",
|
||||
"percent_off": 85,
|
||||
"redeem_by": null,
|
||||
"times_redeemed": 7,
|
||||
"valid": true
|
||||
},
|
||||
"customer": "cus_DVXVXVNyLFSY9I",
|
||||
"end": null,
|
||||
"start": 1542387411,
|
||||
"subscription": null
|
||||
},
|
||||
"email": "user@acme.com",
|
||||
"invoice_prefix": "C0CDBB3",
|
||||
"livemode": true,
|
||||
"metadata": {
|
||||
},
|
||||
"shipping": {
|
||||
"address": {
|
||||
"city": "Pacific",
|
||||
"country": "AA",
|
||||
"line1": "Attn.: Road Runner",
|
||||
"line2": "PO Box 1234",
|
||||
"postal_code": "1234 AA",
|
||||
"state": ""
|
||||
},
|
||||
"name": "",
|
||||
"phone": null
|
||||
},
|
||||
"sources": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "src_1DBQGZDEQaroqDjsfHSp24Hn",
|
||||
"object": "source",
|
||||
"ach_credit_transfer": {
|
||||
"account_number": "1234",
|
||||
"bank_name": "WELLS FARGO BANK, N.A.",
|
||||
"fingerprint": "lfPIkuf8skm5HD7v",
|
||||
"routing_number": "1234",
|
||||
"swift_code": "WFBIUS6S",
|
||||
"refund_routing_number": null,
|
||||
"refund_account_number": null,
|
||||
"refund_account_holder_type": null,
|
||||
"refund_account_holder_name": null
|
||||
},
|
||||
"amount": null,
|
||||
"client_secret": "src_client_secret_DcfbFOQwJOCAfKoNgiVeT6lp",
|
||||
"created": 1537204327,
|
||||
"currency": "usd",
|
||||
"customer": "cus_DVXVXVNyLFSY9I",
|
||||
"flow": "receiver",
|
||||
"livemode": true,
|
||||
"metadata": {
|
||||
},
|
||||
"owner": {
|
||||
"address": null,
|
||||
"email": "user@acme.com",
|
||||
"name": null,
|
||||
"phone": null,
|
||||
"verified_address": null,
|
||||
"verified_email": null,
|
||||
"verified_name": null,
|
||||
"verified_phone": null
|
||||
},
|
||||
"receiver": {
|
||||
"address": "1234-1234",
|
||||
"amount_charged": 100,
|
||||
"amount_received": 100,
|
||||
"amount_returned": 0,
|
||||
"refund_attributes_method": "email",
|
||||
"refund_attributes_status": "missing"
|
||||
},
|
||||
"statement_descriptor": null,
|
||||
"status": "pending",
|
||||
"type": "ach_credit_transfer",
|
||||
"usage": "reusable"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/customers/cus_DVXVXVNyLFSY9I/sources"
|
||||
},
|
||||
"subscriptions": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 0,
|
||||
"url": "/v1/customers/cus_DVXVXVNyLFSY9I/subscriptions"
|
||||
},
|
||||
"tax_info": null,
|
||||
"tax_info_verification": null
|
||||
},
|
||||
"previous_attributes": {
|
||||
"delinquent": true
|
||||
}
|
||||
},
|
||||
"livemode": true,
|
||||
"pending_webhooks": 1,
|
||||
"request": {
|
||||
"id": null,
|
||||
"idempotency_key": null
|
||||
},
|
||||
"type": "customer.updated"
|
||||
}
|
|
@ -84,11 +84,11 @@ Quantity: 1"""
|
|||
expected_topic, expected_message,
|
||||
content_type="application/x-www-form-urlencoded")
|
||||
|
||||
def test_customer_updated__delinquency(self) -> None:
|
||||
def test_customer_updated__account_balance(self) -> None:
|
||||
expected_topic = "cus_00000000000000"
|
||||
expected_message = "[Customer](https://dashboard.stripe.com/customers/cus_00000000000000) updated" + \
|
||||
"\n* Delinquent is now False"
|
||||
self.send_and_test_stream_message('customer_updated__delinquency', expected_topic, expected_message,
|
||||
"\n* Account balance is now 100"
|
||||
self.send_and_test_stream_message('customer_updated__account_balance', expected_topic, expected_message,
|
||||
content_type="application/x-www-form-urlencoded")
|
||||
|
||||
def test_customer_discount_created(self) -> None:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Webhooks for external integrations.
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
@ -16,6 +16,9 @@ from zerver.models import UserProfile
|
|||
class NotImplementedEventType(Exception):
|
||||
pass
|
||||
|
||||
class SuppressedEvent(Exception):
|
||||
pass
|
||||
|
||||
@api_key_only_webhook_view('Stripe')
|
||||
@has_request_variables
|
||||
def api_stripe_webhook(request: HttpRequest, user_profile: UserProfile,
|
||||
|
@ -26,6 +29,8 @@ def api_stripe_webhook(request: HttpRequest, user_profile: UserProfile,
|
|||
check_send_webhook_message(request, user_profile, topic, body)
|
||||
except NotImplementedEventType: # nocoverage
|
||||
pass
|
||||
except SuppressedEvent: # nocoverage
|
||||
pass
|
||||
return json_success()
|
||||
|
||||
def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
||||
|
@ -47,14 +52,29 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||
topic = customer_id
|
||||
body = None
|
||||
|
||||
def default_body() -> str:
|
||||
return '{resource} {verbed}'.format(
|
||||
def update_string(blacklist: List[str]=[]) -> str:
|
||||
assert('previous_attributes' in payload['data'])
|
||||
previous_attributes = payload['data']['previous_attributes']
|
||||
for attribute in blacklist:
|
||||
previous_attributes.pop(attribute, None)
|
||||
if not previous_attributes: # nocoverage
|
||||
raise SuppressedEvent()
|
||||
return ''.join('\n* ' + attribute.replace('_', ' ').capitalize() +
|
||||
' is now ' + str(object_[attribute])
|
||||
for attribute in sorted(previous_attributes.keys()))
|
||||
|
||||
def default_body(update_blacklist: List[str]=[]) -> str:
|
||||
body = '{resource} {verbed}'.format(
|
||||
resource=linkified_id(object_['id']), verbed=event.replace('_', ' '))
|
||||
if event == 'updated':
|
||||
return body + update_string(blacklist=update_blacklist)
|
||||
return body
|
||||
|
||||
if category == 'account': # nocoverage
|
||||
if event == 'updated':
|
||||
topic = "account updates"
|
||||
body = ''
|
||||
if resource == 'account':
|
||||
if event == 'updated':
|
||||
topic = "account updates"
|
||||
body = update_string()
|
||||
else:
|
||||
# Part of Stripe Connect
|
||||
raise NotImplementedEventType()
|
||||
|
@ -93,7 +113,7 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||
# Running into the 60 character topic limit.
|
||||
# topic = '[{}](https://dashboard.stripe.com/customers/{})' % (object_['id'], object_['id'])
|
||||
topic = object_['id']
|
||||
body = default_body()
|
||||
body = default_body(update_blacklist=['delinquent', 'currency', 'default_source'])
|
||||
if event == 'created':
|
||||
if object_['email']:
|
||||
body += '\nEmail: {}'.format(object_['email'])
|
||||
|
@ -131,7 +151,7 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||
if event == 'upcoming': # nocoverage
|
||||
body = 'Upcoming invoice created'
|
||||
else:
|
||||
body = default_body()
|
||||
body = default_body(update_blacklist=['lines', 'description', 'number', 'finalized_at'])
|
||||
if event == 'created': # nocoverage
|
||||
# Could potentially add link to invoice PDF here
|
||||
body += ' ({reason})\nBilling method: {method}\nTotal: {total}\nAmount due: {due}'.format(
|
||||
|
@ -140,7 +160,7 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||
total=amount_string(object_['total'], object_['currency']),
|
||||
due=amount_string(object_['amount_due'], object_['currency']))
|
||||
if category == 'invoiceitem': # nocoverage
|
||||
body = default_body()
|
||||
body = default_body(update_blacklist=['description'])
|
||||
if event == 'created':
|
||||
body += ' for {amount}'.format(amount=amount_string(object_['amount'], object_['currency']))
|
||||
if category.startswith('issuing'): # nocoverage
|
||||
|
@ -159,13 +179,6 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||
|
||||
if body is None:
|
||||
raise UnexpectedWebhookEventType('Stripe', event_type)
|
||||
|
||||
if 'previous_attributes' in payload['data']: # nocoverage
|
||||
previous_attributes = payload['data']['previous_attributes']
|
||||
else:
|
||||
previous_attributes = {}
|
||||
body += update_string(object_, previous_attributes)
|
||||
body = body.strip()
|
||||
return (topic, body)
|
||||
|
||||
def amount_string(amount: int, currency: str) -> str:
|
||||
|
@ -180,10 +193,6 @@ def amount_string(amount: int, currency: str) -> str:
|
|||
return '$' + decimal_amount
|
||||
return decimal_amount + ' {}'.format(currency.upper())
|
||||
|
||||
def update_string(object_: Dict[str, Any], previous_attributes: Dict[str, Any]) -> str:
|
||||
return ''.join('\n* ' + attribute.replace('_', ' ').capitalize() + ' is now ' + str(object_[attribute])
|
||||
for attribute in previous_attributes)
|
||||
|
||||
def linkified_id(object_id: str, lower: bool=False) -> str:
|
||||
names_and_urls = {
|
||||
# Core resources
|
||||
|
|
Loading…
Reference in New Issue