Skip to content

Commit

Permalink
add AbstractRecurringUserPlan.renewal_triggered_by
Browse files Browse the repository at this point in the history
  • Loading branch information
radekholy24 committed Apr 5, 2024
1 parent 50ae042 commit 0cb8a54
Show file tree
Hide file tree
Showing 9 changed files with 455 additions and 65 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ django-plans changelog
1.1.0 (unreleased)
------------------
* Add `AbstractOrder.return_order()`
* Add `AbstractRecurringUserPlan.renewal_triggered_by`
* Add `renewal_triggered_by` parameter to `AbstractUserPlan.set_plan_renewal`
* Deprecate `AbstractRecurringUserPlan.has_automatic_renewal`; use `AbstractRecurringUserPlan.renewal_triggered_by` instead
* Deprecate `AbstractRecurringUserPlan.renewal_triggered_by=None`; set an `AbstractRecurringUserPlan.RENEWAL_TRIGGERED_BY` instead
* Deprecate `has_automatic_renewal` parameter of `AbstractUserPlan.set_plan_renewal`; use `renewal_triggered_by` instead
* Deprecate `None` value of `renewal_triggered_by` parameter of `AbstractUserPlan.set_plan_renewal`; use an `AbstractRecurringUserPlan.RENEWAL_TRIGGERED_BY` instead

1.0.6
-----
Expand Down
16 changes: 14 additions & 2 deletions demo/example/sample_plans/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,24 @@ class Migration(migrations.Migration):
),
),
("currency", models.CharField(max_length=3, verbose_name="currency")),
(
"renewal_triggered_by",
models.IntegerField(
blank=True,
choices=[(1, "other"), (2, "user"), (3, "task")],
db_index=True,
default=None,
help_text="The source of the associated plan's renewal (USER = user-initiated renewal, TASK = autorenew_account-task-initiated renewal, OTHER = renewal is triggered using another mechanism, None = use has_automatic_renewal value (deprecated)). Overrides has_automatic_renewal, if not None.",
null=True,
verbose_name="renewal triggered by",
),
),
(
"has_automatic_renewal",
models.BooleanField(
default=False,
help_text="Automatic renewal is enabled for associated plan. If False, the plan renewal can be still initiated by user.",
verbose_name="has automatic plan renewal",
help_text="Deprecated. Automatic renewal is enabled for associated plan. If False, the plan renewal can be still initiated by user. Overriden by renewal_triggered_by, if not None.",
verbose_name="has automatic plan renewal (deprecated)",
),
),
(
Expand Down
8 changes: 4 additions & 4 deletions docs/source/plans_recurrence.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ Plans recurrence and automatic renewal

To support renewal of plans, use ``RecurringUserPlan`` model to store information about the recurrence.

The plans can be renewed automatically, or the ``RecurringUserPlan`` information can be used only to store information for one-click user initiated renewal (with ``automatic_renewal=False``).
The plans can be renewed automatically using this app, the ``RecurringUserPlan`` information can be used only to store information for one-click user initiated renewal (with ``renewal_triggered_by=USER``), or the ``RecurringUserPlan`` can indicate that another mechanism is used to automatically renew the plans (``renewal_triggered_by=OTHER``).

For plans, that should be renewed automatically fill in information about the recurrence::
For plans, that should be renewed automatically using this app fill in information about the recurrence::

self.order.user.userplan.set_plan_renewal(
order=self.order,
automatic_renewal=True,
renewal_triggered_by=TASK,
...
# Not required
payment_provider='FooProvider',
Expand All @@ -19,7 +19,7 @@ For plans, that should be renewed automatically fill in information about the re
...
)

Then all active ``UserPlan`` with ``RecurringUserPlan.has_automatic_renewal=True`` will be picked by ``autorenew_account`` task, that will send ``account_automatic_renewal`` signal.
Then all active ``UserPlan`` with ``RecurringUserPlan.renewal_triggered_by=TASK`` will be picked by ``autorenew_account`` task, that will send ``account_automatic_renewal`` signal.
This signal can be used for your implementation of automatic plan renewal. You should implement following steps::

@receiver(account_automatic_renewal)
Expand Down
12 changes: 11 additions & 1 deletion plans/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class UserPlanAdmin(UserLinkMixin, admin.ModelAdmin):
"plan__name",
"plan__available",
"plan__visible",
"recurring__renewal_triggered_by",
"recurring__has_automatic_renewal",
"recurring__payment_provider",
"recurring__token_verified",
Expand All @@ -272,6 +273,7 @@ class UserPlanAdmin(UserLinkMixin, admin.ModelAdmin):
"plan",
"expire",
"active",
"recurring__renewal_triggered_by",
"recurring__automatic_renewal",
"recurring__token_verified",
"recurring__payment_provider",
Expand All @@ -290,12 +292,20 @@ class UserPlanAdmin(UserLinkMixin, admin.ModelAdmin):
"plan",
]

def recurring__renewal_triggered_by(self, obj):
return obj.recurring.renewal_triggered_by

recurring__renewal_triggered_by.admin_order_field = (
"recurring__renewal_triggered_by"
)
recurring__renewal_triggered_by.short_description = "Renewal triggered by"

def recurring__automatic_renewal(self, obj):
return obj.recurring.has_automatic_renewal

recurring__automatic_renewal.admin_order_field = "recurring__has_automatic_renewal"
recurring__automatic_renewal.boolean = True
recurring__automatic_renewal.short_description = "Automatic renewal"
recurring__automatic_renewal.short_description = "Automatic renewal (deprecated)"

def recurring__token_verified(self, obj):
return obj.recurring.token_verified
Expand Down
84 changes: 78 additions & 6 deletions plans/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import re
import warnings
from datetime import date, timedelta
from decimal import Decimal

Expand Down Expand Up @@ -297,9 +298,25 @@ def get_plan_extended_from(self, plan):
return date.today()

def has_automatic_renewal(self):
if not hasattr(self, "recurring"):
return False

recurring_renewal_triggered_by = self.recurring.renewal_triggered_by
# TODO: recurring.renewal_triggered_by=None deprecated. Remove in the next major release.
if recurring_renewal_triggered_by is None:
warnings.warn(
"recurring.renewal_triggered_by=None is deprecated. "
"Set an AbstractRecurringUserPlan.RENEWAL_TRIGGERED_BY instead.",
DeprecationWarning,
)
recurring_renewal_triggered_by = (
self.recurring.RENEWAL_TRIGGERED_BY.TASK
if self.recurring.has_automatic_renewal
else self.recurring.RENEWAL_TRIGGERED_BY.USER
)

return (
hasattr(self, "recurring")
and self.recurring.has_automatic_renewal
recurring_renewal_triggered_by != self.recurring.RENEWAL_TRIGGERED_BY.USER
and self.recurring.token_verified
)

Expand Down Expand Up @@ -332,13 +349,40 @@ def plan_autorenew_at(self):
days=plans_autorenew_before_days, hours=plans_autorenew_before_hours
)

def set_plan_renewal(self, order, has_automatic_renewal=True, **kwargs):
def set_plan_renewal(
self,
order,
# TODO: has_automatic_renewal deprecated. Remove in the next major release.
has_automatic_renewal=None,
# TODO: renewal_triggered_by=None deprecated. Set to TASK in the next major release.
renewal_triggered_by=None,
**kwargs,
):
"""
Creates or updates plan renewal information for this userplan with given order
"""
if not hasattr(self, "recurring"):
self.recurring = AbstractRecurringUserPlan.get_concrete_model()()

if has_automatic_renewal is None and renewal_triggered_by is None:
has_automatic_renewal = True
if has_automatic_renewal is not None:
warnings.warn(
"has_automatic_renewal is deprecated. Use renewal_triggered_by instead.",
DeprecationWarning,
)

if renewal_triggered_by is None:
warnings.warn(
"renewal_triggered_by=None is deprecated. "
"Set an AbstractRecurringUserPlan.RENEWAL_TRIGGERED_BY instead.",
DeprecationWarning,
)
else:
has_automatic_renewal = (
renewal_triggered_by != self.recurring.RENEWAL_TRIGGERED_BY.USER
)

# Erase values of all fields
# We don't want to mix the old and new values
self.recurring.set_all_fields_default()
Expand All @@ -349,6 +393,7 @@ def set_plan_renewal(self, order, has_automatic_renewal=True, **kwargs):
self.recurring.amount = order.amount
self.recurring.tax = order.tax
self.recurring.currency = order.currency
self.recurring.renewal_triggered_by = renewal_triggered_by
self.recurring.has_automatic_renewal = has_automatic_renewal
for k, v in kwargs.items():
setattr(self.recurring, k, v)
Expand Down Expand Up @@ -509,6 +554,14 @@ class AbstractRecurringUserPlan(BaseMixin, models.Model):
More about recurring payments in docs.
"""

RENEWAL_TRIGGERED_BY = Enumeration(
[
(1, "OTHER", pgettext_lazy("Renewal triggered by", "other")),
(2, "USER", pgettext_lazy("Renewal triggered by", "user")),
(3, "TASK", pgettext_lazy("Renewal triggered by", "task")),
]
)

user_plan = models.OneToOneField(
"UserPlan", on_delete=models.CASCADE, related_name="recurring"
)
Expand Down Expand Up @@ -550,11 +603,28 @@ class AbstractRecurringUserPlan(BaseMixin, models.Model):
_("tax"), max_digits=4, decimal_places=2, db_index=True, null=True, blank=True
) # Tax=None is when tax is not applicable
currency = models.CharField(_("currency"), max_length=3)
renewal_triggered_by = models.IntegerField(
_("renewal triggered by"),
choices=RENEWAL_TRIGGERED_BY,
help_text=_(
"The source of the associated plan's renewal (USER = user-initiated renewal, "
"TASK = autorenew_account-task-initiated renewal, OTHER = renewal is triggered using another mechanism, "
"None = use has_automatic_renewal value (deprecated)). Overrides has_automatic_renewal, if not None."
),
# TODO: Nullable deprecated. Set to False in the next major release.
null=True,
# TODO: Blank deprecated. Set to False in the next major release.
blank=True,
default=None,
db_index=True,
)
# TODO: has_automatic_renewal deprecated. Remove in the next major release.
has_automatic_renewal = models.BooleanField(
_("has automatic plan renewal"),
_("has automatic plan renewal (deprecated)"),
help_text=_(
"Automatic renewal is enabled for associated plan. "
"If False, the plan renewal can be still initiated by user.",
"Deprecated. Automatic renewal is enabled for associated plan. "
"If False, the plan renewal can be still initiated by user. "
"Overriden by renewal_triggered_by, if not None.",
),
default=False,
)
Expand Down Expand Up @@ -605,6 +675,8 @@ def set_all_fields_default(self):
self.amount = None
self.tax = None
self.currency = None
if self.renewal_triggered_by is not None:
self.renewal_triggered_by = self.RENEWAL_TRIGGERED_BY.USER
self.has_automatic_renewal = False
self.token_verified = False
self.card_expire_year = None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.11 on 2024-04-05 11:50

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("plans", "0012_planpricing_visible"),
]

operations = [
migrations.AddField(
model_name="recurringuserplan",
name="renewal_triggered_by",
field=models.IntegerField(
blank=True,
choices=[(1, "other"), (2, "user"), (3, "task")],
db_index=True,
default=None,
help_text="The source of the associated plan's renewal (USER = user-initiated renewal, TASK = autorenew_account-task-initiated renewal, OTHER = renewal is triggered using another mechanism, None = use has_automatic_renewal value (deprecated)). Overrides has_automatic_renewal, if not None.",
null=True,
verbose_name="renewal triggered by",
),
),
migrations.AlterField(
model_name="recurringuserplan",
name="has_automatic_renewal",
field=models.BooleanField(
default=False,
help_text="Deprecated. Automatic renewal is enabled for associated plan. If False, the plan renewal can be still initiated by user. Overriden by renewal_triggered_by, if not None.",
verbose_name="has automatic plan renewal (deprecated)",
),
),
]
27 changes: 20 additions & 7 deletions plans/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q

from .base.models import AbstractRecurringUserPlan
from .signals import account_automatic_renewal

User = get_user_model()
Expand All @@ -23,13 +25,24 @@ def autorenew_account(providers=None):
PLANS_AUTORENEW_BEFORE_DAYS = getattr(settings, "PLANS_AUTORENEW_BEFORE_DAYS", 0)
PLANS_AUTORENEW_BEFORE_HOURS = getattr(settings, "PLANS_AUTORENEW_BEFORE_HOURS", 0)

accounts_for_renewal = get_active_plans().filter(
userplan__recurring__has_automatic_renewal=True,
userplan__recurring__token_verified=True,
userplan__expire__lt=datetime.date.today()
+ datetime.timedelta(
days=PLANS_AUTORENEW_BEFORE_DAYS, hours=PLANS_AUTORENEW_BEFORE_HOURS
),
accounts_for_renewal = (
get_active_plans()
.filter(
Q(
userplan__recurring__renewal_triggered_by=AbstractRecurringUserPlan.RENEWAL_TRIGGERED_BY.TASK
)
| Q(
userplan__recurring__renewal_triggered_by=None,
userplan__recurring__has_automatic_renewal=True,
)
)
.filter(
userplan__recurring__token_verified=True,
userplan__expire__lt=datetime.date.today()
+ datetime.timedelta(
days=PLANS_AUTORENEW_BEFORE_DAYS, hours=PLANS_AUTORENEW_BEFORE_HOURS
),
)
)

if providers:
Expand Down
Loading

0 comments on commit 0cb8a54

Please sign in to comment.