From bd6e730e8eff49ffcd7b1abc54da38ee8d1cfc39 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:56:39 +0530 Subject: [PATCH 1/7] feat: send notification when payment of auto-recharge/susbcription invoice fails --- daras_ai_v2/send_email.py | 2 +- daras_ai_v2/settings.py | 2 +- payments/tasks.py | 33 +++++++++++++++++ payments/webhooks.py | 35 ++++++++++++++++++- routers/stripe.py | 2 ++ templates/base_email.html | 22 ++++++++++++ .../off_session_payment_failed_email.html | 25 +++++++++++++ 7 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 templates/base_email.html create mode 100644 templates/off_session_payment_failed_email.html diff --git a/daras_ai_v2/send_email.py b/daras_ai_v2/send_email.py index a9ff1934d..84741c2dc 100644 --- a/daras_ai_v2/send_email.py +++ b/daras_ai_v2/send_email.py @@ -76,7 +76,7 @@ def send_email_via_postmark( html_body: str = "", text_body: str = "", message_stream: typing.Literal[ - "outbound", "gooey-ai-workflows", "announcements" + "outbound", "gooey-ai-workflows", "announcements", "billing" ] = "outbound", ): if is_running_pytest: diff --git a/daras_ai_v2/settings.py b/daras_ai_v2/settings.py index 88815ea5f..501a132bd 100644 --- a/daras_ai_v2/settings.py +++ b/daras_ai_v2/settings.py @@ -11,7 +11,6 @@ """ import os -import json from pathlib import Path import sentry_sdk @@ -265,6 +264,7 @@ ADMIN_EMAILS = config("ADMIN_EMAILS", cast=Csv(), default="") SUPPORT_EMAIL = "Gooey.AI Support " SALES_EMAIL = "Gooey.AI Sales " +PAYMENT_EMAIL = "Gooey.AI Payments " SEND_RUN_EMAIL_AFTER_SEC = config("SEND_RUN_EMAIL_AFTER_SEC", 5) DISALLOWED_TITLE_SLUGS = config("DISALLOWED_TITLE_SLUGS", cast=Csv(), default="") + [ diff --git a/payments/tasks.py b/payments/tasks.py index 252064541..7627acfef 100644 --- a/payments/tasks.py +++ b/payments/tasks.py @@ -1,3 +1,6 @@ +from typing import Literal + +import stripe from django.utils import timezone from loguru import logger @@ -39,6 +42,36 @@ def send_monthly_spending_notification_email(user_id: int): user.subscription.save(update_fields=["monthly_spending_notification_sent_at"]) +@app.task +def send_payment_failed_email_with_invoice( + uid: str, + invoice_url: str, + dollar_amt: float, + kind: Literal["subscription", "auto recharge"], +): + from routers.account import account_route + + user = AppUser.objects.get(uid=uid) + if not user.email: + logger.error(f"User doesn't have an email: {user=}") + return + + send_email_via_postmark( + from_address=settings.PAYMENT_EMAIL, + to_address=user.email, + subject=f"Payment failure on your Gooey.AI {kind}", + html_body=templates.get_template( + "off_session_payment_failed_email.html" + ).render( + user=user, + dollar_amt=f"{dollar_amt:.2f}", + invoice_url=invoice_url, + account_url=get_app_route_url(account_route), + ), + message_stream="billing", + ) + + def send_monthly_budget_reached_email(user: AppUser): from routers.account import account_route diff --git a/payments/webhooks.py b/payments/webhooks.py index 0b822cfe7..79c788f19 100644 --- a/payments/webhooks.py +++ b/payments/webhooks.py @@ -12,7 +12,10 @@ from daras_ai_v2 import paypal from .models import Subscription from .plans import PricingPlan -from .tasks import send_monthly_spending_notification_email +from .tasks import ( + send_monthly_spending_notification_email, + send_payment_failed_email_with_invoice, +) class PaypalWebhookHandler: @@ -189,6 +192,36 @@ def handle_subscription_cancelled(cls, uid: str): external_id=None, ) + @classmethod + def handle_invoice_failed(cls, uid: str, data: dict): + logger.info(f"Invoice failed: {data}") + + if stripe.Charge.list(payment_intent=data["payment_intent"], limit=1).has_more: + # we must have already sent an invoice for this to the user. so we should just ignore this event + logger.info("Charge already exists for this payment intent") + return + + if data.get("metadata", {}).get("auto_recharge"): + logger.info("auto recharge failed... sending invoice email") + send_payment_failed_email_with_invoice.delay( + uid=uid, + invoice_url=data["hosted_invoice_url"], + dollar_amt=data["amount_due"] / 100, + kind="auto recharge", + ) + elif data.get("subscription_details", {}): + print("subscription failed") + send_payment_failed_email_with_invoice.delay( + uid=uid, + invoice_url=data["hosted_invoice_url"], + dollar_amt=data["amount_due"] / 100, + kind="subscription", + ) + else: + print("not auto recharge or subscription") + print(f"{data.get('metadata')=}") + return + def add_balance_for_payment( *, diff --git a/routers/stripe.py b/routers/stripe.py index aa948ba98..7b1534ecc 100644 --- a/routers/stripe.py +++ b/routers/stripe.py @@ -42,6 +42,8 @@ def webhook_received(request: Request, payload: bytes = fastapi_request_body): match event["type"]: case "invoice.paid": StripeWebhookHandler.handle_invoice_paid(uid, data) + case "invoice.payment_failed": + StripeWebhookHandler.handle_invoice_failed(uid, data) case "checkout.session.completed": StripeWebhookHandler.handle_checkout_session_completed(uid, data) case "customer.subscription.created" | "customer.subscription.updated": diff --git a/templates/base_email.html b/templates/base_email.html new file mode 100644 index 000000000..63ab8b012 --- /dev/null +++ b/templates/base_email.html @@ -0,0 +1,22 @@ + + + + + + + + {% block title %}{% endblock title %} + + {% block head %}{% endblock head %} + + + + + +
+ {% block content %}{% endblock content %} +
+ + + + diff --git a/templates/off_session_payment_failed_email.html b/templates/off_session_payment_failed_email.html new file mode 100644 index 000000000..c8624fec5 --- /dev/null +++ b/templates/off_session_payment_failed_email.html @@ -0,0 +1,25 @@ +{% extends 'base_email.html' %} + +{% block title %}Payment failed{% endblock title %} + +{% block content %} +

Hi {{ user.first_name() }},

+ +

We attempted to process your payment for ${{ dollar_amt }} but your payment method was declined.

+ +

+ Please make a payment on Gooey.AI for continued service or update + your payment method on your account. +

+ +

+ + + +

+ +

+ Cheers,
+ The Gooey.AI team +

+{% endblock content %} From c539af542adda4fe82d0f5688b8598131f82716d Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:00:35 +0530 Subject: [PATCH 2/7] make subject logic for payment failure notification email simpler --- payments/tasks.py | 7 ++----- payments/webhooks.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/payments/tasks.py b/payments/tasks.py index 7627acfef..f0ee87fce 100644 --- a/payments/tasks.py +++ b/payments/tasks.py @@ -1,6 +1,3 @@ -from typing import Literal - -import stripe from django.utils import timezone from loguru import logger @@ -47,7 +44,7 @@ def send_payment_failed_email_with_invoice( uid: str, invoice_url: str, dollar_amt: float, - kind: Literal["subscription", "auto recharge"], + subject: str, ): from routers.account import account_route @@ -59,7 +56,7 @@ def send_payment_failed_email_with_invoice( send_email_via_postmark( from_address=settings.PAYMENT_EMAIL, to_address=user.email, - subject=f"Payment failure on your Gooey.AI {kind}", + subject=subject, html_body=templates.get_template( "off_session_payment_failed_email.html" ).render( diff --git a/payments/webhooks.py b/payments/webhooks.py index 79c788f19..2c1820065 100644 --- a/payments/webhooks.py +++ b/payments/webhooks.py @@ -207,7 +207,7 @@ def handle_invoice_failed(cls, uid: str, data: dict): uid=uid, invoice_url=data["hosted_invoice_url"], dollar_amt=data["amount_due"] / 100, - kind="auto recharge", + subject="Payment failure on your Gooey.AI auto-recharge", ) elif data.get("subscription_details", {}): print("subscription failed") @@ -215,7 +215,7 @@ def handle_invoice_failed(cls, uid: str, data: dict): uid=uid, invoice_url=data["hosted_invoice_url"], dollar_amt=data["amount_due"] / 100, - kind="subscription", + subject="Payment failure on your Gooey.AI subscription", ) else: print("not auto recharge or subscription") From 2edea041e84993328c95bb055aba5a7db367cd94 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:01:41 +0530 Subject: [PATCH 3/7] remove useless debug logging --- payments/webhooks.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/payments/webhooks.py b/payments/webhooks.py index 2c1820065..bff97390f 100644 --- a/payments/webhooks.py +++ b/payments/webhooks.py @@ -194,15 +194,12 @@ def handle_subscription_cancelled(cls, uid: str): @classmethod def handle_invoice_failed(cls, uid: str, data: dict): - logger.info(f"Invoice failed: {data}") - if stripe.Charge.list(payment_intent=data["payment_intent"], limit=1).has_more: # we must have already sent an invoice for this to the user. so we should just ignore this event logger.info("Charge already exists for this payment intent") return if data.get("metadata", {}).get("auto_recharge"): - logger.info("auto recharge failed... sending invoice email") send_payment_failed_email_with_invoice.delay( uid=uid, invoice_url=data["hosted_invoice_url"], @@ -210,17 +207,12 @@ def handle_invoice_failed(cls, uid: str, data: dict): subject="Payment failure on your Gooey.AI auto-recharge", ) elif data.get("subscription_details", {}): - print("subscription failed") send_payment_failed_email_with_invoice.delay( uid=uid, invoice_url=data["hosted_invoice_url"], dollar_amt=data["amount_due"] / 100, subject="Payment failure on your Gooey.AI subscription", ) - else: - print("not auto recharge or subscription") - print(f"{data.get('metadata')=}") - return def add_balance_for_payment( From 8d29766dd969df4b0b837b1de4d910667f6651f3 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:05:37 +0530 Subject: [PATCH 4/7] cleanup: remove base_email.html template --- templates/base_email.html | 22 -------- .../off_session_payment_failed_email.html | 51 ++++++++++++------- 2 files changed, 32 insertions(+), 41 deletions(-) delete mode 100644 templates/base_email.html diff --git a/templates/base_email.html b/templates/base_email.html deleted file mode 100644 index 63ab8b012..000000000 --- a/templates/base_email.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - {% block title %}{% endblock title %} - - {% block head %}{% endblock head %} - - - - - -
- {% block content %}{% endblock content %} -
- - - - diff --git a/templates/off_session_payment_failed_email.html b/templates/off_session_payment_failed_email.html index c8624fec5..6eaaba7d2 100644 --- a/templates/off_session_payment_failed_email.html +++ b/templates/off_session_payment_failed_email.html @@ -1,25 +1,38 @@ -{% extends 'base_email.html' %} + + -{% block title %}Payment failed{% endblock title %} + + + -{% block content %} -

Hi {{ user.first_name() }},

+ + -

We attempted to process your payment for ${{ dollar_amt }} but your payment method was declined.

+ +
-

- Please make a payment on Gooey.AI for continued service or update - your payment method on your account. -

+

Hi {{ user.first_name() }},

-

- - - -

+

We attempted to process your payment for ${{ dollar_amt }} but your payment method was declined.

-

- Cheers,
- The Gooey.AI team -

-{% endblock content %} +

+ Please make a payment on Gooey.AI for continued service or update + your payment method on your account. +

+ +

+ + + +

+ +

+ Cheers,
+ The Gooey.AI team +

+ +
+ + + + From fbb022ee21b08f091a672ad8661a58f4ebdeeb2f Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:06:30 +0530 Subject: [PATCH 5/7] cleanup: rename off_session_payment_failed_email.html -> auto_payment_failed_email.html more obvious --- payments/tasks.py | 4 +--- ...yment_failed_email.html => auto_payment_failed_email.html} | 0 2 files changed, 1 insertion(+), 3 deletions(-) rename templates/{off_session_payment_failed_email.html => auto_payment_failed_email.html} (100%) diff --git a/payments/tasks.py b/payments/tasks.py index f0ee87fce..4a602c3ba 100644 --- a/payments/tasks.py +++ b/payments/tasks.py @@ -57,9 +57,7 @@ def send_payment_failed_email_with_invoice( from_address=settings.PAYMENT_EMAIL, to_address=user.email, subject=subject, - html_body=templates.get_template( - "off_session_payment_failed_email.html" - ).render( + html_body=templates.get_template("auto_payment_failed_email.html").render( user=user, dollar_amt=f"{dollar_amt:.2f}", invoice_url=invoice_url, diff --git a/templates/off_session_payment_failed_email.html b/templates/auto_payment_failed_email.html similarity index 100% rename from templates/off_session_payment_failed_email.html rename to templates/auto_payment_failed_email.html From e000a4d86b43a294366269248e541794fb9aee47 Mon Sep 17 00:00:00 2001 From: anish-work Date: Thu, 5 Sep 2024 16:20:22 +0530 Subject: [PATCH 6/7] add enableConversations config --- daras_ai_v2/bot_integration_widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daras_ai_v2/bot_integration_widgets.py b/daras_ai_v2/bot_integration_widgets.py index 15d89b4d6..36cca09ec 100644 --- a/daras_ai_v2/bot_integration_widgets.py +++ b/daras_ai_v2/bot_integration_widgets.py @@ -426,6 +426,7 @@ def web_widget_config(bi: BotIntegration, user: AppUser | None): enablePhotoUpload=False, autoPlayResponses=True, enableAudioMessage=True, + enableConversations=True, branding=( dict(showPoweredByGooey=True) | bi.web_config_extras.get("branding", {}) @@ -442,6 +443,9 @@ def web_widget_config(bi: BotIntegration, user: AppUser | None): config["enablePhotoUpload"] = gui.checkbox( "Allow Photo Upload", value=config["enablePhotoUpload"] ) + config["enableConversations"] = gui.checkbox( + 'Show "New Chat"', value=config["enableConversations"] + ) with scol2: config["enableAudioMessage"] = gui.checkbox( "Enable Audio Message", value=config["enableAudioMessage"] From 6c9bf391098e1fa9c03b5708cbc748d7320579a0 Mon Sep 17 00:00:00 2001 From: anish-work Date: Thu, 5 Sep 2024 18:04:28 +0530 Subject: [PATCH 7/7] fix integrations tab ui jump --- routers/root.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/root.py b/routers/root.py index b3a636207..e234a5443 100644 --- a/routers/root.py +++ b/routers/root.py @@ -761,7 +761,7 @@ class RecipeTabs(TabData, Enum): route=history_route, ) integrations = TabData( - title=f'Facebook, Whatsapp, Slack, Instagram Icons Integrations', + title=f'Facebook, Whatsapp, Slack, Instagram Icons Integrations', label="Integrations", route=integrations_route, )