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 %}