- Notifications
You must be signed in to change notification settings - Fork9
A django package for managing subscription states
License
kogan/django-subscriptions
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A django package for managing the status and terms of a subscription.
- Django: 2.2 (LTS versions only)
- Python: 3.6+
Other Django or Python versionsmay work, but that is totally cooincidentaland no effort is made to maintain compatibility with versions other than thoselisted above.
$ pip install django-subscriptions
Then add the following packages toINSTALLED_APPS
in your settings:
INSTALLED_APPS = [ ... "django_fsm_log", "subscriptions.apps.SubscriptionsConfig", ...]
And of course, you'll need to run the migrations:
$ python manage.py migrate
You'll also need to setup the triggers, which can be scheduled with celery orrun from a management task. See theTriggers section below.
Manages subscriptions in a single table. Pushes events (signals) so thatconsumers can do the actual work required for that subscription, like billing.
Subscriptions are built around a Finite State Machine model, where states andallowed transitions between states are well defined on the Model. To update fromone state to another, the user calls methods on the Subscription instance. Thisway, all side-effects and actions are contained within the state methods.
Subscription State must not be modified directly.
When a state change is triggered, the subscription will publish relevant signalsso that interested parties can, themselves, react to the state changes.
There are 3 major API components. State change methods, signals/events, and thetriggers used to begin the state changes.
Method | Source States | Target State | Signal Emitted |
---|---|---|---|
cancel_autorenew() | ACTIVE | EXPIRING | autorenew_canceled |
enable_autorenew() | EXPIRING | ACTIVE | autorenew_enabled |
renew() | ACTIVE,SUSPENDED | RENEWING | subscription_due |
renewed(new_end, new_ref, description=None) | ACTIVE,RENEWING,ERROR | ACTIVE | subscription_renewed |
renewal_failed(description=None) | RENEWING,ERROR | SUSPENDED | renewal_failed |
end_subscription(description=None) | ACTIVE,SUSPENDED,EXPIRING,ERROR | ENDED | subscription_ended |
state_unknown(description=None) | RENEWING | ERROR | subscription_error |
Example:
subscription.renew()
may only be called ifsubscription.state
is eitherACTIVE
orSUSPENDED
,and will causesubscription.state
to move into theRENEWING
state.
Thedescription
argument is a string that can be used to persist the reason for a statechange in theStateLog
table (and admin inlines).
There are a bunch of triggers that are used to update subscriptions as they becomedue or expire. Nothing is configured to run these triggers by default. You caneither call them as part of your own process, or usecelery beat
to executethe triggers using the tasks provided insubscriptions.tasks
.
Create a new subscription:
Subscription.objects.add_subscription(start_date, end_date, reference) -> Subscription
Trigger subscriptions that are due for renewal:
Subscription.objects.trigger_renewals() -> int # number of renewals sent
Trigger subscriptions that are due to expire:
Subscription.objects.trigger_expiring() -> int # number of expirations
Trigger subscriptions that are suspended:
Subscription.objects.trigger_suspended() -> int # number of renewals
Trigger subscriptions that have been suspended for longer thantimeout_hours
toend (usessubscription.end
date, notsubscription.last_updated
):
Subscription.objects.trigger_suspended_timeout(timeout_hours=48) -> int # number of suspensions
Trigger subscriptions that have been stuck in renewing state for longer thantimeout_hours
to be marked as an error (usessubscription.last_updated
to determine the timeout):
Subscription.objects.trigger_stuck(timeout_hours=2) -> int # number of error subscriptions
Ifsettings.SUBSCRIPTIONS_STUCK_RETRY
isTrue
, then subscriptions are moved back intotheSUSPENDED
state, ready to be retried. This can be useful when you have an offlineprocess that can resolve stuck subscription issues, and there is no issue retrying thesubscription.
The following tasks are defined but are not scheduled:
subscriptions.tasks.trigger_renewalssubscriptions.tasks.trigger_expiringsubscriptions.tasks.trigger_suspendedsubscriptions.tasks.trigger_suspended_timeoutsubscriptions.tasks.trigger_stuck
If you'd like to schedule the tasks, do so with a celery beat configuration like this:
# settings.pyCELERYBEAT_SCHEDULE = { "subscriptions_renewals": { "task": "subscriptions.tasks.trigger_renewals", "schedule": crontab(hour=0, minute=10), }, "subscriptions_expiring": { "task": "subscriptions.tasks.trigger_expiring", "schedule": crontab(hour=0, minute=15), }, "subscriptions_suspended": { "task": "subscriptions.tasks.trigger_suspended", "schedule": crontab(hour="3,6,9", minute=30), }, "subscriptions_suspended_timeout": { "task": "subscriptions.tasks.trigger_suspended_timeout", "schedule": crontab(hour=0, minute=40), "kwargs": {"hours": 48}, }, "subscriptions_stuck": { "task": "subscriptions.tasks.trigger_stuck", "schedule": crontab(hour="*/2", minute=50), "kwargs": {"hours": 2}, },}
We usepre-commit <https://pre-commit.com/>
to enforce our code style ruleslocally before you commit them into git. Once you install the pre-commit library(locally via pip is fine), just install the hooks::
pre-commit install -f --install-hooks
The same checks are executed on the build server, so skipping the local linting(withgit commit --no-verify
) will only result in a failed test build.
Current style checking tools:
- flake8: python linting
- isort: python import sorting
- black: python code formatting
Note:
You must have python3.6 available on your path, as it is required for someof the hooks.
After installing all dependencies, you can generate required migration fileslike so:
$ poetry run ipython migrate.py<nameofmigration>
- Bump the version number in pyproject.toml and src/subscriptions/init.py
- Commit and push to master
- From github,create a new release
- Name the release "v<maj.minor.patch>" using the version number from step 1.
- Publish the release
- If the release successfully builds, circleci will publish the new package to pypi
About
A django package for managing subscription states