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 +

+{{ "{{{ pm:unsubscribe }}}" }} \ No newline at end of file From 1c055430a0b9f0d4efa3a51112ee28c66832132e Mon Sep 17 00:00:00 2001 From: clr-li <111320104+clr-li@users.noreply.github.com> Date: Wed, 14 Feb 2024 21:01:10 -0800 Subject: [PATCH 2/6] Fix linter --- bots/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bots/models.py b/bots/models.py index 3a440ab73..c55ef1f74 100644 --- a/bots/models.py +++ b/bots/models.py @@ -935,7 +935,9 @@ def to_df_format( else None ), # only show first feedback as per Sean's request "Analysis JSON": message.analysis_result, - "Run Time": message.saved_run.run_time if message.saved_run else 0, # user messages have no run/run_time + "Run Time": ( + message.saved_run.run_time if message.saved_run else 0 + ), # user messages have no run/run_time } rows.append(row) df = pd.DataFrame.from_records( From a84933b224a73fb5647c3292f0628b3bf38d1774 Mon Sep 17 00:00:00 2001 From: clr-li <111320104+clr-li@users.noreply.github.com> Date: Wed, 14 Feb 2024 21:59:54 -0800 Subject: [PATCH 3/6] Correct credit count --- celeryapp/tasks.py | 33 +++++++++++++++++++++++++++++++- daras_ai_v2/base.py | 25 +----------------------- daras_ai_v2/send_email.py | 6 +++--- templates/low_balance_email.html | 2 +- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py index 5e1690b31..8a4f6a260 100644 --- a/celeryapp/tasks.py +++ b/celeryapp/tasks.py @@ -1,12 +1,13 @@ import traceback import typing +import datetime from time import time from types import SimpleNamespace import sentry_sdk import gooey_ui as st -from app_users.models import AppUser +from app_users.models import AppUser, AppUserTransaction from bots.models import SavedRun from celeryapp.celeryconfig import app from daras_ai.image_input import truncate_text_words @@ -16,6 +17,9 @@ from daras_ai_v2.settings import templates from gooey_ui.pubsub import realtime_push from gooey_ui.state import set_query_params +from daras_ai_v2.send_email import send_low_balance_email +from django.db.models import Sum +from django.utils import timezone @app.task @@ -101,10 +105,37 @@ def save(done=False): save() finally: save(done=True) + low_balance_email(sr) if not is_api_call: send_email_on_completion(page, sr) +def low_balance_email(sr: SavedRun): + user = AppUser.objects.get(uid=sr.uid) + if ( + user.is_paying + and user.balance < 500 + and ( + user.low_balance_email_sent_at == None + or user.low_balance_email_sent_at + < timezone.now() - datetime.timedelta(days=7) + ) + ): + total_credits_consumed = ( + -1 + * AppUserTransaction.objects.filter( + user=user, + created_at__gte=timezone.now() - datetime.timedelta(days=7), + ).aggregate(Sum("amount"))["amount__sum"] + ) + send_low_balance_email( + user=user, + total_credits_consumed=total_credits_consumed, + ) + user.low_balance_email_sent_at = timezone.now() + user.save() + + def send_email_on_completion(page: BasePage, sr: SavedRun): run_time_sec = sr.run_time.total_seconds() if ( diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index ae1150b63..d6243aeb7 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -17,7 +17,6 @@ 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 @@ -58,7 +57,7 @@ from daras_ai_v2.query_params_util import ( extract_query_params, ) -from daras_ai_v2.send_email import send_reported_run_email, send_low_balance_email +from daras_ai_v2.send_email import send_reported_run_email from daras_ai_v2.tabs_widget import MenuTabs from daras_ai_v2.user_date_widgets import ( render_js_dynamic_dates, @@ -1388,28 +1387,6 @@ 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 fea83a855..fc2445f41 100644 --- a/daras_ai_v2/send_email.py +++ b/daras_ai_v2/send_email.py @@ -46,19 +46,19 @@ def send_reported_run_email( def send_low_balance_email( *, user: AppUser, - credits_consumed: int, + total_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, + total_credits_consumed=total_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 ", + subject="Your Gooey.AI credit balance is low", html_body=html_body, ) diff --git a/templates/low_balance_email.html b/templates/low_balance_email.html index b7dfa7850..d3215c861 100644 --- a/templates/low_balance_email.html +++ b/templates/low_balance_email.html @@ -3,7 +3,7 @@

- 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 Date: Wed, 28 Feb 2024 20:45:40 +0530 Subject: [PATCH 6/6] write tests for low balance email check, add indexes, reduce cuttoff to 200 credits avoid hardcoding urls and dates --- app_users/models.py | 4 + celeryapp/tasks.py | 65 +++++++------ conftest.py | 6 ++ daras_ai_v2/send_email.py | 24 ++++- daras_ai_v2/settings.py | 5 +- routers/billing.py | 4 +- templates/low_balance_email.html | 3 +- tests/test_low_balance_email_check.py | 127 ++++++++++++++++++++++++++ 8 files changed, 198 insertions(+), 40 deletions(-) create mode 100644 tests/test_low_balance_email_check.py diff --git a/app_users/models.py b/app_users/models.py index d4f714e96..4c7e716e2 100644 --- a/app_users/models.py +++ b/app_users/models.py @@ -265,6 +265,10 @@ class AppUserTransaction(models.Model): class Meta: verbose_name = "Transaction" + indexes = [ + models.Index(fields=["user", "amount", "-created_at"]), + models.Index(fields=["-created_at"]), + ] def __str__(self): return f"{self.invoice_id} ({self.amount})" diff --git a/celeryapp/tasks.py b/celeryapp/tasks.py index 777aa27ec..2cf56fcdc 100644 --- a/celeryapp/tasks.py +++ b/celeryapp/tasks.py @@ -1,13 +1,15 @@ -from fastapi import HTTPException +import datetime import html import traceback import typing -import datetime from time import time from types import SimpleNamespace import requests import sentry_sdk +from django.db.models import Sum +from django.utils import timezone +from fastapi import HTTPException import gooey_ui as st from app_users.models import AppUser, AppUserTransaction @@ -18,13 +20,11 @@ from daras_ai_v2.base import StateKeys, BasePage from daras_ai_v2.exceptions import UserError from daras_ai_v2.send_email import send_email_via_postmark +from daras_ai_v2.send_email import send_low_balance_email from daras_ai_v2.settings import templates from gooey_ui.pubsub import realtime_push from gooey_ui.state import set_query_params from gooeysite.bg_db_conn import db_middleware, next_db_safe -from daras_ai_v2.send_email import send_low_balance_email -from django.db.models import Sum -from django.utils import timezone @app.task @@ -128,9 +128,9 @@ def save(done=False): save() finally: save(done=True) - low_balance_email(sr) if not is_api_call: send_email_on_completion(page, sr) + run_low_balance_email_check(uid) def err_msg_for_exc(e: Exception): @@ -158,42 +158,41 @@ def err_msg_for_exc(e: Exception): return f"{type(e).__name__}: {e}" -def low_balance_email(sr: SavedRun): - user = AppUser.objects.get(uid=sr.uid) - last_positive_transaction = ( +def run_low_balance_email_check(uid: str): + # don't send email if feature is disabled + if not settings.LOW_BALANCE_EMAIL_ENABLED: + return + user = AppUser.objects.get(uid=uid) + # don't send email if user is not paying or has enough balance + if not user.is_paying or user.balance > settings.LOW_BALANCE_EMAIL_CREDITS: + return + last_purchase = ( 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) + email_date_cutoff = timezone.now() - datetime.timedelta( + days=settings.LOW_BALANCE_EMAIL_DAYS + ) + # send email if user has not been sent email in last X days or last purchase was after last email sent if ( - user.is_paying - 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 - < timezone.now() - datetime.timedelta(days=7) - or last_positive_transaction > user.low_balance_email_sent_at - ) + # user has not been sent any email + not user.low_balance_email_sent_at + # user was sent email before X days + or (user.low_balance_email_sent_at < email_date_cutoff) + # user has made a purchase after last email sent + or (last_purchase and last_purchase.created_at > user.low_balance_email_sent_at) ): - total_credits_consumed = ( - -1 - * AppUserTransaction.objects.filter( - user=user, - created_at__gte=timezone.now() - datetime.timedelta(days=7), - amount__lt=0, + # calculate total credits consumed in last X days + total_credits_consumed = abs( + AppUserTransaction.objects.filter( + user=user, amount__lt=0, created_at__gte=email_date_cutoff ).aggregate(Sum("amount"))["amount__sum"] + or 0 ) - send_low_balance_email( - user=user, - total_credits_consumed=total_credits_consumed, - ) + send_low_balance_email(user=user, total_credits_consumed=total_credits_consumed) user.low_balance_email_sent_at = timezone.now() - user.save() + user.save(update_fields=["low_balance_email_sent_at"]) def send_email_on_completion(page: BasePage, sr: SavedRun): diff --git a/conftest.py b/conftest.py index dba333885..d535b2b47 100644 --- a/conftest.py +++ b/conftest.py @@ -9,6 +9,7 @@ from auth import auth_backend from celeryapp import app from daras_ai_v2.base import BasePage +from daras_ai_v2.send_email import pytest_outbox @pytest.fixture(scope="session") @@ -68,6 +69,11 @@ def runner(*args, **kwargs): t.join() +@pytest.fixture(autouse=True) +def clear_pytest_outbox(): + pytest_outbox.clear() + + # class DummyDatabaseBlocker(pytest_django.plugin._DatabaseBlocker): # class _dj_db_wrapper: # def ensure_connection(self): diff --git a/daras_ai_v2/send_email.py b/daras_ai_v2/send_email.py index fc2445f41..aaf1eb59b 100644 --- a/daras_ai_v2/send_email.py +++ b/daras_ai_v2/send_email.py @@ -1,4 +1,5 @@ import smtplib +import sys import typing from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart @@ -12,6 +13,7 @@ from daras_ai_v2 import settings from daras_ai_v2.settings import templates from gooey_ui import UploadedFile +from routers.billing import account_url def send_reported_run_email( @@ -51,8 +53,9 @@ def send_low_balance_email( recipeints = "support@gooey.ai, devs@gooey.ai" html_body = templates.get_template("low_balance_email.html").render( user=user, - url="https://gooey.ai/account", + url=account_url, total_credits_consumed=total_credits_consumed, + settings=settings, ) send_email_via_postmark( from_address=settings.SUPPORT_EMAIL, @@ -63,6 +66,10 @@ def send_low_balance_email( ) +is_running_pytest = "pytest" in sys.modules +pytest_outbox = [] + + def send_email_via_postmark( *, from_address: str, @@ -76,6 +83,21 @@ def send_email_via_postmark( "outbound", "gooey-ai-workflows", "announcements" ] = "outbound", ): + if is_running_pytest: + pytest_outbox.append( + dict( + from_address=from_address, + to_address=to_address, + cc=cc, + bcc=bcc, + subject=subject, + html_body=html_body, + text_body=text_body, + message_stream=message_stream, + ), + ) + return + r = requests.post( "https://api.postmarkapp.com/email", headers={ diff --git a/daras_ai_v2/settings.py b/daras_ai_v2/settings.py index 42ad40e16..8aed12e7b 100644 --- a/daras_ai_v2/settings.py +++ b/daras_ai_v2/settings.py @@ -268,8 +268,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) +LOW_BALANCE_EMAIL_CREDITS = config("LOW_BALANCE_EMAIL_CREDITS", 200, cast=int) +LOW_BALANCE_EMAIL_DAYS = config("LOW_BALANCE_EMAIL_DAYS", 7, cast=int) +LOW_BALANCE_EMAIL_ENABLED = config("LOW_BALANCE_EMAIL_ENABLED", True, cast=bool) stripe.api_key = config("STRIPE_SECRET_KEY", None) STRIPE_ENDPOINT_SECRET = config("STRIPE_ENDPOINT_SECRET", None) diff --git a/routers/billing.py b/routers/billing.py index bf660325b..39dfa61fb 100644 --- a/routers/billing.py +++ b/routers/billing.py @@ -121,9 +121,7 @@ def account(request: Request): "user_credits": request.user.balance, "subscription": get_user_subscription(request.user), "is_admin": is_admin, - "canonical_url": str( - furl(settings.APP_BASE_URL) / router.url_path_for(account.__name__) - ), + "canonical_url": account_url, } return templates.TemplateResponse("account.html", context) diff --git a/templates/low_balance_email.html b/templates/low_balance_email.html index d3215c861..ca4a65e61 100644 --- a/templates/low_balance_email.html +++ b/templates/low_balance_email.html @@ -3,7 +3,8 @@

- 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