Repository: zulip/zulip

Issue:
webhooks/stripe: Handle invoice and payout events explicitly.
Currently, we support `invoice` events in our Stripe webhooks, but only implicitly (i.e., we let a generic function `default_body()` create a message for most `invoice` events). We don't support `payout` events at all. We should probably consider supporting these events in one form or another.

@timabbott, @rishig: Opening this so we don't forget. I'll add this to my queue for when I get around to restructuring the webhook in general. Thanks! :)


Discussion:
Hello @zulip/server-integrations members, this issue was labeled with the "area: integrations" label, so you may want to check it out!

<!-- areaLabelAddition -->
@zulipbot claim
Hello @sameerchoubey!

Thanks for your interest in Zulip! You have attempted to claim an issue without the labels "help wanted", "good first issue". Since you're a new contributor, you can only claim and submit pull requests for issues with the [help wanted](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22help+wanted%22) or [good first issue](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22good+first+issue%22) labels.

If this is your first time here, we recommend reading our [guide for new contributors](https://zulip.readthedocs.io/en/latest/overview/contributing.html) before getting started.
I'm working on this.
@zulipbot claim
Welcome to Zulip, @sameerchoubey! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:
* Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
* Sign the [Dropbox Contributor License Agreement](https://opensource.dropbox.com/cla/), so that Zulip can use your code.
* [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!
@zulipbot claim

Resolving patch:
diff --git a/zerver/webhooks/stripe/view.py b/zerver/webhooks/stripe/view.py
--- a/zerver/webhooks/stripe/view.py
+++ b/zerver/webhooks/stripe/view.py
@@ -14,6 +14,19 @@
     UnexpectedWebhookEventType
 from zerver.models import UserProfile
 
+INVOICE_INFO = {
+    "created": "has been created",
+    "deleted": "has been deleted",
+    "finalized": "has been finalized",
+    "marked_uncollectible": "has been marked as uncollectible",
+    "payment_failed": "has failed",
+    "payment_succeeded": "has succeeded",
+    "sent": "has been sent",
+    "upcoming": "has been created",
+    "updated": "has been updated",
+    "voided": "has been voided"
+}
+
 class SuppressedEvent(Exception):
     pass
 
@@ -148,18 +161,6 @@ def default_body(update_blacklist: List[str]=[]) -> str:
         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:
-            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(
-                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(update_blacklist=['description'])
         if event == 'created':
@@ -198,6 +199,28 @@ def default_body(update_blacklist: List[str]=[]) -> str:
             amount=amount,
             end=end
         )
+
+    if category.startswith('invoice'):
+        object_id = object_['id']
+        link = "https://dashboard.stripe.com/invoices/{}".format(object_id)
+        amount = amount_string(object_["amount_due"], object_["currency"])
+        body_t = "An {invoice_type} **[invoice]({link})** for the payment of amount **{amount}** {end}."
+
+        end = INVOICE_INFO.get(event)  # type: ignore # expression has type Optional[str]
+
+        if event == "upcoming":
+            invoice_type = "upcoming"
+        else:
+            invoice_type = ""
+
+        topic = "invoice {}".format(object_id)
+        body = body_t.format(
+            invoice_type = invoice_type,
+            link=link,
+            amount=amount,
+            end=end
+        )
+
     if category in ['payment_intent', 'plan', 'product', 'recipient',
                     'reporting', 'review', 'sigma', 'sku', 'source', 'subscription_schedule',
                     'topup', 'transfer']:  # nocoverage


Test patch:
diff --git a/zerver/webhooks/stripe/tests.py b/zerver/webhooks/stripe/tests.py
--- a/zerver/webhooks/stripe/tests.py
+++ b/zerver/webhooks/stripe/tests.py
@@ -112,12 +112,6 @@ def test_customer_discount_created(self) -> None:
         self.send_and_test_stream_message('customer_discount_created', expected_topic, expected_message,
                                           content_type="application/x-www-form-urlencoded")
 
-    def test_invoice_payment_failed(self) -> None:
-        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_payout_canceled(self) -> None:
         expected_topic = u"Payout po_00000000000000"
         expected_message = u"**[Payout](https://dashboard.stripe.com/payout/po_00000000000000)** for amount **$11.00** has been canceled."
@@ -152,3 +146,62 @@ def test_payout_updated(self) -> None:
 
         self.send_and_test_stream_message('payout_updated', expected_topic, expected_message,
                                           content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_created(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has been created."
+        self.send_and_test_stream_message('invoice_created', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_deleted(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has been deleted."
+        self.send_and_test_stream_message('invoice_deleted', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_finalized(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has been finalized."
+        self.send_and_test_stream_message('invoice_finalized', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_marked_uncollectible(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has been marked as uncollectible."
+        self.send_and_test_stream_message('invoice_marked_uncollectible', expected_topic, expected_message, 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](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has failed."
+        self.send_and_test_stream_message('invoice_payment_failed', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_payment_succeeded(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has succeeded."
+        self.send_and_test_stream_message('invoice_payment_succeeded', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_sent(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has been sent."
+        self.send_and_test_stream_message('invoice_sent', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_upcoming(self) -> None:
+        expected_topic = u"invoice None"
+        expected_message = u"An upcoming **[invoice](https://dashboard.stripe.com/invoices/None)** for the payment of amount **$10.00** has been created."
+        self.send_and_test_stream_message('invoice_upcoming', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_updated(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has been updated."
+        self.send_and_test_stream_message('invoice_updated', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")
+
+    def test_invoice_voided(self) -> None:
+        expected_topic = u"invoice in_00000000000000"
+        expected_message = u"An  **[invoice](https://dashboard.stripe.com/invoices/in_00000000000000)** for the payment of amount **$10.00** has been voided."
+        self.send_and_test_stream_message('invoice_voided', expected_topic, expected_message,
+                                          content_type="application/x-www-form-urlencoded")