Skip to content

Commit

Permalink
replace AbstractRecurringUserPlan.has_automatic_renewal with Abstract…
Browse files Browse the repository at this point in the history
…RecurringUserPlan.renewal_triggered_by
  • Loading branch information
radekholy24 committed Apr 10, 2024
1 parent 8581f34 commit c0dbb98
Show file tree
Hide file tree
Showing 10 changed files with 540 additions and 64 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
django-plans changelog
======================

1.1.0 (unreleased)
------------------
* Add `AbstractRecurringUserPlan.renewal_triggered_by`
* Use `AbstractRecurringUserPlan.renewal_triggered_by` instead of `has_automatic_renewal`; `has_automatic_renewal` is not used anywhere anymore; `has_automatic_renewal=True` is automatically migrated to `renewal_triggered_by=TASK` and `has_automatic_renewal=False` to `renewal_triggered_by=USER`
* Rename `AbstractRecurringUserPlan.has_automatic_renewal` to `_has_automatic_renewal_backup_deprecated` so it can be used make your own data migration from the former `has_automatic_renewal` to `renewal_triggered_by` if the default one does not work for you
* Add `AbstractRecurringUserPlan.has_automatic_renewal` property that issues a deprecation warning and uses `renewal_triggered_by` under the hood
* Add `renewal_triggered_by` parameter to `AbstractUserPlan.set_plan_renewal`
* Deprecate `AbstractRecurringUserPlan.has_automatic_renewal`; use `AbstractRecurringUserPlan.renewal_triggered_by` instead
* Deprecate `AbstractRecurringUserPlan._has_automatic_renewal_backup_deprecated`; use `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.7
------------------
* Add `AbstractOrder.return_order()`
Expand Down
13 changes: 12 additions & 1 deletion demo/example/sample_plans/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,19 @@ class Migration(migrations.Migration):
),
("currency", models.CharField(max_length=3, verbose_name="currency")),
(
"has_automatic_renewal",
"renewal_triggered_by",
models.IntegerField(
choices=[(1, "other"), (2, "user"), (3, "task")],
db_index=True,
default=2,
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).",
verbose_name="renewal triggered by",
),
),
(
"_has_automatic_renewal_backup_deprecated",
models.BooleanField(
db_column="has_automatic_renewal",
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",
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
15 changes: 8 additions & 7 deletions plans/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class UserPlanAdmin(UserLinkMixin, admin.ModelAdmin):
"plan__name",
"plan__available",
"plan__visible",
"recurring__has_automatic_renewal",
"recurring__renewal_triggered_by",
"recurring__payment_provider",
"recurring__token_verified",
"recurring__pricing",
Expand All @@ -272,7 +272,7 @@ class UserPlanAdmin(UserLinkMixin, admin.ModelAdmin):
"plan",
"expire",
"active",
"recurring__automatic_renewal",
"recurring__renewal_triggered_by",
"recurring__token_verified",
"recurring__payment_provider",
"recurring__pricing",
Expand All @@ -290,12 +290,13 @@ class UserPlanAdmin(UserLinkMixin, admin.ModelAdmin):
"plan",
]

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

recurring__automatic_renewal.admin_order_field = "recurring__has_automatic_renewal"
recurring__automatic_renewal.boolean = True
recurring__automatic_renewal.short_description = "Automatic renewal"
recurring__renewal_triggered_by.admin_order_field = (
"recurring__renewal_triggered_by"
)
recurring__renewal_triggered_by.short_description = "Renewal triggered by"

def recurring__token_verified(self, obj):
return obj.recurring.token_verified
Expand Down
90 changes: 85 additions & 5 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 @@ -299,7 +300,8 @@ def get_plan_extended_from(self, plan):
def has_automatic_renewal(self):
return (
hasattr(self, "recurring")
and self.recurring.has_automatic_renewal
and self.recurring.renewal_triggered_by
!= self.recurring.RENEWAL_TRIGGERED_BY.USER
and self.recurring.token_verified
)

Expand Down Expand Up @@ -332,13 +334,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,
)
renewal_triggered_by = (
self.recurring.RENEWAL_TRIGGERED_BY.TASK
if has_automatic_renewal
else 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,7 +378,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.has_automatic_renewal = has_automatic_renewal
self.recurring.renewal_triggered_by = renewal_triggered_by
for k, v in kwargs.items():
setattr(self.recurring, k, v)
self.recurring.save()
Expand Down Expand Up @@ -509,6 +538,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,12 +587,26 @@ 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)
has_automatic_renewal = models.BooleanField(
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)."
),
default=RENEWAL_TRIGGERED_BY.USER,
db_index=True,
)
# A backup of the old has_automatic_renewal field to support data migration to the new renewal_triggered_by field.
# Do not make any other modifications to the field in order to let user's auto-migrations detect the renaming.
# TODO: _has_automatic_renewal_backup_deprecated deprecated. Remove in the next major release.
_has_automatic_renewal_backup_deprecated = models.BooleanField(
_("has automatic plan renewal"),
help_text=_(
"Automatic renewal is enabled for associated plan. "
"If False, the plan renewal can be still initiated by user.",
),
db_column="has_automatic_renewal",
default=False,
)
token_verified = models.BooleanField(
Expand All @@ -572,6 +623,35 @@ class AbstractRecurringUserPlan(BaseMixin, models.Model):
class Meta:
abstract = True

# TODO: has_automatic_renewal deprecated. Remove in the next major release.
@property
def has_automatic_renewal(self):
warnings.warn(
"has_automatic_renewal is deprecated. Use renewal_triggered_by instead.",
DeprecationWarning,
)
return self.renewal_triggered_by != self.RENEWAL_TRIGGERED_BY.USER

# TODO: has_automatic_renewal deprecated. Remove in the next major release.
@has_automatic_renewal.setter
def has_automatic_renewal(self, value):
warnings.warn(
"has_automatic_renewal is deprecated. Use renewal_triggered_by instead.",
DeprecationWarning,
)
self.renewal_triggered_by = (
self.RENEWAL_TRIGGERED_BY.TASK if value else self.RENEWAL_TRIGGERED_BY.USER
)

# TODO: has_automatic_renewal deprecated. Remove in the next major release.
@has_automatic_renewal.deleter
def has_automatic_renewal(self):
warnings.warn(
"has_automatic_renewal is deprecated. Use renewal_triggered_by instead.",
DeprecationWarning,
)
del self.renewal_triggered_by

def create_renew_order(self):
"""
Create order for plan renewal
Expand Down Expand Up @@ -605,7 +685,7 @@ def set_all_fields_default(self):
self.amount = None
self.tax = None
self.currency = None
self.has_automatic_renewal = False
self.renewal_triggered_by = self.RENEWAL_TRIGGERED_BY.USER
self.token_verified = False
self.card_expire_year = None
self.card_expire_month = None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 4.2.11 on 2024-04-09 10:20

from django.db import migrations, models


class Migration(migrations.Migration):

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

operations = [
migrations.AlterField(
model_name="recurringuserplan",
name="has_automatic_renewal",
field=models.BooleanField(
db_column="has_automatic_renewal",
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",
),
),
migrations.RenameField(
model_name="recurringuserplan",
old_name="has_automatic_renewal",
new_name="_has_automatic_renewal_backup_deprecated",
),
migrations.AddField(
model_name="recurringuserplan",
name="renewal_triggered_by",
field=models.IntegerField(
choices=[(1, "other"), (2, "user"), (3, "task")],
db_index=True,
default=2,
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).",
verbose_name="renewal triggered by",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Generated by Django 4.2.11 on 2024-04-10 12:50

from enum import IntEnum

from django.db import migrations


def _recurringuserplan_has_automatic_renewal_backup_deprecated_to_renewal_triggered_by(
apps, schema_editor
):
RecurringUserPlan = apps.get_model("plans", "RecurringUserPlan")
recurringuserplans_changed = (
RecurringUserPlan.objects.select_for_update()
.exclude(
_has_automatic_renewal_backup_deprecated=True,
renewal_triggered_by=_RenewalTriggeredByEnum.task,
)
.exclude(
_has_automatic_renewal_backup_deprecated=False,
renewal_triggered_by=_RenewalTriggeredByEnum.user,
)
)
for recurringuserplan_changed in recurringuserplans_changed:
print(
f"RecurringUserPlan's renewal_triggered_by will be overwritten: {recurringuserplan_changed.pk}"
)
RecurringUserPlan.objects.filter(
_has_automatic_renewal_backup_deprecated=True
).update(renewal_triggered_by=_RenewalTriggeredByEnum.task)
RecurringUserPlan.objects.filter(
_has_automatic_renewal_backup_deprecated=False
).update(renewal_triggered_by=_RenewalTriggeredByEnum.user)


def _recurringuserplan_renewal_triggered_by_to_has_automatic_renewal_backup_deprecated(
apps, schema_editor
):
RecurringUserPlan = apps.get_model("plans", "RecurringUserPlan")
recurringuserplans_changed = (
RecurringUserPlan.objects.select_for_update()
.exclude(
renewal_triggered_by__in={
_RenewalTriggeredByEnum.task,
_RenewalTriggeredByEnum.other,
},
_has_automatic_renewal_backup_deprecated=True,
)
.exclude(
renewal_triggered_by=_RenewalTriggeredByEnum.user,
_has_automatic_renewal_backup_deprecated=False,
)
)
for recurringuserplan_changed in recurringuserplans_changed:
print(
f"RecurringUserPlan's _has_automatic_renewal_backup_deprecated will be overwritten: {recurringuserplan_changed.pk}"
)
RecurringUserPlan.objects.filter(
renewal_triggered_by__in={
_RenewalTriggeredByEnum.task,
_RenewalTriggeredByEnum.other,
}
).update(_has_automatic_renewal_backup_deprecated=True)
RecurringUserPlan.objects.filter(
renewal_triggered_by=_RenewalTriggeredByEnum.user
).update(_has_automatic_renewal_backup_deprecated=False)
RecurringUserPlan.objects.update(renewal_triggered_by=_RenewalTriggeredByEnum.user)


class _RenewalTriggeredByEnum(IntEnum):
other = 1
user = 2
task = 3


class Migration(migrations.Migration):

dependencies = [
("plans", "0013_alter_recurringuserplan_has_automatic_renewal_and_more"),
]

operations = [
migrations.RunPython(
_recurringuserplan_has_automatic_renewal_backup_deprecated_to_renewal_triggered_by,
reverse_code=_recurringuserplan_renewal_triggered_by_to_has_automatic_renewal_backup_deprecated,
)
]
Loading

0 comments on commit c0dbb98

Please sign in to comment.