From 68d9f03a3dcdaaffb0ab5dde567415167f5f4812 Mon Sep 17 00:00:00 2001 From: Sergio Mattei <7959397+matteing@users.noreply.github.com> Date: Thu, 9 Apr 2020 18:30:20 -0400 Subject: [PATCH 1/8] Fix a deprecation warning in Django 2.0 Django 2 requires an app_name variable to be set on urls.py when using include(). https://stackoverflow.com/questions/48608894/impropyconfigurederror-about-app-name-when-using-namespace-in-include --- djpaddle/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/djpaddle/urls.py b/djpaddle/urls.py index 0a70d4e..649f6dc 100644 --- a/djpaddle/urls.py +++ b/djpaddle/urls.py @@ -2,6 +2,8 @@ from . import views +app_name = 'djpaddle' + urlpatterns = [ path("webhook/", views.paddle_webhook_view, name="webhook"), ] From 14d18821e0e66c7863e7eb04ddcde4d8f00d660a Mon Sep 17 00:00:00 2001 From: Sergio Mattei Date: Thu, 9 Apr 2020 20:55:22 -0400 Subject: [PATCH 2/8] Make a basic version of subscriber_sync --- .../djpaddle_sync_subscribers_from_paddle.py | 18 ++++++ djpaddle/models.py | 59 +++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py diff --git a/djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py b/djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py new file mode 100644 index 0000000..00c1b97 --- /dev/null +++ b/djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py @@ -0,0 +1,18 @@ +""" +sync_subscribers_from_paddle command. +""" +from django.core.management.base import BaseCommand + +from ...models import Subscription + + +class Command(BaseCommand): + """Sync subscribers from paddle.""" + + help = "Sync subscribers from paddle." + + def handle(self, *args, **options): + """Call sync_from_paddle_data for each subscriber returned by api_list.""" + for sub_data in Subscription.api_list(): + sub = Subscription.sync_from_paddle_data(sub_data) + print("Synchronized {0}".format(str(sub))) diff --git a/djpaddle/models.py b/djpaddle/models.py index 4a1bcd2..1e78d61 100644 --- a/djpaddle/models.py +++ b/djpaddle/models.py @@ -2,6 +2,7 @@ from django.db.models.signals import post_save from django.utils.translation import gettext_lazy as _ from django.dispatch import receiver +from django.utils import timezone from . import settings, signals, api from .fields import PaddleCurrencyCodeField @@ -123,6 +124,7 @@ class Subscription(PaddleBaseModel): (STATUS_PAUSED, _("paused")), (STATUS_DELETED, _("deleted")), ) + PADDLE_URI_LIST = 'subscription/users' id = models.CharField(max_length=32, primary_key=True) subscriber = models.ForeignKey( @@ -151,6 +153,51 @@ class Subscription(PaddleBaseModel): class Meta: ordering = ["created_at"] + @classmethod + def api_list(cls): + return api.retrieve(uri=cls.PADDLE_URI_LIST) + + @classmethod + def sync_from_paddle_data(cls, data): + pk = data.get('subscription_id', None) + # First, find and drop current sub with this data. + cls.objects.filter(pk=pk).delete() + kwargs = {} + + try: + kwargs['subscriber'] = settings.get_subscriber_model().objects.get( + email=data["user_email"] + ) + except settings.get_subscriber_model().DoesNotExist: + pass + + try: + kwargs['plan'] = Plan.objects.get(pk=data.get("plan_id")) + except Plan.DoesNotExist: + print("Skipping, plan not found.") + return + + # Now, create object with this pk. + sub = Subscription.objects.create( + id=pk, + cancel_url=data.get("cancel_url"), + checkout_id="", # ??? + currency=data.get('last_payment').get("currency"), + email=data.get("user_email"), + event_time=timezone.now(), # ??? + marketing_consent=data.get('marketing_consent'), + # next_bill_date can be null if user won't pay again... + next_bill_date=data.get("next_payment", {}).get("date", timezone.now()), + passthrough="", # ??? + quantity=data.get("last_payment", {}).get("amount", 0), + source="", # ??? + status=data.get("state"), + unit_price=0.00, # ??? + update_url = data.get('update_url'), + **kwargs + ) + return sub + @classmethod def _sanitize_webhook_payload(cls, payload): data = {} @@ -160,10 +207,14 @@ def _sanitize_webhook_payload(cls, payload): # transform `user_id` to subscriber ref data["subscriber"] = None subscriber_id = payload.pop("user_id", None) - if subscriber_id not in ["", None]: - data["subscriber"], created = settings.get_subscriber_model().objects.get_or_create( - email=payload["email"] - ) + try: + if subscriber_id not in ["", None]: + data[ + "subscriber"], created = settings.get_subscriber_model().objects.get( + email=payload["email"] + ) + except settings.get_subscriber_model().DoesNotExist: + pass # transform `subscription_plan_id` to plan ref data["plan"] = None From 1e8b2cd90f9550d47f9c81b4f550e24628c14a2b Mon Sep 17 00:00:00 2001 From: Sergio Mattei Date: Thu, 9 Apr 2020 20:56:34 -0400 Subject: [PATCH 3/8] Make next_bill_date nullable Next bill date can be null if a subscription != active --- djpaddle/migrations/0002_auto_20200410_0056.py | 18 ++++++++++++++++++ djpaddle/models.py | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 djpaddle/migrations/0002_auto_20200410_0056.py diff --git a/djpaddle/migrations/0002_auto_20200410_0056.py b/djpaddle/migrations/0002_auto_20200410_0056.py new file mode 100644 index 0000000..c2e1a55 --- /dev/null +++ b/djpaddle/migrations/0002_auto_20200410_0056.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-10 00:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djpaddle', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='subscription', + name='next_bill_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/djpaddle/models.py b/djpaddle/models.py index 1e78d61..995bc00 100644 --- a/djpaddle/models.py +++ b/djpaddle/models.py @@ -141,7 +141,8 @@ class Subscription(PaddleBaseModel): email = models.EmailField() event_time = models.DateTimeField() marketing_consent = models.BooleanField() - next_bill_date = models.DateTimeField() + next_bill_date = models.DateTimeField( + null=True, blank=True) passthrough = models.TextField() quantity = models.IntegerField() source = models.URLField() @@ -187,7 +188,7 @@ def sync_from_paddle_data(cls, data): event_time=timezone.now(), # ??? marketing_consent=data.get('marketing_consent'), # next_bill_date can be null if user won't pay again... - next_bill_date=data.get("next_payment", {}).get("date", timezone.now()), + next_bill_date=data.get("next_payment", {}).get("date", None), passthrough="", # ??? quantity=data.get("last_payment", {}).get("amount", 0), source="", # ??? From 444dbdce8c19c2c061a6a18b1eea4141c03a636e Mon Sep 17 00:00:00 2001 From: Sergio Mattei Date: Thu, 9 Apr 2020 20:55:22 -0400 Subject: [PATCH 4/8] Make a basic version of subscriber_sync --- .../djpaddle_sync_subscribers_from_paddle.py | 18 ++++++ djpaddle/models.py | 59 +++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py diff --git a/djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py b/djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py new file mode 100644 index 0000000..00c1b97 --- /dev/null +++ b/djpaddle/management/commands/djpaddle_sync_subscribers_from_paddle.py @@ -0,0 +1,18 @@ +""" +sync_subscribers_from_paddle command. +""" +from django.core.management.base import BaseCommand + +from ...models import Subscription + + +class Command(BaseCommand): + """Sync subscribers from paddle.""" + + help = "Sync subscribers from paddle." + + def handle(self, *args, **options): + """Call sync_from_paddle_data for each subscriber returned by api_list.""" + for sub_data in Subscription.api_list(): + sub = Subscription.sync_from_paddle_data(sub_data) + print("Synchronized {0}".format(str(sub))) diff --git a/djpaddle/models.py b/djpaddle/models.py index 4a1bcd2..1e78d61 100644 --- a/djpaddle/models.py +++ b/djpaddle/models.py @@ -2,6 +2,7 @@ from django.db.models.signals import post_save from django.utils.translation import gettext_lazy as _ from django.dispatch import receiver +from django.utils import timezone from . import settings, signals, api from .fields import PaddleCurrencyCodeField @@ -123,6 +124,7 @@ class Subscription(PaddleBaseModel): (STATUS_PAUSED, _("paused")), (STATUS_DELETED, _("deleted")), ) + PADDLE_URI_LIST = 'subscription/users' id = models.CharField(max_length=32, primary_key=True) subscriber = models.ForeignKey( @@ -151,6 +153,51 @@ class Subscription(PaddleBaseModel): class Meta: ordering = ["created_at"] + @classmethod + def api_list(cls): + return api.retrieve(uri=cls.PADDLE_URI_LIST) + + @classmethod + def sync_from_paddle_data(cls, data): + pk = data.get('subscription_id', None) + # First, find and drop current sub with this data. + cls.objects.filter(pk=pk).delete() + kwargs = {} + + try: + kwargs['subscriber'] = settings.get_subscriber_model().objects.get( + email=data["user_email"] + ) + except settings.get_subscriber_model().DoesNotExist: + pass + + try: + kwargs['plan'] = Plan.objects.get(pk=data.get("plan_id")) + except Plan.DoesNotExist: + print("Skipping, plan not found.") + return + + # Now, create object with this pk. + sub = Subscription.objects.create( + id=pk, + cancel_url=data.get("cancel_url"), + checkout_id="", # ??? + currency=data.get('last_payment').get("currency"), + email=data.get("user_email"), + event_time=timezone.now(), # ??? + marketing_consent=data.get('marketing_consent'), + # next_bill_date can be null if user won't pay again... + next_bill_date=data.get("next_payment", {}).get("date", timezone.now()), + passthrough="", # ??? + quantity=data.get("last_payment", {}).get("amount", 0), + source="", # ??? + status=data.get("state"), + unit_price=0.00, # ??? + update_url = data.get('update_url'), + **kwargs + ) + return sub + @classmethod def _sanitize_webhook_payload(cls, payload): data = {} @@ -160,10 +207,14 @@ def _sanitize_webhook_payload(cls, payload): # transform `user_id` to subscriber ref data["subscriber"] = None subscriber_id = payload.pop("user_id", None) - if subscriber_id not in ["", None]: - data["subscriber"], created = settings.get_subscriber_model().objects.get_or_create( - email=payload["email"] - ) + try: + if subscriber_id not in ["", None]: + data[ + "subscriber"], created = settings.get_subscriber_model().objects.get( + email=payload["email"] + ) + except settings.get_subscriber_model().DoesNotExist: + pass # transform `subscription_plan_id` to plan ref data["plan"] = None From e371caf74c39eac0753dd1cc94208317953cfabc Mon Sep 17 00:00:00 2001 From: Sergio Mattei Date: Thu, 9 Apr 2020 20:56:34 -0400 Subject: [PATCH 5/8] Make next_bill_date nullable Next bill date can be null if a subscription != active --- djpaddle/migrations/0002_auto_20200410_0056.py | 18 ++++++++++++++++++ djpaddle/models.py | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 djpaddle/migrations/0002_auto_20200410_0056.py diff --git a/djpaddle/migrations/0002_auto_20200410_0056.py b/djpaddle/migrations/0002_auto_20200410_0056.py new file mode 100644 index 0000000..c2e1a55 --- /dev/null +++ b/djpaddle/migrations/0002_auto_20200410_0056.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-10 00:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djpaddle', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='subscription', + name='next_bill_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/djpaddle/models.py b/djpaddle/models.py index 1e78d61..995bc00 100644 --- a/djpaddle/models.py +++ b/djpaddle/models.py @@ -141,7 +141,8 @@ class Subscription(PaddleBaseModel): email = models.EmailField() event_time = models.DateTimeField() marketing_consent = models.BooleanField() - next_bill_date = models.DateTimeField() + next_bill_date = models.DateTimeField( + null=True, blank=True) passthrough = models.TextField() quantity = models.IntegerField() source = models.URLField() @@ -187,7 +188,7 @@ def sync_from_paddle_data(cls, data): event_time=timezone.now(), # ??? marketing_consent=data.get('marketing_consent'), # next_bill_date can be null if user won't pay again... - next_bill_date=data.get("next_payment", {}).get("date", timezone.now()), + next_bill_date=data.get("next_payment", {}).get("date", None), passthrough="", # ??? quantity=data.get("last_payment", {}).get("amount", 0), source="", # ??? From ffef1a667043e93ad0d0fce54d5335b7393183e8 Mon Sep 17 00:00:00 2001 From: Sergio Mattei Date: Thu, 9 Apr 2020 22:11:06 -0400 Subject: [PATCH 6/8] Fix extra 'created' unpack --- djpaddle/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djpaddle/models.py b/djpaddle/models.py index 995bc00..b4e26b9 100644 --- a/djpaddle/models.py +++ b/djpaddle/models.py @@ -211,7 +211,7 @@ def _sanitize_webhook_payload(cls, payload): try: if subscriber_id not in ["", None]: data[ - "subscriber"], created = settings.get_subscriber_model().objects.get( + "subscriber"] = settings.get_subscriber_model().objects.get( email=payload["email"] ) except settings.get_subscriber_model().DoesNotExist: From ff86a946f410f3ca623443e67201a0e7020fbd01 Mon Sep 17 00:00:00 2001 From: Sergio Mattei Date: Thu, 9 Apr 2020 22:40:05 -0400 Subject: [PATCH 7/8] Fix mistyped parameter in update_or_create --- djpaddle/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djpaddle/models.py b/djpaddle/models.py index b4e26b9..0f4f9ec 100644 --- a/djpaddle/models.py +++ b/djpaddle/models.py @@ -244,7 +244,7 @@ def from_subscription_created(cls, payload): def update_by_payload(cls, payload): data = cls._sanitize_webhook_payload(payload) pk = data.pop("id") - return cls.objects.update_or_create(pk, defaults=data) + return cls.objects.update_or_create(pk=pk, defaults=data) def __str__(self): return "Subscription <{}:{}>".format(str(self.subscriber), str(self.id)) From a72e46f330d6653e7870cd265ba8b3668fc6d478 Mon Sep 17 00:00:00 2001 From: Sergio Mattei Date: Fri, 10 Apr 2020 00:57:24 -0400 Subject: [PATCH 8/8] Make fields with length=32 TextFields --- .../migrations/0003_auto_20200410_0455.py | 23 +++++++++++++++++++ djpaddle/models.py | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 djpaddle/migrations/0003_auto_20200410_0455.py diff --git a/djpaddle/migrations/0003_auto_20200410_0455.py b/djpaddle/migrations/0003_auto_20200410_0455.py new file mode 100644 index 0000000..dcba4d7 --- /dev/null +++ b/djpaddle/migrations/0003_auto_20200410_0455.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.5 on 2020-04-10 04:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djpaddle', '0002_auto_20200410_0056'), + ] + + operations = [ + migrations.AlterField( + model_name='subscription', + name='checkout_id', + field=models.TextField(), + ), + migrations.AlterField( + model_name='subscription', + name='id', + field=models.TextField(primary_key=True, serialize=False), + ), + ] diff --git a/djpaddle/models.py b/djpaddle/models.py index 0f4f9ec..ed362c5 100644 --- a/djpaddle/models.py +++ b/djpaddle/models.py @@ -126,7 +126,7 @@ class Subscription(PaddleBaseModel): ) PADDLE_URI_LIST = 'subscription/users' - id = models.CharField(max_length=32, primary_key=True) + id = models.TextField(primary_key=True) subscriber = models.ForeignKey( settings.DJPADDLE_SUBSCRIBER_MODEL, related_name="subscriptions", @@ -136,7 +136,7 @@ class Subscription(PaddleBaseModel): ) cancel_url = models.URLField() - checkout_id = models.CharField(max_length=32) + checkout_id = models.TextField() currency = models.CharField(max_length=3) email = models.EmailField() event_time = models.DateTimeField()