├── .gitignore
├── README.md
├── django_paddle
├── __init__.py
├── admin.py
├── apps.py
├── client.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── paddle_sync_payments.py
│ │ ├── paddle_sync_plans.py
│ │ └── paddle_sync_subscriptions.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_paddlesubscription_cancellation_effective_date.py
│ └── __init__.py
├── models.py
├── receivers.py
├── signals.py
├── urls.py
├── utils.py
└── views.py
├── runtests.py
├── setup.py
└── tests
├── client
├── __init__.py
└── test_paddle_client.py
├── helpers.py
├── models
├── __init__.py
├── test_paddle_plan.py
└── test_paddle_subscription.py
├── settings.py
├── urls.py
└── webhooks
├── __init__.py
└── test_signals.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # PyCharm
2 | .idea/
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | .hypothesis/
51 | .pytest_cache/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 | db.sqlite3
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # Environments
88 | .env
89 | .venv
90 | env/
91 | venv/
92 | ENV/
93 | env.bak/
94 | venv.bak/
95 |
96 | # Spyder project settings
97 | .spyderproject
98 | .spyproject
99 |
100 | # Rope project settings
101 | .ropeproject
102 |
103 | # mkdocs documentation
104 | /site
105 |
106 | # mypy
107 | .mypy_cache/
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-paddle
2 |
3 | Django models and helpers for integrating Paddle.com subscriptions with your Django app
4 |
5 | ⚠️This library is very much **WORK IN PROGRESS**, please read this document carefully to understand what is currently supported.
6 |
7 | Currently this package includes:
8 |
9 | * Django Models for plans, plan prices, subscriptions, payments (invoices)
10 | * Django management commands for sycing plans, subscriptions, payments
11 | * Webhook receivers that handle subscription creation, subscription cancellation
12 |
13 | ### Installation
14 |
15 | Requires:
16 |
17 | * Python 3.6+
18 | * Django 2.1.0+
19 |
20 | 1. Install the `django-paddle` package
21 |
22 | ```
23 | pip install django-paddle
24 | ```
25 |
26 | 2. Add `django_paddle` to your INSTALLED_APPS
27 |
28 | ```python
29 | INSTALLED_APPS = [
30 | # ...
31 | 'django_paddle',
32 | # ...
33 | ]
34 | ```
35 |
36 | 3. In your `settings.py` add the following settings:
37 |
38 | ```python
39 | PADDLE_VENDOR_ID = 'your-vendor-id-here' # https://vendors.paddle.com/authentication
40 | PADDLE_AUTH_CODE = 'your-auth-code-here' # https://vendors.paddle.com/authentication
41 | PADDLE_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
42 | your
43 | public
44 | key
45 | here
46 | -----END PUBLIC KEY-----""" # https://vendors.paddle.com/public-key
47 | PADDLE_ACCOUNT_MODEL = 'auth.User'
48 | ```
49 |
50 | ℹ️ If you are using the default Django User model, set `PADDLE_ACCOUNT_MODEL` to `auth.User`. If you are using a custom User model set this to something like `your_custom_app.YourUserModel`.
51 |
52 | 5. In your projects main `urls.py` add the `django_paddle` URLs for receiving webhooks:
53 |
54 | ```python
55 | urlpatterns = [
56 | path('', include('django_paddle.urls')),
57 | ]
58 | ```
59 |
60 | ℹ️ This will result in an absolute webhook URL `https://example.com/webhook`. Make sure this is the Webhook URL you set in your Paddle settings (https://vendors.paddle.com/alerts-webhooks).
61 |
62 | 4. Run migrations
63 |
64 | `python manage.py migrate`
65 |
66 | The User Model specified in `PADDLE_ACCOUNT_MODEL` will now have a back-reference to the PaddleSubscription and vice versa.
67 |
68 | Example:
69 |
70 | ```python
71 | sub = PaddleSubscription.objects.all()[0]
72 | print(sub.account) #
73 | ```
74 |
75 | or
76 |
77 | ```python
78 | user = User.objects.get(username='johndoe@example.com')
79 | print(u.subscriptions.all()) # ]>
80 | ```
81 |
82 | 5. Done!
83 |
84 |
85 | ### Automatically connecting Users and Subscriptions
86 |
87 | We need a shared identifier between the User model and the PaddleSubscription model. This needs to be provided when we redirect a user to the Paddle checkout. If you are using the default Django User model you can provide a unique user ID as a passthrough value. The `subscription_created` webook will check the passtrough field and see if a User with this ID exists and automatically connect it to the newly created subscription.
88 |
89 | Example:
90 |
91 | ```html
92 |
93 |
101 | ```
102 |
103 | ### Django Management Commands
104 |
105 | * `manage.py paddle_sync_plans` - Syncs Subscription Plans
106 | * `manage.py paddle_sync_subscriptions` - Syncs Subscriptions
107 | * `manage.py paddle_sync_payments` - Syncs payments for all subscriptions
108 |
109 | ### Run tests
110 |
111 | ```
112 | python runtests.py
113 | ```
114 |
--------------------------------------------------------------------------------
/django_paddle/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennell/django-paddle/b74ffea4c84d652ee0a8066a33762818c7f69d00/django_paddle/__init__.py
--------------------------------------------------------------------------------
/django_paddle/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django_paddle.models import PaddlePlan, PaddleInitialPrice, PaddleRecurringPrice, PaddleSubscription
3 |
4 |
5 | class PaddleInitialPriceInline(admin.TabularInline):
6 | model = PaddleInitialPrice
7 |
8 |
9 | class PaddleRecurringPriceInline(admin.TabularInline):
10 | model = PaddleRecurringPrice
11 |
12 |
13 | class PaddlePlanAdmin(admin.ModelAdmin):
14 | list_display = ['id', 'name', 'billing_type', 'billing_period']
15 | inlines = [PaddleInitialPriceInline, PaddleRecurringPriceInline]
16 |
17 |
18 | class PaddleSubscriptionAdmin(admin.ModelAdmin):
19 | list_display = ['id', 'signup_date', 'user_email', 'state']
20 |
21 |
22 | admin.site.register(PaddlePlan, PaddlePlanAdmin)
23 | admin.site.register(PaddleSubscription, PaddleSubscriptionAdmin)
24 |
--------------------------------------------------------------------------------
/django_paddle/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DjangoPaddleConfig(AppConfig):
5 | name = 'django_paddle'
6 | verbose_name = 'Django Paddle'
7 |
--------------------------------------------------------------------------------
/django_paddle/client.py:
--------------------------------------------------------------------------------
1 | from copy import copy
2 | import requests
3 | from django.conf import settings
4 |
5 |
6 | class PaddleClient:
7 |
8 | def __init__(self):
9 | self.base_url = 'https://vendors.paddle.com/api/2.0/'
10 | self.vendor_id = settings.PADDLE_VENDOR_ID
11 | self.vendor_auth_code = settings.PADDLE_AUTH_CODE
12 | self.base_payload = {
13 | 'vendor_id': self.vendor_id,
14 | 'vendor_auth_code': self.vendor_auth_code
15 | }
16 |
17 | # Plans
18 |
19 | def plans_list(self):
20 | rsp = requests.post(
21 | url=self.base_url + 'subscription/plans',
22 | json=self.base_payload
23 | )
24 | return rsp.json()['response']
25 |
26 | def plans_get(self, plan_id):
27 | payload = copy(self.base_payload)
28 | payload.update(plan_id=plan_id)
29 | rsp = requests.post(
30 | url=self.base_url + 'subscription/plans',
31 | json=payload
32 | )
33 | return rsp.json()['response'][0]
34 |
35 | # Subscriptions
36 |
37 | def subscriptions_list(self, state=None):
38 |
39 | """
40 | :param state: filter by state, returns all active, past_due, trialing
41 | and paused subscription plans if not specified.
42 | Will NOT return deleted subscriptions
43 | See https://developer.paddle.com/api-reference/subscription-api/users/listusers
44 | """
45 |
46 | subscriptions = []
47 | max_results = 200
48 | payload = copy(self.base_payload)
49 | payload.update(page=1, results_per_page=max_results)
50 |
51 | if state:
52 | payload.update(state=state)
53 |
54 | while True:
55 | data = requests.post(
56 | url=self.base_url + 'subscription/users',
57 | json=payload
58 | ).json()['response']
59 | subscriptions += data
60 | if len(data) < max_results:
61 | break
62 | else:
63 | payload['page'] += 1
64 |
65 | return subscriptions
66 |
67 | def subscriptions_get(self, subscription_id):
68 | payload = copy(self.base_payload)
69 | payload.update(subscription_id=subscription_id)
70 | return requests.post(
71 | url=self.base_url + 'subscription/users',
72 | json=payload
73 | ).json()['response'][0]
74 |
75 | def subscriptions_cancel(self, subscription_id):
76 | payload = copy(self.base_payload)
77 | payload.update(
78 | subscription_id=subscription_id
79 | )
80 | requests.post(
81 | url=self.base_url + 'subscription/users_cancel',
82 | json=payload
83 | )
84 |
85 | def subscriptions_pause(self, subscription_id):
86 | payload = copy(self.base_payload)
87 | payload.update(
88 | subscription_id=subscription_id,
89 | pause=True
90 | )
91 | requests.post(
92 | url=self.base_url + 'subscription/users/update',
93 | json=payload
94 | )
95 |
96 | def subscriptions_unpause(self, subscription_id):
97 | payload = copy(self.base_payload)
98 | payload.update(
99 | subscription_id=subscription_id,
100 | pause=False
101 | )
102 | requests.post(
103 | url=self.base_url + 'subscription/users/update',
104 | json=payload
105 | )
106 |
107 | # Payments
108 |
109 | def payments_list(self, subscription_id=None, is_paid=None):
110 | payload = copy(self.base_payload)
111 | if subscription_id:
112 | payload.update(subscription_id=subscription_id)
113 | if is_paid is not None:
114 | payload.update(is_paid=is_paid)
115 | return requests.post(
116 | url=self.base_url + 'subscription/payments',
117 | json=payload
118 | ).json()['response']
119 |
120 | # Transactions
121 |
122 | def transactions_list(self, entity, id):
123 | payload = copy(self.base_payload)
124 | return requests.post(
125 | url=self.base_url + '{}/{}/transactions'.format(entity, id),
126 | json=payload
127 | ).json()['response']
128 |
--------------------------------------------------------------------------------
/django_paddle/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennell/django-paddle/b74ffea4c84d652ee0a8066a33762818c7f69d00/django_paddle/management/__init__.py
--------------------------------------------------------------------------------
/django_paddle/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennell/django-paddle/b74ffea4c84d652ee0a8066a33762818c7f69d00/django_paddle/management/commands/__init__.py
--------------------------------------------------------------------------------
/django_paddle/management/commands/paddle_sync_payments.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from django_paddle.models import PaddleSubscription, PaddlePayment
3 |
4 |
5 | class Command(BaseCommand):
6 | help = 'Sync payments'
7 |
8 | def handle(self, *args, **options):
9 | for subscription in PaddleSubscription.objects.all():
10 | subscription.sync_payments()
11 |
--------------------------------------------------------------------------------
/django_paddle/management/commands/paddle_sync_plans.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from django_paddle.models import PaddlePlan
3 |
4 |
5 | class Command(BaseCommand):
6 | help = 'Sync plans'
7 |
8 | def handle(self, *args, **options):
9 | PaddlePlan.sync()
10 |
--------------------------------------------------------------------------------
/django_paddle/management/commands/paddle_sync_subscriptions.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from django_paddle.models import PaddleSubscription
3 |
4 |
5 | class Command(BaseCommand):
6 | help = 'Sync subscriptions'
7 |
8 | def handle(self, *args, **options):
9 | PaddleSubscription.sync() # sync default
10 | PaddleSubscription.sync(state='deleted') # also sync deleted
11 |
--------------------------------------------------------------------------------
/django_paddle/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-05-09 22:04
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='PaddlePlan',
19 | fields=[
20 | ('id', models.PositiveIntegerField(primary_key=True, serialize=False, unique=True)),
21 | ('name', models.CharField(max_length=255)),
22 | ('billing_type', models.CharField(max_length=255)),
23 | ('billing_period', models.PositiveIntegerField()),
24 | ('trial_days', models.PositiveIntegerField()),
25 | ],
26 | ),
27 | migrations.CreateModel(
28 | name='PaddleSubscription',
29 | fields=[
30 | ('id', models.PositiveIntegerField(primary_key=True, serialize=False, unique=True)),
31 | ('user_id', models.PositiveIntegerField()),
32 | ('user_email', models.EmailField(max_length=254)),
33 | ('marketing_consent', models.BooleanField()),
34 | ('update_url', models.CharField(max_length=255)),
35 | ('cancel_url', models.CharField(max_length=255)),
36 | ('state', models.CharField(max_length=255)),
37 | ('signup_date', models.DateTimeField()),
38 | ('account', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscriptions', to=settings.AUTH_USER_MODEL)),
39 | ('plan', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscriptions', to='django_paddle.PaddlePlan')),
40 | ],
41 | ),
42 | migrations.CreateModel(
43 | name='PaddleRecurringPrice',
44 | fields=[
45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
46 | ('currency', models.CharField(max_length=255)),
47 | ('amount', models.CharField(max_length=255)),
48 | ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recurring_prices', to='django_paddle.PaddlePlan')),
49 | ],
50 | options={
51 | 'default_related_name': 'recurring_prices',
52 | },
53 | ),
54 | migrations.CreateModel(
55 | name='PaddlePayment',
56 | fields=[
57 | ('id', models.PositiveIntegerField(primary_key=True, serialize=False, unique=True)),
58 | ('amount', models.PositiveIntegerField()),
59 | ('currency', models.CharField(max_length=255)),
60 | ('payout_date', models.DateField(max_length=255)),
61 | ('is_paid', models.BooleanField()),
62 | ('is_one_off_charge', models.BooleanField()),
63 | ('receipt_url', models.CharField(max_length=255, null=True)),
64 | ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='django_paddle.PaddleSubscription')),
65 | ],
66 | ),
67 | migrations.CreateModel(
68 | name='PaddleInitialPrice',
69 | fields=[
70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
71 | ('currency', models.CharField(max_length=255)),
72 | ('amount', models.CharField(max_length=255)),
73 | ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='initial_prices', to='django_paddle.PaddlePlan')),
74 | ],
75 | options={
76 | 'default_related_name': 'initial_prices',
77 | },
78 | ),
79 | ]
80 |
--------------------------------------------------------------------------------
/django_paddle/migrations/0002_paddlesubscription_cancellation_effective_date.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.4 on 2021-11-28 15:32
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('django_paddle', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='paddlesubscription',
15 | name='cancellation_effective_date',
16 | field=models.DateTimeField(default=None, null=True, blank=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/django_paddle/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennell/django-paddle/b74ffea4c84d652ee0a8066a33762818c7f69d00/django_paddle/migrations/__init__.py
--------------------------------------------------------------------------------
/django_paddle/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from django.db import models
4 | from django.utils import timezone
5 | from django_paddle.client import PaddleClient
6 | from django_paddle.utils import get_account_model, get_account_by_passthrough
7 |
8 |
9 | pc = PaddleClient()
10 |
11 |
12 | class PaddlePlan(models.Model):
13 | id = models.PositiveIntegerField(
14 | primary_key=True,
15 | unique=True
16 | )
17 | name = models.CharField(max_length=255)
18 | billing_type = models.CharField(max_length=255)
19 | billing_period = models.PositiveIntegerField()
20 | trial_days = models.PositiveIntegerField()
21 |
22 | def initial_price_in(self, currency):
23 | return self.initial_prices.get(currency=currency).amount
24 |
25 | def recurring_price_in(self, currency):
26 | return self.recurring_prices.get(currency=currency).amount
27 |
28 | @staticmethod
29 | def sync():
30 | for plan in pc.plans_list():
31 | plan_obj, _ = PaddlePlan.objects.update_or_create(
32 | id=plan['id'],
33 | defaults={
34 | 'name': plan['name'],
35 | 'billing_type': plan['billing_type'],
36 | 'billing_period': plan['billing_period'],
37 | 'trial_days': plan['trial_days']
38 | }
39 | )
40 | for currency, amount in plan['initial_price'].items():
41 | plan_obj.initial_prices.update_or_create(
42 | currency=currency,
43 | defaults={
44 | 'amount': amount
45 | }
46 | )
47 | for currency, amount in plan['recurring_price'].items():
48 | plan_obj.recurring_prices.update_or_create(
49 | currency=currency,
50 | defaults={
51 | 'amount': amount
52 | }
53 | )
54 |
55 |
56 | class PaddlePrice(models.Model):
57 | plan = models.ForeignKey(
58 | to=PaddlePlan,
59 | on_delete=models.CASCADE,
60 | )
61 | currency = models.CharField(max_length=255)
62 | amount = models.CharField(max_length=255)
63 |
64 | class Meta:
65 | abstract = True
66 | unique_together = ['plan', 'currency']
67 |
68 |
69 | class PaddleInitialPrice(PaddlePrice):
70 | class Meta:
71 | default_related_name = 'initial_prices'
72 |
73 |
74 | class PaddleRecurringPrice(PaddlePrice):
75 |
76 | class Meta:
77 | default_related_name = 'recurring_prices'
78 |
79 |
80 | class PaddleSubscription(models.Model):
81 |
82 | # TODO: use enum types for state field instead of plain varchar strings
83 | # https://docs.djangoproject.com/en/3.0/ref/models/fields/#enumeration-types
84 |
85 | id = models.PositiveIntegerField(
86 | primary_key=True,
87 | unique=True
88 | )
89 | account = models.ForeignKey(
90 | to=get_account_model(),
91 | null=True,
92 | on_delete=models.SET_NULL,
93 | related_name='subscriptions'
94 | )
95 | plan = models.ForeignKey(
96 | to=PaddlePlan,
97 | null=True,
98 | on_delete=models.SET_NULL,
99 | related_name='subscriptions'
100 | )
101 | user_id = models.PositiveIntegerField()
102 | user_email = models.EmailField()
103 | marketing_consent = models.BooleanField()
104 | update_url = models.CharField(max_length=255)
105 | cancel_url = models.CharField(max_length=255)
106 | state = models.CharField(max_length=255)
107 | signup_date = models.DateTimeField()
108 | cancellation_effective_date = models.DateTimeField(null=True, blank=True, default=None)
109 |
110 | @property
111 | def is_canceled(self):
112 | return bool(self.cancellation_effective_date)
113 |
114 | @property
115 | def is_trialing(self):
116 | return self.state == 'trialing'
117 |
118 | @property
119 | def is_active(self):
120 | if self.state in ['active', 'past_due']:
121 | return True
122 |
123 | if self.is_canceled:
124 | if timezone.now() < self.cancellation_effective_date:
125 | return True
126 |
127 | return False
128 |
129 | def cancel(self):
130 | pc.subscriptions_cancel(self.id)
131 | self.state = 'deleted'
132 | self.save()
133 |
134 | def pause(self):
135 | pc.subscriptions_pause(subscription_id=self.id)
136 | self.state = 'paused'
137 | self.save()
138 |
139 | def unpause(self):
140 | pc.subscriptions_unpause(subscription_id=self.id)
141 | self.state = 'active'
142 | self.save()
143 |
144 | def sync_payments(self):
145 | for payment in pc.payments_list(subscription_id=self.id):
146 | defaults = {
147 | 'amount': payment['amount'],
148 | 'currency': payment['currency'],
149 | 'payout_date': timezone.make_aware(datetime.strptime(payment['payout_date'], '%Y-%m-%d')),
150 | 'is_paid': payment['is_paid'],
151 | 'is_one_off_charge': payment['is_one_off_charge'],
152 | }
153 | if 'receipt_url' in payment:
154 | defaults['receipt_url'] = payment['receipt_url']
155 | PaddlePayment.objects.update_or_create(
156 | id=payment['id'],
157 | subscription=self,
158 | defaults=defaults
159 | )
160 |
161 | @staticmethod
162 | def sync(state=None):
163 | for sub in pc.subscriptions_list(state=state):
164 | transaction = pc.transactions_list(entity='subscription', id=sub['subscription_id'])[0]
165 | account = get_account_by_passthrough(transaction['passthrough'])
166 |
167 | try:
168 | plan = PaddlePlan.objects.get(id=sub['plan_id'])
169 | except PaddlePlan.DoesNotExist:
170 | plan = None
171 |
172 | defaults = {
173 | 'user_id': sub['user_id'],
174 | 'user_email': sub['user_email'],
175 | 'marketing_consent': sub['marketing_consent'],
176 | 'update_url': sub['update_url'],
177 | 'cancel_url': sub['cancel_url'],
178 | 'state': sub['state'],
179 | 'signup_date': timezone.make_aware(datetime.strptime(sub['signup_date'], '%Y-%m-%d %H:%M:%S'))
180 | }
181 |
182 | if plan:
183 | defaults['plan'] = plan
184 |
185 | if account:
186 | defaults['account'] = account
187 |
188 | PaddleSubscription.objects.update_or_create(
189 | id=sub['subscription_id'],
190 | defaults=defaults
191 | )
192 |
193 |
194 | class PaddlePayment(models.Model):
195 | id = models.PositiveIntegerField(
196 | unique=True,
197 | primary_key=True
198 | )
199 | subscription = models.ForeignKey(
200 | to=PaddleSubscription,
201 | on_delete=models.CASCADE,
202 | related_name='payments'
203 | )
204 | amount = models.PositiveIntegerField()
205 | currency = models.CharField(max_length=255)
206 | payout_date = models.DateField(max_length=255)
207 | is_paid = models.BooleanField()
208 | is_one_off_charge = models.BooleanField()
209 | receipt_url = models.CharField(max_length=255, null=True)
210 |
--------------------------------------------------------------------------------
/django_paddle/receivers.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from django.dispatch import receiver
3 | from django.utils.timezone import make_aware
4 | from .signals import subscription_created, subscription_cancelled, subscription_updated
5 | from .models import PaddlePlan, PaddleSubscription
6 | from .utils import get_account_by_passthrough
7 |
8 |
9 | @receiver(subscription_created)
10 | def subscription_created_receiver(**kwargs):
11 | payload = kwargs['payload']
12 |
13 | account = get_account_by_passthrough(payload['passthrough'])
14 |
15 | try:
16 | plan = PaddlePlan.objects.get(id=payload['subscription_plan_id'])
17 | except PaddlePlan.DoesNotExist:
18 | # raise warning/exception here
19 | plan = None
20 |
21 | PaddleSubscription.objects.create(
22 | id=payload['subscription_id'],
23 | account=account,
24 | plan=plan,
25 | user_id=payload['user_id'],
26 | user_email=payload['email'],
27 | marketing_consent=bool(payload['marketing_consent']),
28 | update_url=payload['update_url'],
29 | cancel_url=payload['cancel_url'],
30 | state=payload['status'],
31 | signup_date=make_aware(datetime.strptime(payload['event_time'], '%Y-%m-%d %H:%M:%S')),
32 | cancellation_effective_date=None
33 | )
34 |
35 |
36 | @receiver(subscription_cancelled)
37 | def subscription_cancelled_receiver(**kwargs):
38 | payload = kwargs['payload']
39 |
40 | try:
41 | subscription = PaddleSubscription.objects.get(id=payload['subscription_id'])
42 | except PaddleSubscription.DoesNotExist:
43 | # raise warning/exception here
44 | pass
45 |
46 | subscription.cancellation_effective_date = make_aware(datetime.strptime(payload['cancellation_effective_date'], '%Y-%m-%d'))
47 | subscription.state = payload['status']
48 | subscription.save()
49 |
50 |
51 | @receiver(subscription_updated)
52 | def subscription_updated_receiver(**kwargs):
53 | payload = kwargs['payload']
54 |
55 | try:
56 | plan = PaddlePlan.objects.get(id=payload['subscription_plan_id'])
57 | except PaddlePlan.DoesNotExist:
58 | # raise warning/exception here
59 | plan = None
60 |
61 | try:
62 | subscription = PaddleSubscription.objects.get(id=payload['subscription_id'])
63 | except PaddleSubscription.DoesNotExist:
64 | # raise warning/exception here
65 | pass
66 |
67 | subscription.state = payload['status']
68 | subscription.plan = plan
69 | subscription.save()
70 |
--------------------------------------------------------------------------------
/django_paddle/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import Signal
2 |
3 |
4 | class WebhookSignalFactory():
5 |
6 | def __new__(self):
7 | return Signal()
8 |
9 |
10 | # Subscriptions
11 | subscription_created = WebhookSignalFactory()
12 | subscription_updated = WebhookSignalFactory()
13 | subscription_cancelled = WebhookSignalFactory()
14 | subscription_payment_succeeded = WebhookSignalFactory()
15 | subscription_payment_failed = WebhookSignalFactory()
16 | subscription_payment_refunded = WebhookSignalFactory()
17 |
18 | # One-off Purchases
19 | locker_processed = WebhookSignalFactory()
20 | payment_succeeded = WebhookSignalFactory()
21 | payment_refunded = WebhookSignalFactory()
22 |
23 | # Risk & Dispute Alerts
24 | payment_dispute_created = WebhookSignalFactory()
25 | payment_dispute_closed = WebhookSignalFactory()
26 | high_risk_transaction_created = WebhookSignalFactory()
27 | high_risk_transaction_updated = WebhookSignalFactory()
28 |
29 | # Payout Alerts
30 | transfer_created = WebhookSignalFactory()
31 | transfer_paid = WebhookSignalFactory()
32 |
33 | # Audience Alerts
34 | new_audience_member = WebhookSignalFactory()
35 | update_audience_member = WebhookSignalFactory()
36 |
--------------------------------------------------------------------------------
/django_paddle/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from django_paddle import views
4 |
5 |
6 | urlpatterns = [
7 | path("webhook", views.webhook, name="django_paddle_webhook"),
8 | ]
9 |
--------------------------------------------------------------------------------
/django_paddle/utils.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import collections
3 | import json
4 |
5 | import phpserialize
6 | from cryptography import exceptions
7 | from cryptography.hazmat.backends import default_backend
8 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
9 | from cryptography.hazmat.primitives.hashes import SHA1
10 | from cryptography.hazmat.primitives.serialization import load_pem_public_key
11 | from django.conf import settings
12 | from django.apps import apps
13 |
14 |
15 | def get_account_model():
16 | app, model = settings.PADDLE_ACCOUNT_MODEL.split('.')
17 | return apps.get_model(app, model, require_ready=False)
18 |
19 |
20 | def get_account_by_passthrough(passthrough):
21 | if passthrough:
22 | try:
23 | passthrough = json.loads(passthrough)
24 | account_id = passthrough['account_id']
25 | except json.decoder.JSONDecodeError:
26 | account_id = passthrough
27 | else:
28 | account_id = None
29 |
30 | try:
31 | account = get_account_model().objects.get(id=account_id)
32 | except get_account_model().DoesNotExist:
33 | account = None
34 |
35 | return account
36 |
37 |
38 | def webhook_signature_is_valid(payload):
39 |
40 | signature = base64.b64decode(payload.pop('p_signature'))
41 |
42 | for field in payload:
43 | payload[field] = str(payload[field])
44 |
45 | sorted_data = collections.OrderedDict(sorted(payload.items()))
46 | serialized_data = phpserialize.dumps(sorted_data)
47 |
48 | public_key = load_pem_public_key(settings.PADDLE_PUBLIC_KEY.encode(), backend=default_backend())
49 |
50 | try:
51 | public_key.verify(
52 | signature=signature,
53 | data=serialized_data,
54 | padding=PKCS1v15(),
55 | algorithm=SHA1()
56 | )
57 | return True
58 | except exceptions.InvalidSignature:
59 | return False
60 |
--------------------------------------------------------------------------------
/django_paddle/views.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 | from .utils import webhook_signature_is_valid
3 | from django.views.decorators.http import require_POST
4 | from django.views.decorators.csrf import csrf_exempt
5 | from django_paddle import signals
6 |
7 |
8 | class Webhook:
9 | pass
10 |
11 |
12 | @csrf_exempt
13 | @require_POST
14 | def webhook(request):
15 | payload = request.POST.dict()
16 |
17 | if webhook_signature_is_valid(payload):
18 | alert_name = payload['alert_name']
19 | signal = getattr(signals, alert_name)
20 | if signal:
21 | signal.send(
22 | sender=Webhook,
23 | payload=payload
24 | )
25 | else:
26 | # Log warning if alert_name does not match any signal?
27 | pass
28 |
29 | return HttpResponse()
30 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | import django
6 | from django.conf import settings
7 | from django.test.utils import get_runner
8 |
9 |
10 | if __name__ == '__main__':
11 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
12 | django.setup()
13 | TestRunner = get_runner(settings)
14 | test_runner = TestRunner()
15 | failures = test_runner.run_tests(['tests'])
16 | sys.exit(bool(failures))
17 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | setup(
5 | name='django-paddle',
6 | version='0.0.16',
7 | packages=find_packages(),
8 | description='Django models for integrating Paddle.com subscriptions',
9 | url='https://github.com/kennell/django-paddle',
10 | author='Kevin Kennell',
11 | author_email='kevin@kennell.de',
12 | install_requires=[
13 | 'django',
14 | 'requests',
15 | 'cryptography',
16 | 'phpserialize'
17 | ],
18 | extras_require={
19 | 'dev': [
20 | 'responses',
21 | ]
22 | }
23 | )
24 |
--------------------------------------------------------------------------------
/tests/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennell/django-paddle/b74ffea4c84d652ee0a8066a33762818c7f69d00/tests/client/__init__.py
--------------------------------------------------------------------------------
/tests/client/test_paddle_client.py:
--------------------------------------------------------------------------------
1 | import responses
2 | from django.test import TestCase
3 |
4 | from django_paddle.client import PaddleClient
5 |
6 |
7 | class TestPaddleClient(TestCase):
8 |
9 | def setUp(self):
10 | self.client = PaddleClient()
11 |
12 | # Plans
13 |
14 | @responses.activate
15 | def test_plans_list(self):
16 | expected = {
17 | 'success': True,
18 | 'response': [
19 | {
20 | 'id': 12345,
21 | 'name': 'Foo',
22 | 'billing_type': 'month',
23 | 'billing_period': 1,
24 | 'trial_days': 14,
25 | 'initial_price': {
26 | 'USD': '10.00',
27 | 'EUR': '10.00'
28 | },
29 | 'recurring_price': {
30 | 'USD': '10.00',
31 | 'EUR': '10.00'
32 | }
33 | }
34 | ]
35 | }
36 | responses.add(
37 | method=responses.POST,
38 | url='https://vendors.paddle.com/api/2.0/subscription/plans',
39 | json=expected
40 | )
41 | self.assertListEqual(
42 | self.client.plans_list(), expected['response']
43 | )
44 |
45 | # Subscriptions
46 |
47 | @responses.activate
48 | def test_subscriptions_list(self):
49 | expected = {
50 | 'success': True,
51 | 'response': [
52 | {
53 | 'subscription_id': 12345,
54 | 'plan_id': 12345,
55 | 'user_id': 12345,
56 | 'user_email': 'foo@example.com',
57 | 'marketing_consent': True,
58 | 'update_url': 'https://checkout.paddle.com/subscription/update?foo=bar&qux=baz',
59 | 'cancel_url': 'https://checkout.paddle.com/subscription/cancel?foo=bar&qux=baz',
60 | 'state': 'active',
61 | 'signup_date': '2020-01-01 20:20:20',
62 | 'last_payment': {
63 | 'amount': 0,
64 | 'currency': 'USD',
65 | 'date': '2019-11-10'
66 | },
67 | 'linked_subscriptions': []
68 | }
69 | ]
70 | }
71 | responses.add(
72 | method=responses.POST,
73 | url='https://vendors.paddle.com/api/2.0/subscription/users',
74 | json=expected
75 | )
76 | self.assertListEqual(
77 | self.client.subscriptions_list(), expected['response']
78 | )
79 |
80 | @responses.activate
81 | def test_subscriptions_list_pagination(self):
82 | responses.add(
83 | method=responses.POST,
84 | url='https://vendors.paddle.com/api/2.0/subscription/users',
85 | json={
86 | 'success': True,
87 | 'response': [{} for _ in range(0, 200)]
88 | }
89 | )
90 | responses.add(
91 | method=responses.POST,
92 | url='https://vendors.paddle.com/api/2.0/subscription/users',
93 | json={
94 | 'success': True,
95 | 'response': [{} for _ in range(0, 15)]
96 | }
97 | )
98 | data = self.client.subscriptions_list()
99 | self.assertEqual(len(responses.calls), 2)
100 | self.assertEqual(len(data), 215)
101 |
102 | @responses.activate
103 | def test_subscriptions_cancel(self):
104 | responses.add(
105 | method=responses.POST,
106 | url='https://vendors.paddle.com/api/2.0/subscription/users_cancel',
107 | json={
108 | 'success': True,
109 | }
110 | )
111 | self.assertEqual(self.client.subscriptions_cancel(123), None)
112 |
--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from unittest.mock import patch
3 |
4 |
5 | def mocked_webhook_signature_is_valid(*args):
6 | return True
7 |
8 |
9 | def disable_webhook_verification(f):
10 | @wraps(f)
11 | @patch('django_paddle.views.webhook_signature_is_valid', new=mocked_webhook_signature_is_valid)
12 | def wrapper(*args, **kwds):
13 | return f(*args, **kwds)
14 | return wrapper
15 |
--------------------------------------------------------------------------------
/tests/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennell/django-paddle/b74ffea4c84d652ee0a8066a33762818c7f69d00/tests/models/__init__.py
--------------------------------------------------------------------------------
/tests/models/test_paddle_plan.py:
--------------------------------------------------------------------------------
1 | import responses
2 | from django.test import TestCase
3 | from django_paddle.models import PaddlePlan, PaddleInitialPrice, PaddleRecurringPrice
4 |
5 |
6 | class TestPaddlePlan(TestCase):
7 |
8 | def setUp(self):
9 | self.plan = PaddlePlan.objects.create(
10 | id=123,
11 | name='Plan Foo',
12 | billing_type='month',
13 | billing_period=1,
14 | trial_days=14
15 | )
16 |
17 | def test_initial_price_in(self):
18 | PaddleInitialPrice.objects.create(
19 | plan=self.plan,
20 | currency='USD',
21 | amount='10.000'
22 | )
23 | self.assertEqual(self.plan.initial_price_in('USD'), '10.000')
24 |
25 | def test_recurring_price_in(self):
26 | PaddleRecurringPrice.objects.create(
27 | plan=self.plan,
28 | currency='USD',
29 | amount='10.000'
30 | )
31 | self.assertEqual(self.plan.recurring_price_in('USD'), '10.000')
32 |
33 | def test_cancel(self):
34 | # TBI
35 | self.assertTrue(True)
36 |
37 | def test_pause(self):
38 | # TBI
39 | self.assertTrue(True)
40 |
41 | def test_unpause(self):
42 | # TBI
43 | self.assertTrue(True)
44 |
45 |
46 | class TestPaddlePlanSync(TestCase):
47 |
48 | @responses.activate
49 | def test_sync_create(self):
50 | expected = {
51 | 'success': True,
52 | 'response': [
53 | {
54 | 'id': 12345,
55 | 'name': 'Foo',
56 | 'billing_type': 'month',
57 | 'billing_period': 1,
58 | 'trial_days': 14,
59 | 'initial_price': {
60 | 'USD': '10.00',
61 | 'EUR': '10.00'
62 | },
63 | 'recurring_price': {
64 | 'USD': '10.00',
65 | 'EUR': '10.00'
66 | }
67 | }
68 | ]
69 | }
70 | responses.add(
71 | method=responses.POST,
72 | url='https://vendors.paddle.com/api/2.0/subscription/plans',
73 | json=expected
74 | )
75 | PaddlePlan.sync()
76 | self.assertEqual(PaddlePlan.objects.count(), 1)
77 | self.assertEqual(PaddleInitialPrice.objects.count(), 2)
78 | self.assertEqual(PaddleRecurringPrice.objects.count(), 2)
79 |
80 | @responses.activate
81 | def test_sync_update(self):
82 | plan = PaddlePlan.objects.create(
83 | id=12345,
84 | name='Foo',
85 | billing_type='month',
86 | billing_period=1,
87 | trial_days=14
88 | )
89 | plan.initial_prices.create(
90 | currency='USD',
91 | amount='5.00'
92 | )
93 | plan.recurring_prices.create(
94 | currency='USD',
95 | amount='5.00'
96 | )
97 | expected = {
98 | 'success': True,
99 | 'response': [
100 | {
101 | 'id': 12345,
102 | 'name': 'Bar',
103 | 'billing_type': 'month',
104 | 'billing_period': 1,
105 | 'trial_days': 14,
106 | 'initial_price': {
107 | 'USD': '10.00',
108 | 'EUR': '10.00'
109 | },
110 | 'recurring_price': {
111 | 'USD': '10.00',
112 | 'EUR': '10.00'
113 | }
114 | }
115 | ]
116 | }
117 | responses.add(
118 | method=responses.POST,
119 | url='https://vendors.paddle.com/api/2.0/subscription/plans',
120 | json=expected
121 | )
122 | PaddlePlan.sync()
123 | plan.refresh_from_db()
124 | self.assertEqual(plan.name, 'Bar')
125 | self.assertEqual(plan.initial_price_in('USD'), '10.00')
126 | self.assertEqual(plan.initial_price_in('EUR'), '10.00')
127 | self.assertEqual(plan.recurring_price_in('USD'), '10.00')
128 | self.assertEqual(plan.recurring_price_in('EUR'), '10.00')
129 |
--------------------------------------------------------------------------------
/tests/models/test_paddle_subscription.py:
--------------------------------------------------------------------------------
1 | import responses
2 | from datetime import timedelta
3 | from django.test import TestCase
4 | from django_paddle.models import PaddleSubscription, PaddlePlan
5 | from django.utils import timezone
6 |
7 |
8 | class TestPaddleSubscription(TestCase):
9 |
10 | def setUp(self):
11 | self.subscription = PaddleSubscription.objects.create(
12 | id=12345,
13 | plan=PaddlePlan.objects.create(
14 | id=123,
15 | name='Plan Foo',
16 | billing_type='month',
17 | billing_period=1,
18 | trial_days=14
19 | ),
20 | user_id=12345,
21 | user_email='foo@example.com',
22 | marketing_consent=True,
23 | update_url='https://checkout.paddle.com/subscription/update?foo=bar&qux=baz',
24 | cancel_url='https://checkout.paddle.com/subscription/cancel?foo=bar&qux=baz',
25 | state='active',
26 | signup_date='2020-01-01 20:20:20',
27 | cancellation_effective_date=None
28 | )
29 |
30 | @responses.activate
31 | def test_subscription_cancel(self):
32 | responses.add(
33 | method=responses.POST,
34 | url='https://vendors.paddle.com/api/2.0/subscription/users_cancel',
35 | json={
36 | 'success': True
37 | }
38 | )
39 | self.subscription.cancel()
40 | self.subscription.refresh_from_db()
41 | self.assertEqual(self.subscription.state, 'deleted')
42 |
43 | @responses.activate
44 | def test_subscription_pause(self):
45 | # TBI
46 | self.assertTrue(True)
47 |
48 | @responses.activate
49 | def test_subscription_unpause(self):
50 | # TBI
51 | self.assertTrue(True)
52 |
53 |
54 | class TestPaddleSubscriptionIsCanceled(TestCase):
55 |
56 | """
57 | Tests the is_canceled property
58 | """
59 |
60 | def setUp(self):
61 | self.subscription = PaddleSubscription.objects.create(
62 | id=12345,
63 | plan=PaddlePlan.objects.create(
64 | id=123,
65 | name='Plan Foo',
66 | billing_type='month',
67 | billing_period=1,
68 | trial_days=14
69 | ),
70 | user_id=12345,
71 | user_email='foo@example.com',
72 | marketing_consent=True,
73 | update_url='https://checkout.paddle.com/subscription/update?foo=bar&qux=baz',
74 | cancel_url='https://checkout.paddle.com/subscription/cancel?foo=bar&qux=baz',
75 | state='active',
76 | signup_date='2020-01-01 20:20:20',
77 | cancellation_effective_date=None
78 | )
79 |
80 | def test_is_canceled_cancellation_effective_date_is_null(self):
81 | self.subscription.cancellation_effective_date = None
82 | self.subscription.save()
83 | self.assertFalse(self.subscription.is_canceled)
84 |
85 | def test_is_canceled_cancellation_effective_date_is_set(self):
86 | self.subscription.cancellation_effective_date = timezone.now()
87 | self.subscription.save()
88 | self.assertTrue(self.subscription.is_canceled)
89 |
90 |
91 | class TestPaddleSubscriptionIsActive(TestCase):
92 |
93 | """
94 | Tests the is_active property
95 | """
96 |
97 | def setUp(self):
98 | self.subscription = PaddleSubscription.objects.create(
99 | id=12345,
100 | plan=PaddlePlan.objects.create(
101 | id=123,
102 | name='Plan Foo',
103 | billing_type='month',
104 | billing_period=1,
105 | trial_days=14
106 | ),
107 | user_id=12345,
108 | user_email='foo@example.com',
109 | marketing_consent=True,
110 | update_url='https://checkout.paddle.com/subscription/update?foo=bar&qux=baz',
111 | cancel_url='https://checkout.paddle.com/subscription/cancel?foo=bar&qux=baz',
112 | state='active',
113 | signup_date='2020-01-01 20:20:20',
114 | cancellation_effective_date=None
115 | )
116 |
117 | def test_is_active(self):
118 | self.subscription.state = 'active'
119 | self.subscription.save()
120 | self.assertTrue(self.subscription.is_active)
121 |
122 | def test_is_active_deleted_no_cancellation_date(self):
123 | self.subscription.state = 'deleted'
124 | self.subscription.cancellation_effective_date = None
125 | self.subscription.save()
126 | self.assertFalse(self.subscription.is_active)
127 |
128 | def test_is_active_cancellation_date_in_future(self):
129 | self.subscription.state = 'deleted'
130 | self.subscription.cancellation_effective_date = timezone.now() + timedelta(days=3)
131 | self.subscription.save()
132 | self.assertTrue(self.subscription.is_active)
133 |
134 | def test_is_active_cancellation_date_in_past(self):
135 | self.subscription.state = 'deleted'
136 | self.subscription.cancellation_effective_date = timezone.now() - timedelta(days=3)
137 | self.subscription.save()
138 | self.assertFalse(self.subscription.is_active)
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | SECRET_KEY = 'fake-key'
2 |
3 | ROOT_URLCONF = 'tests.urls'
4 |
5 | DATABASES = {
6 | 'default': {
7 | 'ENGINE': 'django.db.backends.sqlite3'
8 | }
9 | }
10 |
11 | INSTALLED_APPS = [
12 | 'django.contrib.auth',
13 | 'django.contrib.contenttypes',
14 | 'django_paddle'
15 | ]
16 |
17 | PADDLE_VENDOR_ID = '12345'
18 | PADDLE_AUTH_CODE = 'very-secret-auth-code'
19 | PADDLE_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
20 | MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgFRcEWH8FJB0UnbD7Owhl6anraQS
21 | /5xrqyPyLkjR3Xb9/WsvrA1eP3ePg+vKdypMD+1puGg2/ler8aDi1OmvWC031ERs
22 | 06LHL628aVMvu1n6nZyKtvoFJpYTxBE804Evf6FSH5C+oba2BH6fEW9BxtraK7Co
23 | SKoHy0wFWqzUHsBBAgMBAAE=
24 | -----END PUBLIC KEY-----"""
25 | PADDLE_ACCOUNT_MODEL='auth.User'
--------------------------------------------------------------------------------
/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 |
3 | urlpatterns = [
4 | path("", include('django_paddle.urls'))
5 | ]
6 |
--------------------------------------------------------------------------------
/tests/webhooks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennell/django-paddle/b74ffea4c84d652ee0a8066a33762818c7f69d00/tests/webhooks/__init__.py
--------------------------------------------------------------------------------
/tests/webhooks/test_signals.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import MagicMock
2 | from django.test import Client, TestCase
3 | from django.urls import reverse
4 | from django_paddle import signals
5 | from django_paddle.views import Webhook
6 | from tests.helpers import disable_webhook_verification
7 |
8 |
9 | class TestWebhookSignals(TestCase):
10 |
11 | def setUp(self):
12 | self.client = Client()
13 | self.path = reverse('django_paddle_webhook')
14 |
15 | @disable_webhook_verification
16 | def test_subscription_created(self):
17 | receiver = MagicMock()
18 | signals.subscription_created.connect(receiver, sender=Webhook)
19 | rsp = self.client.post(
20 | path=self.path,
21 | data={
22 | 'alert_name': 'subscription_created',
23 | 'p_signature': 'cXdlcnR5'
24 | }
25 | )
26 |
27 | self.assertEqual(rsp.status_code, 200)
28 | receiver.assert_called_once()
29 |
30 |
--------------------------------------------------------------------------------