Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement subscriber sync from Paddle API #19

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

matteing
Copy link
Contributor

@matteing matteing commented Apr 10, 2020

Hi, I decided to implement subscriber sync.

A few things to note.

  • Some fields aren't returned - see lines marked with question marks. Some are even returned conditionally depending on the state of the subscription.
  • There is a serious problem - we can't get_or_create users (get_subscriber_model). The User model does not allow creating users without usernames, etc.
  • I had to change a few DB fields to accommodate the API returns.
  • No tests implemented yet. I did test with a prod workload and it seems to sync just fine, but nothing automated.

As a side note, I implemented a partial fix to bullet point 2: this seemed very worrying.

Code review welcome. So far it seems to be working fine, but it certainly isn't perfect.

matteing and others added 5 commits April 9, 2020 18:30
@matteing
Copy link
Contributor Author

Apologies for the messy commit history - it seems GH automatically merged another commit into my pull.

@matteing
Copy link
Contributor Author

Added a few other bugfixes:

  • Fix mistyped parameter in update_or_create: this was breaking functionality from_update.
  • Fix extra 'created' unpack: this fixed a mistake I made.

@matteing
Copy link
Contributor Author

matteing commented Apr 10, 2020

More bugfixes:

  • Made max_length on IDs TextFields rather than 32, for some reason Paddle is returning IDs of length 35 and this broke webhooks completely for me (on production).

@kennell
Copy link
Contributor

kennell commented Apr 10, 2020

@matteing A potential problem i see here is that the email used for Paddle checkout may not match the local user's email. A clean way of doing this would be to send a unique local user id as a passthrough argument when initializing the Paddle JS snippet (see https://paddle.com/docs/paddle-checkout-web/ - section "Sending additional user data"), this will then be included in the subscription_created webook and can be used to match a local user, even if a different email was used during the Paddle checkout.

However, Paddle currently only includes this passthrough string in the Webhook, not in the List-Subscriptions API endpoint*. So if anything goes wrong during the webhook, it will not be possible to manually sync via Django command later (or we will have to fallback to email matching).

*I've already contacted Paddle about including the passthrough argument is the API endpoint, was told they see that it would be useful but they can't provide a ETA.

Just something to consider.

@matteing
Copy link
Contributor Author

matteing commented Apr 10, 2020

I actually absolutely agree with @kennell on this subject (I face this problem on production), however I didn't find that the current code did this linking using the user_id argument - so I didn't implement it as to keep feature parity (see here).

If given a green light I think I can take a shot at adding user_id linking.

Edit: Reading more carefully, noted on the endpoint issue not returning webhook data.

"""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)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • since it can become a lot of subscriptions, this statement might be too verbose. Please make it a summary or add a parameter for verbose output
  • let's switch to self.stdout.write(self.style.SUCCESS('...')) - I'll change this in the plan-command as well

@@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-04-10 00:56
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • please squash migrations


def handle(self, *args, **options):
"""Call sync_from_paddle_data for each subscriber returned by api_list."""
for sub_data in Subscription.api_list():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • let's call it subscription_data

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please initialize kwargs with all parameters needed to create/update the Subscription except pk, then use update_or_create instead of deleting the Subscription right away.

  • [ } use update_or_create

update_url = data.get('update_url'),
**kwargs
)
return sub
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • please rename to instance or subscription

data["subscriber"], created = settings.get_subscriber_model().objects.get_or_create(
email=payload["email"]
)
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subscriber = settings.get_subscriber_model()
if subscriber_id not in ["", None]:
        try:        
                data[ "subscriber"] = Subscriber.objects.get(email=payload["email"])
        except settings.get_subscriber_model().DoesNotExist:
               data[ "subscriber"] = None

try:
kwargs['plan'] = Plan.objects.get(pk=data.get("plan_id"))
except Plan.DoesNotExist:
print("Skipping, plan not found.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • please use log.warn or make it a comment
  • please return None

quantity=data.get("last_payment", {}).get("amount", 0),
source="", # ???
status=data.get("state"),
unit_price=0.00, # ???
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matteing shall we rather make this a default in the model field?

@FPurchess
Copy link
Member

@matteing thanks for your good work! I added some comments. However, it would be great to have a test for it. I'd like to see our test-coverage reach a reasonable level soon.

@FPurchess
Copy link
Member

@matteing A potential problem i see here is that the email used for Paddle checkout may not match the local user's email. A clean way of doing this would be to send a unique local user id as a passthrough argument when initializing the Paddle JS snippet (see https://paddle.com/docs/paddle-checkout-web/ - section "Sending additional user data"), this will then be included in the subscription_created webook and can be used to match a local user, even if a different email was used during the Paddle checkout.

However, Paddle currently only includes this passthrough string in the Webhook, not in the List-Subscriptions API endpoint*. So if anything goes wrong during the webhook, it will not be possible to manually sync via Django command later (or we will have to fallback to email matching).

*I've already contacted Paddle about including the passthrough argument is the API endpoint, was told they see that it would be useful but they can't provide a ETA.

Just something to consider.

Agree. This is another good argument to make the matching of subscribers configurable with a sane default (e.g. email-matching). So the developer that implements dj-paddle into his project can decide how to handle it for his respective use-case.

@juanbzpy
Copy link

I need this feature. Are the owners of the repo active lately? I don't want to put in the work if they aren't reviewing pull requests.

@matteing
Copy link
Contributor Author

Package seems long unmaintained. I'm unsure of any alternatives. It's been a while since this thread! :)

@juanbzpy
Copy link

@matteing thanks for the reply and the initial work!

Package seems long unmaintained.

ugh, forking it is then.

@matteing
Copy link
Contributor Author

@matteing thanks for the reply and the initial work!

Package seems long unmaintained.

ugh, forking it is then.

That's a good approach! Although no shade on the original authors -- they did great work pushing support for Paddle with Django.

I hope the Paddle API primitives haven't changed since then!

@juanbzpy
Copy link

That's a good approach! Although no shade on the original authors -- they did great work pushing support for Paddle with Django.

Totally true. My comment was rude. The authors don't owe nobody their time and the project is of great value as it is.

@matteing
Copy link
Contributor Author

matteing commented May 26, 2023 via email

@saschwarz
Copy link

@j0lv3r4 I ended up forking for a fix/features I needed semi-recently. Don't know if you are interested in any of those changes: https://github.com/saschwarz/dj-paddle/commits/master

@juanbzpy
Copy link

@saschwarz awesome, thank you. I will check it out.

@kennell
Copy link
Contributor

kennell commented May 31, 2023

Just FYI i started a alternative implementation a while ago:

https://github.com/kennell/django-paddle

Far from feature complete, but perhaps a alternative to some -- or at least a inspiration ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants