Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

replace AbstractRecurringUserPlan.has_automatic_renewal with AbstractRecurringUserPlan.renewal_triggered_by #188

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
17 changes: 9 additions & 8 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 All @@ -313,7 +314,7 @@ def recurring__payment_provider(self, obj):
def recurring__pricing(self, obj):
return obj.recurring.pricing

recurring__automatic_renewal.admin_order_field = "recurring__pricing"
recurring__pricing.admin_order_field = "recurring__pricing"


admin.site.register(Quota, QuotaAdmin)
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,88 @@
# 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(
"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(
"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
Loading