Skip to content

Commit

Permalink
Merge pull request #379 from GooeyAI/pricing-v2-improvements
Browse files Browse the repository at this point in the history
Pricing improvements and bug fixes
  • Loading branch information
devxpy authored Jul 14, 2024
2 parents 0b4d2f2 + e6b9955 commit 0146f0c
Show file tree
Hide file tree
Showing 32 changed files with 1,110 additions and 623 deletions.
19 changes: 16 additions & 3 deletions app_users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,26 +192,39 @@ class AppUserTransactionAdmin(admin.ModelAdmin):
"invoice_id",
"user",
"amount",
"dollar_amount",
"end_balance",
"payment_provider",
"dollar_amount",
"reason",
"plan",
"created_at",
]
readonly_fields = ["created_at"]
readonly_fields = ["view_payment_provider_url", "created_at"]
list_filter = [
"created_at",
"reason",
("payment_provider", admin.EmptyFieldListFilter),
"payment_provider",
"plan",
"created_at",
]
inlines = [SavedRunInline]
ordering = ["-created_at"]
search_fields = ["invoice_id"]

@admin.display(description="Charged Amount")
def dollar_amount(self, obj: models.AppUserTransaction):
if not obj.payment_provider:
return
return f"${obj.charged_amount / 100}"

@admin.display(description="Payment Provider URL")
def view_payment_provider_url(self, txn: models.AppUserTransaction):
url = txn.payment_provider_url()
if url:
return open_in_new_tab(url, label=url)
else:
raise txn.DoesNotExist


@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 4.2.7 on 2024-07-14 20:51

from django.db import migrations, models


def forwards_func(apps, schema_editor):
from payments.plans import PricingPlan
from app_users.models import TransactionReason

# We get the model from the versioned app registry;
# if we directly import it, it'll be the wrong version
AppUserTransaction = apps.get_model("app_users", "AppUserTransaction")
db_alias = schema_editor.connection.alias
objects = AppUserTransaction.objects.using(db_alias)

for transaction in objects.all():
if transaction.amount <= 0:
transaction.reason = TransactionReason.DEDUCT
else:
# For old transactions, we didn't have a subscription field.
# It just so happened that all monthly subscriptions we offered had
# different amounts from the one-time purchases.
# This uses that heuristic to determine whether a transaction
# was a subscription payment or a one-time purchase.
transaction.reason = TransactionReason.ADDON
for plan in PricingPlan:
if (
transaction.amount == plan.credits
and transaction.charged_amount == plan.monthly_charge * 100
):
transaction.plan = plan.db_value
transaction.reason = TransactionReason.SUBSCRIBE
transaction.save(update_fields=["reason", "plan"])


class Migration(migrations.Migration):

dependencies = [
('app_users', '0017_alter_appuser_subscription'),
]

operations = [
migrations.AddField(
model_name='appusertransaction',
name='plan',
field=models.IntegerField(blank=True, choices=[(1, 'Basic Plan'), (2, 'Premium Plan'), (3, 'Starter'), (4, 'Creator'), (5, 'Business'), (6, 'Enterprise / Agency')], default=None, help_text="User's plan at the time of this transaction.", null=True),
),
migrations.AddField(
model_name='appusertransaction',
name='reason',
field=models.IntegerField(choices=[(1, 'Deduct'), (2, 'Addon'), (3, 'Subscribe'), (4, 'Sub-Create'), (5, 'Sub-Cycle'), (6, 'Sub-Update'), (7, 'Auto-Recharge')], default=0, help_text='The reason for this transaction.<br><br>Deduct: Credits deducted due to a run.<br>Addon: User purchased an add-on.<br>Subscribe: Applies to subscriptions where no distinction was made between create, update and cycle.<br>Sub-Create: A subscription was created.<br>Sub-Cycle: A subscription advanced into a new period.<br>Sub-Update: A subscription was updated.<br>Auto-Recharge: Credits auto-recharged due to low balance.'),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
migrations.AlterField(
model_name='appusertransaction',
name='reason',
field=models.IntegerField(choices=[(1, 'Deduct'), (2, 'Addon'), (3, 'Subscribe'), (4, 'Sub-Create'), (5, 'Sub-Cycle'), (6, 'Sub-Update'), (7, 'Auto-Recharge')], help_text='The reason for this transaction.<br><br>Deduct: Credits deducted due to a run.<br>Addon: User purchased an add-on.<br>Subscribe: Applies to subscriptions where no distinction was made between create, update and cycle.<br>Sub-Create: A subscription was created.<br>Sub-Cycle: A subscription advanced into a new period.<br>Sub-Update: A subscription was updated.<br>Auto-Recharge: Credits auto-recharged due to low balance.'),
),
]
98 changes: 70 additions & 28 deletions app_users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.db.models import Sum
from django.utils import timezone
from firebase_admin import auth
from furl import furl
from phonenumber_field.modelfields import PhoneNumberField

from bots.custom_fields import CustomURLField, StrippedTextField
Expand Down Expand Up @@ -172,6 +173,7 @@ def add_balance(
user: AppUser = AppUser.objects.select_for_update().get(pk=self.pk)
user.balance += amount
user.save(update_fields=["balance"])
kwargs.setdefault("plan", user.subscription and user.subscription.plan)
return AppUserTransaction.objects.create(
user=self,
invoice_id=invoice_id,
Expand Down Expand Up @@ -273,6 +275,18 @@ def get_dollars_spent_this_month(self) -> float:
return (cents_spent or 0) / 100


class TransactionReason(models.IntegerChoices):
DEDUCT = 1, "Deduct"
ADDON = 2, "Addon"

SUBSCRIBE = 3, "Subscribe"
SUBSCRIPTION_CREATE = 4, "Sub-Create"
SUBSCRIPTION_CYCLE = 5, "Sub-Cycle"
SUBSCRIPTION_UPDATE = 6, "Sub-Update"

AUTO_RECHARGE = 7, "Auto-Recharge"


class AppUserTransaction(models.Model):
user = models.ForeignKey(
"AppUser", on_delete=models.CASCADE, related_name="transactions"
Expand Down Expand Up @@ -307,6 +321,25 @@ class AppUserTransaction(models.Model):
default=0,
)

reason = models.IntegerField(
choices=TransactionReason.choices,
help_text="The reason for this transaction.<br><br>"
f"{TransactionReason.DEDUCT.label}: Credits deducted due to a run.<br>"
f"{TransactionReason.ADDON.label}: User purchased an add-on.<br>"
f"{TransactionReason.SUBSCRIBE.label}: Applies to subscriptions where no distinction was made between create, update and cycle.<br>"
f"{TransactionReason.SUBSCRIPTION_CREATE.label}: A subscription was created.<br>"
f"{TransactionReason.SUBSCRIPTION_CYCLE.label}: A subscription advanced into a new period.<br>"
f"{TransactionReason.SUBSCRIPTION_UPDATE.label}: A subscription was updated.<br>"
f"{TransactionReason.AUTO_RECHARGE.label}: Credits auto-recharged due to low balance.",
)
plan = models.IntegerField(
choices=PricingPlan.db_choices(),
help_text="User's plan at the time of this transaction.",
null=True,
blank=True,
default=None,
)

created_at = models.DateTimeField(editable=False, blank=True, default=timezone.now)

class Meta:
Expand All @@ -320,32 +353,41 @@ class Meta:
def __str__(self):
return f"{self.invoice_id} ({self.amount})"

def get_subscription_plan(self) -> PricingPlan | None:
"""
It just so happened that all monthly subscriptions we offered had
different amounts from the one-time purchases.
This uses that heuristic to determine whether a transaction
was a subscription payment or a one-time purchase.
TODO: Implement this more robustly
"""
if self.amount <= 0:
# credits deducted
return None

for plan in PricingPlan:
if (
self.amount == plan.credits
and self.charged_amount == plan.monthly_charge * 100
def save(self, *args, **kwargs):
if self.reason is None:
if self.amount <= 0:
self.reason = TransactionReason.DEDUCT
else:
self.reason = TransactionReason.ADDON
super().save(*args, **kwargs)

def reason_note(self) -> str:
match self.reason:
case (
TransactionReason.SUBSCRIPTION_CREATE
| TransactionReason.SUBSCRIPTION_CYCLE
| TransactionReason.SUBSCRIPTION_UPDATE
| TransactionReason.SUBSCRIBE
):
return plan

return None

def note(self) -> str:
if self.amount <= 0:
return ""
elif plan := self.get_subscription_plan():
return f"Subscription payment: {plan.title} (+{self.amount:,} credits)"
else:
return f"Addon purchase (+{self.amount:,} credits)"
ret = "Subscription payment"
if self.plan:
ret += f": {PricingPlan.from_db_value(self.plan).title}"
return ret
case TransactionReason.AUTO_RECHARGE:
return "Auto recharge"
case TransactionReason.ADDON:
return "Addon purchase"
case TransactionReason.DEDUCT:
return "Run deduction"

def payment_provider_url(self) -> str | None:
match self.payment_provider:
case PaymentProvider.STRIPE:
return str(
furl("https://dashboard.stripe.com/invoices/") / self.invoice_id
)
case PaymentProvider.PAYPAL:
return str(
furl("https://www.paypal.com/unifiedtransactions/details/payment/")
/ self.invoice_id
)
2 changes: 1 addition & 1 deletion bots/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
Workflow,
)
from bots.tasks import create_personal_channels_for_all_members
from celeryapp.tasks import gui_runner
from celeryapp.tasks import runner_task
from daras_ai_v2.fastapi_tricks import get_route_url
from gooeysite.custom_actions import export_to_excel, export_to_csv
from gooeysite.custom_filters import (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.7 on 2024-07-12 19:30

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('bots', '0076_alter_workflowmetadata_default_image_and_more'),
]

operations = [
migrations.AddField(
model_name='savedrun',
name='error_code',
field=models.IntegerField(blank=True, default=None, help_text='The HTTP status code of the error. If this is not set, 500 is assumed.', null=True),
),
migrations.AddField(
model_name='savedrun',
name='error_type',
field=models.TextField(blank=True, default='', help_text='The exception type'),
),
migrations.AlterField(
model_name='savedrun',
name='error_msg',
field=models.TextField(blank=True, default='', help_text='The error message. If this is not set, the run is deemed successful.'),
),
]
27 changes: 22 additions & 5 deletions bots/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,24 @@ class SavedRun(models.Model):

state = models.JSONField(default=dict, blank=True, encoder=PostgresJSONEncoder)

error_msg = models.TextField(default="", blank=True)
error_msg = models.TextField(
default="",
blank=True,
help_text="The error message. If this is not set, the run is deemed successful.",
)
run_time = models.DurationField(default=datetime.timedelta, blank=True)
run_status = models.TextField(default="", blank=True)

error_code = models.IntegerField(
null=True,
default=None,
blank=True,
help_text="The HTTP status code of the error. If this is not set, 500 is assumed.",
)
error_type = models.TextField(
default="", blank=True, help_text="The exception type"
)

hidden = models.BooleanField(default=False)
is_flagged = models.BooleanField(default=False)

Expand Down Expand Up @@ -282,9 +296,12 @@ def __str__(self):
def parent_published_run(self) -> typing.Optional["PublishedRun"]:
return self.parent_version and self.parent_version.published_run

def get_app_url(self):
def get_app_url(self, query_params: dict = None):
return Workflow(self.workflow).page_cls.app_url(
example_id=self.example_id, run_id=self.run_id, uid=self.uid
example_id=self.example_id,
run_id=self.run_id,
uid=self.uid,
query_params=query_params,
)

def to_dict(self) -> dict:
Expand Down Expand Up @@ -1624,9 +1641,9 @@ def duplicate(
visibility=visibility,
)

def get_app_url(self):
def get_app_url(self, query_params: dict = None):
return Workflow(self.workflow).page_cls.app_url(
example_id=self.published_run_id
example_id=self.published_run_id, query_params=query_params
)

def add_version(
Expand Down
Loading

0 comments on commit 0146f0c

Please sign in to comment.