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

Paddle payment provider #165

Merged
merged 14 commits into from Nov 18, 2020
Merged

Paddle payment provider #165

merged 14 commits into from Nov 18, 2020

Conversation

nm
Copy link
Contributor

@nm nm commented Jun 5, 2020

Why Paddle?

For many countries there are cases where the tax is due in the target country of a customer. This makes it obviously very complicated for small companies. Paddle acts on your behalf as what is called a Merchant of Record (MOR). That makes them different from Stripe or Paypal. If you don't want to deal with tax or invoicing issues, Paddle is an interesting alternative.

Integration

Paddle is different from Stripe and Braintree in several ways. This makes integration a bit challenging. I tried to adapt to the existing implementation structure as good as possible, but there are a few edge cases:

  • It is not possible to create customer or subscription objects via the API. This only works via webhooks. You have to use the Paddle passthrough parameter to be able to assign the subscription owner correctly.
  • Payment details can only be updated via an update url. That's why I added the fields update_url and cancel_url to the subscription model. It is also possible to change certain fields via the API (plan change, quantity, cancellation, ..), but for updating payment details you definitely need the update URL.
  • To change invoice data or VAT ID, customers must use a receipt url. For this reason, I have also added the receipt_url field to the Charge Model. The field may also be useful for other external invoicing solutions.
  • Charges are only possible on top of an existing subscription.

Paddle Gem

I also wrote a paddle gem which is integrated in the proposed integration.

The pull request further includes tests and an update of the readme.md.
I am happy to discuss the integration approach and can also do improvements or modifications if needed.

@excid3
Copy link
Collaborator

excid3 commented Jun 5, 2020

Wow, this is awesome @nm 🙏

I'm finishing up moving to a new place, so it may be a couple days before I can fully review this.

One thing we definitely need is a better documentation system than the README, especially with the introduction of Paddle since there are a decent bit of unique differences / examples that will need to go along with it.

@iamajvillalobos
Copy link

I'm using this currently on building https://btfy.io. Seems alright! I haven't used in prod yet since I'm still working on the authorization. Looking forward for this to get merge :)

@excid3
Copy link
Collaborator

excid3 commented Aug 28, 2020

Looks like we're gonna want to start putting together a more organized set of docs because Paddle introduces a lot of exceptions, and I'm sure we'll have plenty of exceptions as we get into marketplaces and other things.

Maybe the wiki or github pages?

I'll probably merge this shortly. I just wanted to find some time to try it out before I did.

@nm
Copy link
Contributor Author

nm commented Aug 30, 2020

Yes, I think an separate page for each payment provider definitely makes sense. Paddle is different from Stripe or Braintree in many ways and therefore the documentation will also be different.

At this stage I don't think github pages or wiki make a huge difference. The wiki would probably be sufficient for now, but choose whatever you prefer. I can then adapt the documentation for paddle with pleasure.

In a further step, one would certainly have to investigate again to what extent the various payment providers can be combined and for which functions it makes sense. Regarding Paddle handling the webhooks was pretty much the same, but the customer and subscriptions objects were different.

Please let me know if there is anything I can do to help.

@deanpcmad
Copy link
Contributor

This looks great! Nice work @nm, I started working on a gem and was going to start working on the same integration before I noticed this PR

lib/pay/paddle/webhooks/subscription_created.rb Outdated Show resolved Hide resolved
product: 12345,
passthrough: "{\"owner_id\": 98765, \"owner_type\": \"User\"}"
});
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, if using the paddle button:

<a href="#!" class="paddle_button" data-product="12345" data-email="<%= current_user.email %>" data-passthrough="<%= { owner_id: current_user.id, owner_type: "User" }.to_json %>"

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this secure? I could change the model and ID to anything.

Seems like we should do a signed GlobalID or something instead that we could verify server-side.

Copy link
Contributor Author

@nm nm Nov 17, 2020

Choose a reason for hiding this comment

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

Yes, you're right. That would allow to pay for another owner id or owner type. I changed it and now a signed GlobalID is used. In addition, a purpose string ensures that GlobalIDs cannot be swapped between Paddle products.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is received by the webhook for the subscription, I think the for: is probably unnecessary?

If so, we can remove it and simplify a little bit which is nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It depends: If you can create subscriptions for multiple owner types in your app or signed global IDs are made public otherwise, the for: would be necessary to prevent swapping the SGID in the javascript code.

Paddle.Checkout.open({
	product: 12345,
	passthrough: "{\"owner_sgid\": \"<%= modelA.to_sgid.to_s %>\"}"
});
Paddle.Checkout.open({
	product: 67890,
	passthrough: "{\"owner_sgid\": \"<%= modelB.to_sgid.to_s %>\"}"
});

How likely is such a scenario? Probably not very likely. To be honest: If I pay for a subscription I am not interesting in changing the owner. That is the reason I just used owner_type and owner_id in the beginning. I don't know, maybe using SGIDs without for: is a good tradeoff between simplicity and security. What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

At least with an SGID, they could only swap SGIDs they had access to since it'd have to be generated server side. That'd leave it only to accounts they have access to.

If it was clear text, it's easy to tamper with. To be fair, you wouldn't want to pay for something and get it assigned to someone else, so it seems illogical to tamper with.

Maybe I'll build a helper to generate the passthrough JSON.

lib/pay/paddle/webhooks/subscription_cancelled.rb Outdated Show resolved Hide resolved
@grk
Copy link
Contributor

grk commented Sep 26, 2020

I started integrating this locally and in my so far limited testing it seems to be working without issues. Not deployed to production yet though.

@nm
Copy link
Contributor Author

nm commented Sep 27, 2020

Thanks @grk for the review! I've included your suggestions in the PR.

@excid3
Copy link
Collaborator

excid3 commented Sep 27, 2020

Sorry for the delay on this, I got busy and I get married tomorrow so I'll dive into this once I get back from our honeymoon.

Thanks for the work on this! It's looking good.

@misner
Copy link

misner commented Oct 6, 2020

+1 on this one. plus I'm thinking about using jumpstartpro which use itself pay gem. Would I be able then to use paddle via pay gem within jumpstart pro with minimum adjustement?


charge.update(
amount: Integer(data["sale_gross"].to_f * 100),
card_type: data["payment_method"],
Copy link
Contributor

Choose a reason for hiding this comment

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

payment_method will be one of card, paypal, free, apple-pay, wire-transfer, not sure if it fits in this field

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, storing the payment method in a card type column is definitely misleading. I'll second that. But this is consistent to the existing Braintree integration which stores "PayPal" in the card_type column as well.

Maybe it would be better to have more general columns for payment method and payment method meta data. But I didn't want to alter too much with this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, we need to improve this in Pay at some point. Braintree stores "paypal" in card_type and the paypal email in card_last4 which isn't great.

card_type: data["payment_method"],
receipt_url: data["receipt_url"],
created_at: DateTime.parse(data["event_time"])
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be cool if we could store all the information from the webhook somehow. In postgres I'd create a jsonb column for that, but I'm not sure what would be an universal method for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a great idea! Having a json column both on the subscription and the charges table would allow payment provider specific information to be saved easily. I know that @excid3 used a json column for storing params in the noticed gem. Maybe that would fit in here as well?

Copy link
Collaborator

Choose a reason for hiding this comment

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

JSON column would be nice, although compatibility is tough. I'm serializing and using text for sqlite, json for mysql, and jsonb for postgres in noticed and serializing with the ActiveJob coder. That works pretty well, but not queryable in all cases. I think that's probably okay. We can open up a separate issue for that.

@Ruk33
Copy link

Ruk33 commented Nov 13, 2020

Hello, thank you for the amazing work. I set it up on staging just to test it out and seems to be working, only issue is that when a user subscribes, the ends_at property is nil, I don't know if this is by design. Regards.

@excid3
Copy link
Collaborator

excid3 commented Nov 13, 2020

ends_at should be nil for active subscriptions. It's only set once canceled.

@excid3
Copy link
Collaborator

excid3 commented Nov 13, 2020

Since this PR adds columns unique to Paddle, this is probably a good opportunity to refactor and use json columns instead.

I think I'm going to do that using what we've got in the Noticed gem.

Other than that, I think this looks fantastic and will merge it right away. 👍

Thanks for your patience on this guys! 🙏

@excid3
Copy link
Collaborator

excid3 commented Nov 16, 2020

I ended up having to do a lot of changes for the json column. Will you guys check out this branch and let me know if it works? https://github.com/pay-rails/pay/tree/nm-paddle

@excid3 excid3 merged commit a665e01 into pay-rails:master Nov 18, 2020
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

7 participants