diff --git a/static/images/integrations/logos/stripe.png b/static/images/integrations/logos/stripe.png new file mode 100644 index 0000000000..046894bf5f Binary files /dev/null and b/static/images/integrations/logos/stripe.png differ diff --git a/zerver/fixtures/stripe/stripe_charge_dispute_closed.json b/zerver/fixtures/stripe/stripe_charge_dispute_closed.json index 80108b0c09..1939d17d36 100644 --- a/zerver/fixtures/stripe/stripe_charge_dispute_closed.json +++ b/zerver/fixtures/stripe/stripe_charge_dispute_closed.json @@ -11,7 +11,7 @@ "object": { "id": "dp_00000000000000", "object": "dispute", - "amount": 1000, + "amount": 1001, "balance_transactions": [], "charge": "ch_00000000000000", "created": 1480672052, diff --git a/zerver/fixtures/stripe/stripe_charge_dispute_created.json b/zerver/fixtures/stripe/stripe_charge_dispute_created.json index 1380260e83..81663cb95b 100644 --- a/zerver/fixtures/stripe/stripe_charge_dispute_created.json +++ b/zerver/fixtures/stripe/stripe_charge_dispute_created.json @@ -15,7 +15,7 @@ "balance_transactions": [], "charge": "ch_00000000000000", "created": 1480672043, - "currency": "aud", + "currency": "jpy", "evidence": { "access_activity_log": null, "billing_address": null, diff --git a/zerver/fixtures/stripe/stripe_customer_created_email.json b/zerver/fixtures/stripe/stripe_customer_created_email.json new file mode 100644 index 0000000000..daad6ac13a --- /dev/null +++ b/zerver/fixtures/stripe/stripe_customer_created_email.json @@ -0,0 +1,41 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.created", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2016-07-06", + "data": { + "object": { + "id": "cus_00000000000000", + "object": "customer", + "account_balance": 0, + "created": 1480672088, + "currency": "aud", + "default_source": null, + "delinquent": false, + "description": null, + "discount": null, + "email": "example@abc.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_9fYl7K4F7pQxrY/sources" + }, + "subscriptions": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_9fYl7K4F7pQxrY/subscriptions" + } + } + } +} diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index 9db539e6e6..12612dcbcb 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -127,6 +127,7 @@ WEBHOOK_INTEGRATIONS = [ WebhookIntegration('semaphore'), WebhookIntegration('sentry'), WebhookIntegration('stash'), + WebhookIntegration('stripe', display_name='Stripe'), WebhookIntegration('taiga'), WebhookIntegration('teamcity'), WebhookIntegration('transifex'), diff --git a/zerver/tests/webhooks/test_stripe.py b/zerver/tests/webhooks/test_stripe.py new file mode 100644 index 0000000000..70f6f846ec --- /dev/null +++ b/zerver/tests/webhooks/test_stripe.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +from six import text_type +from zerver.lib.test_classes import WebhookTestCase + +class StripeHookTests(WebhookTestCase): + STREAM_NAME = 'test' + URL_TEMPLATE = "/api/v1/external/stripe?&api_key={api_key}" + FIXTURE_DIR_NAME = 'stripe' + + def test_charge_dispute_closed(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"A charge dispute for **10.01aud** has been closed as **won**.\nThe charge in dispute was **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)**." + + # use fixture named stripe_charge_dispute_closed + self.send_and_test_stream_message('charge_dispute_closed', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_charge_dispute_created(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"A charge dispute for **1000jpy** has been created.\nThe charge in dispute is **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)**." + + # use fixture named stripe_charge_dispute_created + self.send_and_test_stream_message('charge_dispute_created', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_charge_failed(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"A charge with id **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)** for **1.00aud** has failed." + + # use fixture named stripe_charge_failed + self.send_and_test_stream_message('charge_failed', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_charge_succeeded(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"A charge with id **[ch_00000000000000](https://dashboard.stripe.com/payments/ch_00000000000000)** for **1.00aud** has succeeded." + + # use fixture named stripe_charge_succeeded + self.send_and_test_stream_message('charge_succeeded', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_customer_created_email(self): + # type: () -> None + expected_subject = u"stripe" + 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_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_customer_created(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"A new customer with id **[cus_00000000000000](https://dashboard.stripe.com/customers/cus_00000000000000)** has been created." + + # use fixture named stripe_customer_created + self.send_and_test_stream_message('customer_created', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_customer_deleted(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"A customer with id **[cus_00000000000000](https://dashboard.stripe.com/customers/cus_00000000000000)** has been deleted." + + # use fixture named stripe_customer_deleted + self.send_and_test_stream_message('customer_deleted', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_customer_subscription_created(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"A new customer subscription for **20.00aud** every **month** has been created.\nThe subscription has id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)**." + + # use fixture named stripe_customer_subscription_created + self.send_and_test_stream_message('customer_subscription_created', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_customer_subscription_deleted(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"The customer subscription with id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)** was deleted." + + # use fixture named stripe_customer_subscription_deleted + self.send_and_test_stream_message('customer_subscription_deleted', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_customer_subscription_trial_will_end(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"The customer subscription trial with id **[sub_00000000000000](https://dashboard.stripe.com/subscriptions/sub_00000000000000)** will end on Dec 04 2016 at 06:07PM" + + # use fixture named stripe_customer_subscription_trial_will_end + self.send_and_test_stream_message('customer_subscription_trial_will_end', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_invoice_payment_failed(self): + # type: () -> None + expected_subject = u"stripe" + expected_message = u"An invoice payment on invoice with id **[in_00000000000000](https://dashboard.stripe.com/invoices/in_00000000000000)** and with **0.00aud** due has failed." + + # use fixture named stripe_invoice_payment_failed + self.send_and_test_stream_message('invoice_payment_failed', expected_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_order_payment_failed(self): + # type: () -> None + expected_subject = u"stripe" + 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_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_order_payment_succeeded(self): + # type: () -> None + expected_subject = u"stripe" + 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_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_order_updated(self): + # type: () -> None + expected_subject = u"stripe" + 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_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_transfer_failed(self): + # type: () -> None + expected_subject = u"stripe" + 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_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def test_transfer_paid(self): + # type: () -> None + expected_subject = u"stripe" + 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_subject, expected_message, + content_type="application/x-www-form-urlencoded") + + def get_body(self, fixture_name): + # type: (text_type) -> text_type + return self.fixture_data("stripe", fixture_name, file_type="json") diff --git a/zerver/views/webhooks/stripe.py b/zerver/views/webhooks/stripe.py new file mode 100644 index 0000000000..35ae4c7f20 --- /dev/null +++ b/zerver/views/webhooks/stripe.py @@ -0,0 +1,126 @@ +# Webhooks for external integrations. +from __future__ import absolute_import +from django.utils.translation import ugettext as _ +from zerver.lib.actions import check_send_message +from zerver.lib.response import json_success, json_error +from zerver.decorator import REQ, has_request_variables, api_key_only_webhook_view +from zerver.lib.validator import check_dict, check_string +from zerver.models import Client, UserProfile + +from django.http import HttpRequest, HttpResponse +from six import text_type +from typing import Dict, Any, Iterable, Optional + +from datetime import datetime + +@api_key_only_webhook_view('Stripe') +@has_request_variables +def api_stripe_webhook(request, user_profile, client, + payload=REQ(argument_type='body'), stream=REQ(default='test'), + topic=REQ(default='stripe')): + # type: (HttpRequest, UserProfile, Client, Dict[str, Any], text_type, Optional[text_type]) -> HttpResponse + body = "" + event_type = "" + try: + event_type = payload["type"] + if event_type == "charge.dispute.closed": + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + link = "https://dashboard.stripe.com/payments/"+payload["data"]["object"]["charge"] + body_template = "A charge dispute for **" + amount_string + "** has been closed as **{object[status]}**.\n"\ + + "The charge in dispute was **[{object[charge]}](" + link + ")**." + body = body_template.format(**(payload["data"])) + elif event_type == "charge.dispute.created": + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + link = "https://dashboard.stripe.com/payments/"+payload["data"]["object"]["charge"] + body_template = "A charge dispute for **" + amount_string + "** has been created.\n"\ + + "The charge in dispute is **[{object[charge]}](" + link + ")**." + body = body_template.format(**(payload["data"])) + elif event_type == "charge.failed": + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + link = "https://dashboard.stripe.com/payments/"+payload["data"]["object"]["id"] + body_template = "A charge with id **[{object[id]}](" + link + ")** for **" + amount_string + "** has failed." + body = body_template.format(**(payload["data"])) + elif event_type == "charge.succeeded": + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + link = "https://dashboard.stripe.com/payments/"+payload["data"]["object"]["id"] + body_template = "A charge with id **[{object[id]}](" + link + ")** for **" + amount_string + "** has succeeded." + body = body_template.format(**(payload["data"])) + elif event_type == "customer.created": + link = "https://dashboard.stripe.com/customers/"+payload["data"]["object"]["id"] + if payload["data"]["object"]["email"] is None: + body_template = "A new customer with id **[{object[id]}](" + link + ")** has been created." + body = body_template.format(**(payload["data"])) + else: + body_template = "A new customer with id **[{object[id]}](" + link + ")** and email **{object[email]}** has been created." + body = body_template.format(**(payload["data"])) + elif event_type == "customer.deleted": + link = "https://dashboard.stripe.com/customers/"+payload["data"]["object"]["id"] + body_template = "A customer with id **[{object[id]}](" + link + ")** has been deleted." + body = body_template.format(**(payload["data"])) + elif event_type == "customer.subscription.created": + amount_string = amount(payload["data"]["object"]["plan"]["amount"], payload["data"]["object"]["plan"]["currency"]) + link = "https://dashboard.stripe.com/subscriptions/"+payload["data"]["object"]["id"] + body_template = "A new customer subscription for **" + amount_string + "** every **{plan[interval]}** has been created.\n" + body_template += "The subscription has id **[{id}](" + link + ")**." + body = body_template.format(**(payload["data"]["object"])) + elif event_type == "customer.subscription.deleted": + link = "https://dashboard.stripe.com/subscriptions/"+payload["data"]["object"]["id"] + body_template = "The customer subscription with id **[{object[id]}](" + link + ")** was deleted." + body = body_template.format(**(payload["data"])) + elif event_type == "customer.subscription.trial_will_end": + link = "https://dashboard.stripe.com/subscriptions/"+payload["data"]["object"]["id"] + body_template = "The customer subscription trial with id **[{object[id]}](" + link + ")** will end on " + body_template += datetime.fromtimestamp(payload["data"]["object"]["trial_end"]).strftime('%b %d %Y at %I:%M%p') + body = body_template.format(**(payload["data"])) + elif event_type == "invoice.payment_failed": + link = "https://dashboard.stripe.com/invoices/"+payload["data"]["object"]["id"] + amount_string = amount(payload["data"]["object"]["amount_due"], payload["data"]["object"]["currency"]) + body_template = "An invoice payment on invoice with id **[{object[id]}](" + link + ")** and with **"\ + + amount_string + "** due has failed." + body = body_template.format(**(payload["data"])) + elif event_type == "order.payment_failed": + link = "https://dashboard.stripe.com/orders/"+payload["data"]["object"]["id"] + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + body_template = "An order payment on order with id **[{object[id]}](" + link + ")** for **" + amount_string + "** has failed." + body = body_template.format(**(payload["data"])) + elif event_type == "order.payment_succeeded": + link = "https://dashboard.stripe.com/orders/"+payload["data"]["object"]["id"] + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + body_template = "An order payment on order with id **[{object[id]}](" + link + ")** for **"\ + + amount_string + "** has succeeded." + body = body_template.format(**(payload["data"])) + elif event_type == "order.updated": + link = "https://dashboard.stripe.com/orders/"+payload["data"]["object"]["id"] + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + body_template = "The order with id **[{object[id]}](" + link + ")** for **" + amount_string + "** has been updated." + body = body_template.format(**(payload["data"])) + elif event_type == "transfer.failed": + link = "https://dashboard.stripe.com/transfers/"+payload["data"]["object"]["id"] + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + body_template = "The transfer with description **{object[description]}** and id **[{object[id]}]("\ + + link + ")** for amount **"\ + + amount_string + "** has failed." + body = body_template.format(**(payload["data"])) + elif event_type == "transfer.paid": + link = "https://dashboard.stripe.com/transfers/"+payload["data"]["object"]["id"] + amount_string = amount(payload["data"]["object"]["amount"], payload["data"]["object"]["currency"]) + body_template = "The transfer with description **{object[description]}** and id **[{object[id]}]("\ + + link + ")** for amount **"\ + + amount_string + "** has been paid." + body = body_template.format(**(payload["data"])) + except KeyError as e: + body = "Missing key {} in JSON".format(str(e)) + + # send the message + check_send_message(user_profile, client, 'stream', [stream], topic, body) + + return json_success() + +def amount(amount, currency): + # type: (int, str) -> str + # zero-decimal currencies + zero_decimal_currencies = ["bif", "djf", "jpy", "krw", "pyg", "vnd", "xaf", "xpf", "clp", "gnf", "kmf", "mga", "rwf", "vuv", "xof"] + if currency in zero_decimal_currencies: + return str(amount) + currency + else: + return '{0:.02f}'.format(float(amount) * 0.01) + currency