From 5099769eac978333a3ebad9d8ce950e812ea905f Mon Sep 17 00:00:00 2001 From: clr-li <111320104+clr-li@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:59:51 -0800 Subject: [PATCH 1/6] Still need to check top up time --- app_users/admin.py | 1 + .../0012_appuser_low_balance_email_sent_at.py | 18 +++++++++++++ app_users/models.py | 2 ++ daras_ai_v2/base.py | 25 ++++++++++++++++++- daras_ai_v2/send_email.py | 20 +++++++++++++++ templates/low_balance_email.html | 16 ++++++++++++ 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 app_users/migrations/0012_appuser_low_balance_email_sent_at.py create mode 100644 templates/low_balance_email.html diff --git a/app_users/admin.py b/app_users/admin.py index 191197eb6..ffe8ccdc6 100644 --- a/app_users/admin.py +++ b/app_users/admin.py @@ -41,6 +41,7 @@ class AppUserAdmin(admin.ModelAdmin): "view_transactions", "open_in_firebase", "open_in_stripe", + "low_balance_email_sent_at", ] @admin.display(description="User Runs") diff --git a/app_users/migrations/0012_appuser_low_balance_email_sent_at.py b/app_users/migrations/0012_appuser_low_balance_email_sent_at.py new file mode 100644 index 000000000..efc2beaf0 --- /dev/null +++ b/app_users/migrations/0012_appuser_low_balance_email_sent_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-02-14 07:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app_users', '0011_appusertransaction_charged_amount_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='appuser', + name='low_balance_email_sent_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/app_users/models.py b/app_users/models.py index 576ea0390..d4f714e96 100644 --- a/app_users/models.py +++ b/app_users/models.py @@ -89,6 +89,8 @@ class AppUser(models.Model): stripe_customer_id = models.CharField(max_length=255, default="", blank=True) is_paying = models.BooleanField("paid", default=False) + low_balance_email_sent_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField( "created", editable=False, blank=True, default=timezone.now ) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index d6243aeb7..ae1150b63 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -17,6 +17,7 @@ import sentry_sdk from django.utils import timezone from django.utils.text import slugify +from django.db.models import Sum from fastapi import HTTPException from firebase_admin import auth from furl import furl @@ -57,7 +58,7 @@ from daras_ai_v2.query_params_util import ( extract_query_params, ) -from daras_ai_v2.send_email import send_reported_run_email +from daras_ai_v2.send_email import send_reported_run_email, send_low_balance_email from daras_ai_v2.tabs_widget import MenuTabs from daras_ai_v2.user_date_widgets import ( render_js_dynamic_dates, @@ -1387,6 +1388,28 @@ def estimate_run_duration(self) -> int | None: def on_submit(self): example_id, run_id, uid = self.create_new_run() + if ( + self.request.user.is_paying + and self.request.user.balance < 500 + # and ( + # self.request.user.low_balance_email_sent_at == None + # or self.request.user.low_balance_email_sent_at + # < timezone.now() - datetime.timedelta(days=7) + # ) + ): + credits_consumed = ( + -1 + * AppUserTransaction.objects.filter( + user=self.request.user, + created_at__gte=timezone.now() - datetime.timedelta(days=7), + ).aggregate(Sum("amount"))["amount__sum"] + ) + send_low_balance_email( + user=self.request.user, credits_consumed=credits_consumed + ) + app_user = AppUser.objects.get(uid=uid) + app_user.low_balance_email_sent_at = timezone.now() + app_user.save() if settings.CREDITS_TO_DEDUCT_PER_RUN and not self.check_credits(): st.session_state[StateKeys.run_status] = None st.session_state[StateKeys.error_msg] = self.generate_credit_error_message( diff --git a/daras_ai_v2/send_email.py b/daras_ai_v2/send_email.py index 6c3f0bccb..fea83a855 100644 --- a/daras_ai_v2/send_email.py +++ b/daras_ai_v2/send_email.py @@ -43,6 +43,26 @@ def send_reported_run_email( ) +def send_low_balance_email( + *, + user: AppUser, + credits_consumed: int, +): + recipeints = "support@gooey.ai, devs@gooey.ai" + html_body = templates.get_template("low_balance_email.html").render( + user=user, + url="https://gooey.ai/account", + credits_consumed=credits_consumed, + ) + send_email_via_postmark( + from_address=settings.SUPPORT_EMAIL, + to_address=user.email or recipeints, + bcc=recipeints, + subject="Your Gooey.AI credit balance is low ", + html_body=html_body, + ) + + def send_email_via_postmark( *, from_address: str, diff --git a/templates/low_balance_email.html b/templates/low_balance_email.html new file mode 100644 index 000000000..b7dfa7850 --- /dev/null +++ b/templates/low_balance_email.html @@ -0,0 +1,16 @@ +
+ Hey {{ user.display_name }}! +
+ +
+ This is a friendly reminder that your Gooey.AI balance is now just {{ user.balance }}. Your account has consumed {{ credits_consumed }} credits in the last 7 days.
+
+ To buy more credits, please visit https://gooey.ai/account.
+
+ As always, email us at sales@gooey.ai if you have any questions too.
+
+ Thanks again for your business,
+
+ Sean and the Gooey.AI team
+
- This is a friendly reminder that your Gooey.AI balance is now just {{ user.balance }}. Your account has consumed {{ credits_consumed }} credits in the last 7 days.
+ This is a friendly reminder that your Gooey.AI balance is now just {{ user.balance }}. Your account has consumed {{ total_credits_consumed }} credits in the last 7 days.
To buy more credits, please visit https://gooey.ai/account.
From 618609e2b0e6ba25513756fb936256241722feb2 Mon Sep 17 00:00:00 2001
From: clr-li <111320104+clr-li@users.noreply.github.com>
Date: Wed, 14 Feb 2024 23:16:10 -0800
Subject: [PATCH 4/6] Fixed checks on sending
---
celeryapp/tasks.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py
index 8a4f6a260..d9c70c425 100644
--- a/celeryapp/tasks.py
+++ b/celeryapp/tasks.py
@@ -112,6 +112,15 @@ def save(done=False):
def low_balance_email(sr: SavedRun):
user = AppUser.objects.get(uid=sr.uid)
+ last_positive_transaction = (
+ AppUserTransaction.objects.filter(user=user, amount__gt=0)
+ .order_by("-created_at")
+ .first()
+ )
+ if last_positive_transaction:
+ last_positive_transaction = last_positive_transaction.created_at
+ else:
+ last_positive_transaction = timezone.now() - datetime.timedelta(days=8)
if (
user.is_paying
and user.balance < 500
@@ -119,6 +128,7 @@ def low_balance_email(sr: SavedRun):
user.low_balance_email_sent_at == None
or user.low_balance_email_sent_at
< timezone.now() - datetime.timedelta(days=7)
+ or last_positive_transaction > user.low_balance_email_sent_at
)
):
total_credits_consumed = (
@@ -126,6 +136,7 @@ def low_balance_email(sr: SavedRun):
* AppUserTransaction.objects.filter(
user=user,
created_at__gte=timezone.now() - datetime.timedelta(days=7),
+ amount__lt=0,
).aggregate(Sum("amount"))["amount__sum"]
)
send_low_balance_email(
From 0f6070bc3b6d1e2d81c17a3cc0579ca232272848 Mon Sep 17 00:00:00 2001
From: clr-li <111320104+clr-li@users.noreply.github.com>
Date: Thu, 15 Feb 2024 14:49:14 -0800
Subject: [PATCH 5/6] Added settings for toggling email sending
---
celeryapp/tasks.py | 3 ++-
daras_ai_v2/settings.py | 3 +++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py
index d9c70c425..c09028a57 100644
--- a/celeryapp/tasks.py
+++ b/celeryapp/tasks.py
@@ -123,7 +123,8 @@ def low_balance_email(sr: SavedRun):
last_positive_transaction = timezone.now() - datetime.timedelta(days=8)
if (
user.is_paying
- and user.balance < 500
+ and user.balance < settings.EMAIL_CREDITS_THRESHOLD
+ and settings.ALLOW_SENDING_CREDIT_EMAILS
and (
user.low_balance_email_sent_at == None
or user.low_balance_email_sent_at
diff --git a/daras_ai_v2/settings.py b/daras_ai_v2/settings.py
index 512fb3834..951003ae8 100644
--- a/daras_ai_v2/settings.py
+++ b/daras_ai_v2/settings.py
@@ -266,6 +266,9 @@
ANON_USER_FREE_CREDITS = config("ANON_USER_FREE_CREDITS", 25, cast=int)
LOGIN_USER_FREE_CREDITS = config("LOGIN_USER_FREE_CREDITS", 1000, cast=int)
+EMAIL_CREDITS_THRESHOLD = config("EMAIL_CREDITS_THRESHOLD", 500, cast=int)
+ALLOW_SENDING_CREDIT_EMAILS = config("ALLOW_SENDING_CREDIT_EMAILS", True, cast=bool)
+
stripe.api_key = config("STRIPE_SECRET_KEY", None)
STRIPE_ENDPOINT_SECRET = config("STRIPE_ENDPOINT_SECRET", None)
From 2589be3b6118e5ebfe40f3fb20b0042226ef760d Mon Sep 17 00:00:00 2001
From: Dev Aggarwal
- This is a friendly reminder that your Gooey.AI balance is now just {{ user.balance }}. Your account has consumed {{ total_credits_consumed }} credits in the last 7 days.
+ This is a friendly reminder that your Gooey.AI balance is now just {{ user.balance }}.
+ Your account has consumed {{ total_credits_consumed }} credits in the last {{ settings.LOW_BALANCE_EMAIL_DAYS }} days.
To buy more credits, please visit https://gooey.ai/account.
diff --git a/tests/test_low_balance_email_check.py b/tests/test_low_balance_email_check.py
new file mode 100644
index 000000000..66203a19b
--- /dev/null
+++ b/tests/test_low_balance_email_check.py
@@ -0,0 +1,127 @@
+from django.utils import timezone
+
+from app_users.models import AppUserTransaction
+from bots.models import AppUser
+from celeryapp.tasks import run_low_balance_email_check
+from daras_ai_v2 import settings
+from daras_ai_v2.send_email import pytest_outbox
+
+
+def test_dont_send_email_if_feature_is_disabled(transactional_db):
+ user = AppUser.objects.create(
+ uid="test_user", is_paying=True, balance=0, is_anonymous=False
+ )
+ settings.LOW_BALANCE_EMAIL_ENABLED = False
+ run_low_balance_email_check(user.uid)
+ assert not pytest_outbox
+
+
+def test_dont_send_email_if_user_is_not_paying(transactional_db):
+ user = AppUser.objects.create(
+ uid="test_user", is_paying=False, balance=0, is_anonymous=False
+ )
+ settings.LOW_BALANCE_EMAIL_ENABLED = True
+ run_low_balance_email_check(user.uid)
+ assert not pytest_outbox
+
+
+def test_dont_send_email_if_user_has_enough_balance(transactional_db):
+ user = AppUser.objects.create(
+ uid="test_user", is_paying=True, balance=500, is_anonymous=False
+ )
+ settings.LOW_BALANCE_EMAIL_CREDITS = 100
+ settings.LOW_BALANCE_EMAIL_ENABLED = True
+ run_low_balance_email_check(user.uid)
+ assert not pytest_outbox
+
+
+def test_dont_send_email_if_user_has_been_emailed_recently(transactional_db):
+ user = AppUser.objects.create(
+ uid="test_user",
+ is_paying=True,
+ balance=66,
+ is_anonymous=False,
+ low_balance_email_sent_at=timezone.now(),
+ )
+ settings.LOW_BALANCE_EMAIL_ENABLED = True
+ settings.LOW_BALANCE_EMAIL_DAYS = 1
+ settings.LOW_BALANCE_EMAIL_CREDITS = 100
+ run_low_balance_email_check(user.uid)
+ assert not pytest_outbox
+
+
+def test_send_email_if_user_has_been_email_recently_but_made_a_purchase(
+ transactional_db,
+):
+ user = AppUser.objects.create(
+ uid="test_user",
+ is_paying=True,
+ balance=22,
+ is_anonymous=False,
+ low_balance_email_sent_at=timezone.now(),
+ )
+ AppUserTransaction.objects.create(
+ invoice_id="test_invoice_1",
+ user=user,
+ amount=100,
+ created_at=timezone.now(),
+ end_balance=100,
+ )
+ AppUserTransaction.objects.create(
+ invoice_id="test_invoice_2",
+ user=user,
+ amount=-78,
+ created_at=timezone.now(),
+ end_balance=22,
+ )
+ settings.LOW_BALANCE_EMAIL_ENABLED = True
+ settings.LOW_BALANCE_EMAIL_DAYS = 1
+ settings.LOW_BALANCE_EMAIL_CREDITS = 100
+ run_low_balance_email_check(user.uid)
+
+ assert len(pytest_outbox) == 1
+ assert " 22" in pytest_outbox[0]["html_body"]
+ assert " 78" in pytest_outbox[0]["html_body"]
+
+ pytest_outbox.clear()
+ run_low_balance_email_check(user.uid)
+ assert not pytest_outbox
+
+
+def test_send_email(transactional_db):
+ user = AppUser.objects.create(
+ uid="test_user",
+ is_paying=True,
+ balance=66,
+ is_anonymous=False,
+ )
+ AppUserTransaction.objects.create(
+ invoice_id="test_invoice_1",
+ user=user,
+ amount=-100,
+ created_at=timezone.now() - timezone.timedelta(days=2),
+ end_balance=150 + 66,
+ )
+ AppUserTransaction.objects.create(
+ invoice_id="test_invoice_2",
+ user=user,
+ amount=-150,
+ created_at=timezone.now(),
+ end_balance=66,
+ )
+
+ settings.LOW_BALANCE_EMAIL_ENABLED = True
+ settings.LOW_BALANCE_EMAIL_DAYS = 1
+ settings.LOW_BALANCE_EMAIL_CREDITS = 100
+
+ run_low_balance_email_check(user.uid)
+ assert len(pytest_outbox) == 1
+ body = pytest_outbox[0]["html_body"]
+ assert " 66" in body
+ assert " 150" in body
+ assert " pm:unsubscribe" in body
+ assert " 100" not in body
+
+ pytest_outbox.clear()
+ run_low_balance_email_check(user.uid)
+ assert not pytest_outbox