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/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/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 4a1bcd2..ed362c5 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,8 +124,9 @@ class Subscription(PaddleBaseModel): (STATUS_PAUSED, _("paused")), (STATUS_DELETED, _("deleted")), ) + 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", @@ -134,12 +136,13 @@ 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() 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() @@ -151,6 +154,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", None), + 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 +208,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"] = 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 @@ -192,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)) 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"), ]