-
Notifications
You must be signed in to change notification settings - Fork 67
/
Copy pathREADME
375 lines (340 loc) · 16 KB
/
README
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
Django Subscription
===================
Django-subscription is an application for handling PayPal-based pay
subscriptions. Module does not handle explicit permissions; instead,
subscribed user are automatically added to predefined groups using
`django.contrib.auth' application. It needs django-paypal application
available at [http://github.com/johnboxall/django-paypal/] for handling
payments.
Table of Contents
=================
1 Installation
2 Settings
3 Models
3.1 Subscription
3.1.1 methods
3.2 UserSubscription
3.2.1 methods
3.3 Transaction
4 Signals
5 Views
6 URLs
7 Templates
8 Subscription change
9 Example code
10 Bugs and omissions
10.1 Plans
11 License
1 Installation
~~~~~~~~~~~~~~
Copy or symlink `subscription/' directory on Python path (`setup.py'
script for automated installation will be supplied later on). Module
contents are available in the `subscription' module.
In order to use application, add `subscription' to INSTALLED_APPS in
Django project `settings.py' file.
2 Settings
~~~~~~~~~~
In project's `settings.py' file `SUBSCRIPTION_PAYPAL_SETTINGS'
should be set to a dictionary with default PayPal button settings,
as described in django-paypal documentation. At least the `business'
key should be set to your PayPal business e-mail.
`SUBSCRIPTION_PAYPAL_FORM' can be set to a form class pathname as
string to use custom PayPal payment button class. Default is
'paypal.standard.forms.PayPalPaymentsForm'. To use PayPal encrypted
buttons or shared secrets, specify needed django-paypal settings and
set an appropriate class here.
`SUBSCRIPTION_GRACE_PERIOD' is an integer and it specifies number of
days after individual subscription expiry on which account is
actually treated as expired. Default is 2 days. Intent of this
setting is that recurring payments take place e.g. monthly, so
either on last day of subscription period, or even on first day
after it; this way we avoind unintentionally locking out user
account.
3 Models
~~~~~~~~
Two models defined by the application are available in the
`subscription.models' module.
3.1 Subscription
================
Main model used by the application is `Subscription'. It
represents a single subscription available for users. Subscription
has following fields:
- `name' - short name
- `description' - longer description
- `price' - subscription price
- `recurrence_period' - PayPal subscription recurrence period (used
only if `recurrence_unit' is not `None')
- `recurrence_unit' - in what units is recurrence period expressed:
- D for days
- W for weeks
- M for months
- Y for years
- None (NULL) for one-time (non-recurring) payment
- `group' - one to one relation to
`django.contrib.auth.models.Group'. Subscription is identified
by the group.
3.1.1 methods
-------------
- `price_per_day()' - returns estimate subscription price per day,
as a float. This value is used to give user that upgrades
subscription a rebate for unused part of month. Value is only
an estimate: average length of month (30.4368 days) and year
(365.2425 days) are used.
- `get_pricing_display()' - return pretty pricing info for display
as a string.
3.2 UserSubscription
====================
This model instances define a user's subscription. Model has
following fields:
- `user' - one-to-one relation to `auth.User' model, primary key;
- `subscription' - foreign key relation to `Subscription' model,
specifies kind of subscription `user' is subscribed to;
- `expires' - expiry date (if null, subscription never expires)
- `active' - boolean, True if subscription is active
- `cancelled' - boolean, True if subscription was cancelled
Fields `active' and `cancelled' are used for implementing the
subscription change flow (see later). Every UserSubscription
starts with both `active' and `cancelled' set to `False'. When
PayPal subscription is confirmed, `active' is set to True. When
any other PayPal subscription for the same user is confirmed,
`active' is set to `False' (because `active' is set to `True' for
this other subscription, in other UserSubscription instance). When
subscription is cancelled at PayPal, `cancelled' is set to `True'.
When UserSubscription is cancelled and not active, it is deleted.
When UserSubscription has expired and is cancelled, it is deleted.
Transition graph of these state bits can be found in
[file:docs/usersubscription-states.dot.png] (GraphViz source in
[file:docs/usersubscription-states.dot]).
Class field `grace_timedelta' is provided (read-only) and contains
effective value of `SUBSCRIPTION_GRACE_PERIOD' setting as
`datetime.timedelta' object.
3.2.1 methods
-------------
- `user_is_group_member()' - returns true if `user' is member of
`subscription.group';
- `expired()' - returns true if there is more than
`SUBSCRIPTION_GRACE_PERIOD' days after `expires' date;
- `valid()' - returns true if:
+ `expired()' is false and `user_is_group_member()' is false, or
+ `expired()' is true and `user_is_group_member()' is true;
- `unsubscribe()' - remove `subscription.group' from `user''s groups
- `subscribe()' - add `subscription.group' to `user''s groups
(called automatically on PayPal one-time payment and
subscription start);
- `fix()' - if not `valid()', call `unsubscribe()' or `subscribe()';
- `extend(timedelta=None)' - extend `expires' field by provided
`datetime.timedelta', or by `subscription''s recurrence period
(called automatically on PayPal subscription payments);
- `try_change(subscription)' - sends `change_check' signal to test
whether change from `self.subscription' to Subscription object
supplied in `subscription' parameter is possible. Returns list
of reasons why upgrade is denied; if list is empty, upgrade is
allowed.
Convenience function `subscription.models.unsubscribe_expired()'
is also provided. It loops over all expired `UserSubscription'
instances and calls `unsubscribe()' method. It is intended to be
called automatically from cron, django-cron, or on some event.
Alternatively, `fix()' can be called on events related to
user, e.g. on user login.
3.3 Transaction
===============
`Transaction' model is mostly read-only and is used to view
subscription-related events in the admin panel. It has following
fields:
- `timestamp' - date and time of event
- `subscription' - foreign key of `Subscription' model that event
was related to
- `user' - foreign key of `django.contrib.auth.models.User' model
that event was related to
- `ipn' - foreign key of `paypal.standard.ipn.models.PayPalIPN'
model identifying payment callback related to event
- `event' - type of event, one of:
- new usersubscription
- one-time payment
- subscription payment
- unexpected payment
- payment flagged
- deactivated
- activated
- unexpected subscription
- remove subscription
- cancel subscription
- unexpected cancel
- modify subscription
- subscription expired
The "unexpected" events are ones that could not be related to any
specific user/subscription pair.
- `amount' - amount (`mc_gross') of `ipn'
- `comment' - site admin's comment, only field intended to be
modified.
In admin panel's `Transaction' object list, fields `subscription',
`user', `ipn' are links to related modes instance's admin forms.
4 Signals
~~~~~~~~~
On subscription-related events, the application sends signals that
project code can connect to and do some site-specific things (e.g.
send a nice e-mail to user). Signals are available in
`subscription.signals' package. All signals have `Subscription'
instance (or, in extreme cases with `event' signal, `None') as
sender, and have arguments `ipn'
(`paypal.standard.ipn.models.PayPalIPN' model instance), `user'
(`django.contrib.auth.models.User' instance), `subscription'
(`Subscription' instance or None, same as sender),
`usersubscription' (`UserSubscription' instance). Signals are:
- `signed_up' - user signed up for one-time payment,
- `subscribed' - user subscribed
- `unsubscribed' - user unsubscribed from PayPal (`usersubscription'
is a deleted object if `usersubscription.active' is True)
- `paid' - payment received from a subscription
- `event' - other strange event, does not receive `usersubscription'
argument (there is no meaningful `UserSubscription' object) and
receives additional `event' argument, which may be
- `unexpected_payment'
- `flagged'
- `unexpected_subscription'
- `unexpected_cancel'
- `subscription_modify'
Signal `change_check' is a hook for verification of subscription
change. Sender is `UserSubscription' object with user's current
subscription, additional parameter `subscription' provides
subscription to change to. If subscription change is possible,
listener should return `None', otherwise it should return a string
describing reason that will be displayed to user.
5 Views
~~~~~~~
Views are available in `subscription.views' module
- `subscription_list' lists available subscription using
`subscription/subscription_list.html' template
- `subscription_detail' presents details of the selected
subscription (login is required for this view) along with PayPal
button for subscription or upgrade.
6 URLs
~~~~~~
Module `subscription.urls' configures default urls for module. This
are:
- root URL displays `subscription_list' view
- /id/ (numeric ID) displays `subscription_detail' view for
Subscription with ID /id/
- `paypal/' is PayPal IPN URL
- `done/' displays `subscription/subscription_done.html' template
and is where successful PayPal transactions for initial
subscription are redirected
- `change-done/' displays
`subscription/subscription_change_done.html' template and is
where successful PayPal transactions for subscription change are
redirected
- `cancel/' displays `subscription/subscription_cancel.html'
template and is where cancelled PayPal transactions are redirected
7 Templates
~~~~~~~~~~~
Templates `subscription/subscription_done.html' and
`subscription/subscription_cancel.html' receive no context.
Template `subscription/subscription_change_dane.html' receives
`cancel_url' parameter, which is URL to PayPal list of transactions
with site's merchant account, making it easier to cancel the old
subscription.
Template `subscription/subscription_list.html' receives
`object_list' variable which is a list of `Subscription' objects.
Template `subscription/subscription_detail.html' receives:
- `object' variable which is a `Subscription' object,
- `usersubscription' variable, which is current user's active
`UserSubscription' instance (may be used to tell apart initial
subscription from subscription change/upgrade, or to display
current subscription's expiry date),
- `change_denied_reasons', which is a list of reasons that
subscription change/upgrade is denied; if false (empty list or
`None' if user is not subscribed), change or signup is allowed,
- `form' variable which is a PayPal form for the `object', if
`change_denied_reasons' is false,
- `cancel_url', which is URL to PayPal list of transactions with
site's merchant account, making it easier to cancel the old
subscription.
8 Subscription change
~~~~~~~~~~~~~~~~~~~~~
Most complex flow in this app is when user wants to change (upgrade)
current subscription. For subscriptions we are using PayPal
standard subscriptions API. This means, we get three kinds of
asynchronous IPN notifications:
- subscr_signup when user signs up for new subscription,
- subscr_payment on every single payment,
- subscr_cancel when user or merchant cancels subscription (or
subscr_eot when time-limited subscription runs out; we treat
subscr_eot exactly as subscr_cancel).
When user signs up, we get subscr_signup and subscr_payment for
first payment, in random order. There is no support for changing
running subscription, so user needs to sign up for new subscription
and cancel old one.
Events for subscriptions are handled this way:
- subscr_payment finds UserSubscription object for User and
Subscription ID specified in the IPN. If UserSubscription is not
found, new one is created, which becomes inactive. Found or new
UserSubscription object is extended for the next billing period.
- subscr_signup finds UserSubscription object for User and
Subscription ID specified in the IPN. If UserSubscription is not
found, new one is created. Found or created UserSubscription is
set to active, User is added to subscription's group; if user has
another UserSubscription, they are made inactive and user is
removed from these Subscription groups. In effect, on signup the
new subscription becomes user's only active one, and its group
only subscription-related group to which user belongs.
- subscr_cancel finds relevant UserSubscription object. If it is
inactive (which means subscription change), removes user from its
subscription's group, and deletes the UserSubscription. If it is
active, does nothing, so user can use up rest of current billing
period.
So, signup flow is:
- user clicks in PayPal subscribe button displayed on subscription
detail page and subscribes at PayPal,
- subscr_payment extends the UserSubscription,
- subscr_signup makes the UserSubscription active and uncancelled
and adds user to group,
- whichever of those got called first, creates the UserSubscription.
Cancel flow is:
- user cancels subscription at PayPal,
- UserSubscription is active, so it is marked cancelled, kept and
stays valid until expiry.
Subscription change flow is:
- If user is allowed to change subscription, subscription detail page
displays PayPal subscribe button,
- user clicks subscribe button and signs up for new subscription at
PayPal,
- landing page after PayPal transaction displays link to PayPal
transaction list which user can use to cancel old subscription at
PayPal,
- user cancels old subscription at PayPal;
- whichever of subscr_payment or subscr_signup gets called first,
creates new, inactive, uncancelled UserSubscription instance,
- subscr_payment extends new UserSubscription instance for next
billing period,
- subscr_signup deactivates all active UserSubscriptions and removes
user from group; then, activates and uncancels new
UserSubscription and adds user to its subscription's group,
- subscr_cancel (which gets called after previous two, because user
needs some time to click through the PayPal forms) finds inactive
UserSubscription, ensures that user is really not member of group,
and deletes the UserSubscription object.
If user makes a mistake and cancels new subscription instead of the
old one, new subscription goes through "Cancel flow" above, does not
get deleted, so user has chance to fix things at PayPal. Project
should add `signals.unsubscribed' handler that would detect such
situation (if `usersubscription' parameter is active, and user has
inactive UserSubscription objects, cancel was probably a mistake)
and notify user of his mistake.
9 Example code
~~~~~~~~~~~~~~
Example usage and templates are available as `django-saas-kit'
project at [http://github.com/saas-kit/django-saas-kit/]
10 Bugs and omissions
~~~~~~~~~~~~~~~~~~~~~
- There is no `setup.py' script for automated installation.
- No support for PayPal PDT; PDT has only presentational value (IPN
needs to be received anyway, and PDT should be used only to
display transaction details to user on after transaction landing
page), so support for it has been intentionally omitted.
10.1 Plans
==========
- Single payments for subscription, including possibility of
pay-as-you-go scheme
11 License
~~~~~~~~~~
This project is licensed on terms of GPL (GPL-LICENSE.txt) licenses.