pay-lago 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +28 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +2 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +292 -0
- data/LICENSE.txt +21 -0
- data/README.md +85 -0
- data/Rakefile +32 -0
- data/app/controllers/pay/webhooks/lago_controller.rb +41 -0
- data/app/models/concerns/pay/lago/pay_customer_extensions.rb +15 -0
- data/app/models/concerns/pay/lago/pay_extensions.rb +15 -0
- data/bin/console +15 -0
- data/bin/rails +16 -0
- data/bin/setup +8 -0
- data/config/routes.rb +5 -0
- data/docs/1_customers.md +18 -0
- data/docs/2_charges.md +72 -0
- data/docs/3_subscriptions.md +67 -0
- data/docs/4_webhooks.md +66 -0
- data/lib/pay/lago/attributes.rb +24 -0
- data/lib/pay/lago/billable.rb +148 -0
- data/lib/pay/lago/charge.rb +82 -0
- data/lib/pay/lago/engine.rb +27 -0
- data/lib/pay/lago/error.rb +9 -0
- data/lib/pay/lago/payment_method.rb +37 -0
- data/lib/pay/lago/subscription.rb +134 -0
- data/lib/pay/lago/version.rb +7 -0
- data/lib/pay/lago/webhook_extensions.rb +10 -0
- data/lib/pay/lago/webhooks/customer_payment_provider_created.rb +18 -0
- data/lib/pay/lago/webhooks/invoice_created.rb +15 -0
- data/lib/pay/lago/webhooks/invoice_drafted.rb +11 -0
- data/lib/pay/lago/webhooks/invoice_one_off_created.rb +15 -0
- data/lib/pay/lago/webhooks/invoice_payment_status_updated.rb +14 -0
- data/lib/pay/lago/webhooks/subscription_started.rb +12 -0
- data/lib/pay/lago/webhooks/subscription_terminated.rb +12 -0
- data/lib/pay/lago.rb +91 -0
- data/pay-lago.gemspec +27 -0
- data/sig/pay/lago.rbs +6 -0
- data/test/controllers/pay/webhooks/lago_controller_test.rb +37 -0
- data/test/dummy/.browserslistrc +1 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +4 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/channels/application_cable/channel.rb +4 -0
- data/test/dummy/app/channels/application_cable/connection.rb +4 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/main_controller.rb +4 -0
- data/test/dummy/app/controllers/payment_methods_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/helpers/current_helper.rb +9 -0
- data/test/dummy/app/javascript/application.js +9 -0
- data/test/dummy/app/javascript/controllers/application.js +10 -0
- data/test/dummy/app/javascript/controllers/index.js +11 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/account.rb +3 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/team.rb +10 -0
- data/test/dummy/app/models/user.rb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +36 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/app/webhooks/charge_succeeded.rb +5 -0
- data/test/dummy/bin/importmap +4 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +33 -0
- data/test/dummy/bin/webpack +18 -0
- data/test/dummy/bin/webpack-dev-server +18 -0
- data/test/dummy/config/application.rb +25 -0
- data/test/dummy/config/boot.rb +6 -0
- data/test/dummy/config/cable.yml +9 -0
- data/test/dummy/config/database.yml +3 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +70 -0
- data/test/dummy/config/environments/production.rb +86 -0
- data/test/dummy/config/environments/test.rb +60 -0
- data/test/dummy/config/importmap.rb +8 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/pay.rb +11 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/puma.rb +47 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config/storage.yml +34 -0
- data/test/dummy/config.ru +6 -0
- data/test/dummy/db/migrate/20170205000000_create_users.rb +22 -0
- data/test/dummy/db/schema.rb +125 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/vendor/javascript/.keep +0 -0
- data/test/fixtures/accounts.yml +2 -0
- data/test/fixtures/pay/customers.yml +7 -0
- data/test/fixtures/teams.yml +4 -0
- data/test/fixtures/users.yml +11 -0
- data/test/pay/lago/billable_test.rb +42 -0
- data/test/pay/lago/charge_test.rb +23 -0
- data/test/pay/lago/subscription_test.rb +48 -0
- data/test/support/fixtures/lago/invoice.one_off_created.json +120 -0
- data/test/support/vcr.rb +28 -0
- data/test/test_helper.rb +66 -0
- data/test/vcr_cassettes/test_add_payment_method_to_lago.yml +56 -0
- data/test/vcr_cassettes/test_can_get_lago_processor_customer.yml +56 -0
- data/test/vcr_cassettes/test_can_make_a_lago_processor_charge.yml +163 -0
- data/test/vcr_cassettes/test_generates_lago_processor_id.yml +107 -0
- data/test/vcr_cassettes/test_lago_cannot_change_quantity.yml +108 -0
- data/test/vcr_cassettes/test_lago_processor_cancel.yml +212 -0
- data/test/vcr_cassettes/test_lago_processor_charge.yml +325 -0
- data/test/vcr_cassettes/test_lago_processor_on_grace_period__is_always_false.yml +108 -0
- data/test/vcr_cassettes/test_lago_processor_pause_raises_an_error.yml +108 -0
- data/test/vcr_cassettes/test_lago_processor_paused__is_always_false.yml +108 -0
- data/test/vcr_cassettes/test_lago_processor_refund_with_premium.yml +378 -0
- data/test/vcr_cassettes/test_lago_processor_refund_without_premium.yml +375 -0
- data/test/vcr_cassettes/test_lago_processor_resume_raises_an_error.yml +108 -0
- data/test/vcr_cassettes/test_lago_processor_subscribe.yml +108 -0
- data/test/vcr_cassettes/test_lago_processor_subscription.yml +160 -0
- data/test/vcr_cassettes/test_lago_processor_swap.yml +212 -0
- metadata +212 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module Pay
|
2
|
+
module Lago
|
3
|
+
module PayExtensions
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
scope :lago, -> { joins(:customer).where(pay_customers: {processor: "lago"}) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def lago?
|
11
|
+
customer.processor == "lago"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "pay/lago"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/rails
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
3
|
+
# installed from the root of your application.
|
4
|
+
|
5
|
+
ENV["RAILS_ENV"] ||= "test"
|
6
|
+
|
7
|
+
ENGINE_ROOT = File.expand_path('..', __dir__)
|
8
|
+
ENGINE_PATH = File.expand_path('../lib/pay/lago/engine', __dir__)
|
9
|
+
APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
|
10
|
+
|
11
|
+
# Set up gems listed in the Gemfile.
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
13
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
14
|
+
|
15
|
+
require 'rails/all'
|
16
|
+
require 'rails/engine/commands'
|
data/bin/setup
ADDED
data/config/routes.rb
ADDED
data/docs/1_customers.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Customers
|
2
|
+
|
3
|
+
## Adding a Customer to Lago
|
4
|
+
Calling the `customer` method on an existing Pay::Customer with the Lago processor will automatically create a Lago customer,
|
5
|
+
with the external_id of the Pay::Customer's processor_id.
|
6
|
+
|
7
|
+
If a processor_id is not set, it will use the [GlobalID](https://github.com/rails/globalid) of the Pay::Customer object
|
8
|
+
as the external_id, and update processor_id to match.
|
9
|
+
|
10
|
+
## Adding a Customer from Lago
|
11
|
+
Creating a Pay::Customer with a matching processor_id to a Lago Customer's external_id, and giving them the Lago processor,
|
12
|
+
will sync the two records when the `customer` method is called.
|
13
|
+
|
14
|
+
## Updating a Lago Customer
|
15
|
+
Lago Customers can be updated by calling `update_customer!` on the Pay::Customer object. The method takes a Hash of attributes to be
|
16
|
+
changed, and returns a Lago Customer [OpenStruct](https://ruby-doc.org/current/stdlibs/ostruct/OpenStruct.html).
|
17
|
+
|
18
|
+
See [Lago Customer API](https://docs.getlago.com/api-reference/customers/update) for valid attributes.
|
data/docs/2_charges.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Charges
|
2
|
+
|
3
|
+
Lago itself does not create charges. Rather, it creates invoices, and triggers a charge through another service if configured.
|
4
|
+
|
5
|
+
Lago invoices will sync to Pay::Charges however, if the webhook is configured.
|
6
|
+
|
7
|
+
## Charging a Customer
|
8
|
+
Even though Lago doesn't necessarily support arbitrary charges, Pay implements somewhat of a workaround to maintain consistency.
|
9
|
+
|
10
|
+
Using the `charge` method on the Pay::Customer will function as a normal charge, taking an amount, then "charging" the customer
|
11
|
+
said amount.
|
12
|
+
|
13
|
+
This creates a [One-off Invoice](https://docs.getlago.com/guide/one-off-invoices/create-one-off-invoices) on Lago.
|
14
|
+
|
15
|
+
One-off Invoices require an add-on on Lago. Pay will automatically create a "default" add-on, and use that if no add-on is provided
|
16
|
+
to the `charge` method.
|
17
|
+
|
18
|
+
### Using your Own Add-ons
|
19
|
+
While not explicitly necessary, **it is best practise to use your own add-ons**.
|
20
|
+
|
21
|
+
Add-ons should provide meaningful information about the service to the invoice. Creating your own allows you to give it a nice name, and a description.
|
22
|
+
|
23
|
+
Create add-ons through the [API](https://docs.getlago.com/api-reference/add-ons/create), or by using the Lago UI.
|
24
|
+
|
25
|
+
You can provide your addon code to the `charge` method, for it to use that add-on instead.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Using an add-on with the charge method
|
29
|
+
customer = Pay::Customer.find(1234)
|
30
|
+
customer.charge(10_00, addon: "my-addon-code")
|
31
|
+
```
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# If you would like to add some attributes, pass an options hash.
|
35
|
+
# Note: currency must match the customer's currency.
|
36
|
+
customer = Pay::Customer.find(1234)
|
37
|
+
customer.charge(10_00, addon: "my-addon-code", options: { currency: "AUD" })
|
38
|
+
```
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# If you would like to use the amount defined in the add-on instead,
|
42
|
+
# set the charge amount to a falsey value.
|
43
|
+
customer = Pay::Customer.find(1234)
|
44
|
+
customer.charge(nil, addon: "my-addon-code")
|
45
|
+
```
|
46
|
+
|
47
|
+
## Updating a Lago Invoice
|
48
|
+
Lago Invoices can be updated by calling `update_charge!` on the Pay::Charge object. The method takes a Hash of attributes to be
|
49
|
+
changed, and returns a Lago Invoice [OpenStruct](https://ruby-doc.org/current/stdlibs/ostruct/OpenStruct.html).
|
50
|
+
|
51
|
+
See [Lago Invoice API](https://docs.getlago.com/api-reference/invoices/update) for valid attributes.
|
52
|
+
|
53
|
+
## Issuing a Refund
|
54
|
+
**Refunding charges requires [Lago Premium!](https://www.getlago.com/pricing)**
|
55
|
+
|
56
|
+
If you are self hosting Lago, its fairly trivial to override the premium license check. However, if you are using the cloud service, you will have to purchase Lago Premium. If your organisation has the means, please consider purchasing Lago Premium to support
|
57
|
+
their open source development. Keep in mind: any changes to the source you make must be released as per the terms of AGPL.
|
58
|
+
|
59
|
+
Lago creates refunds by issuing a [Credit Note](https://docs.getlago.com/guide/credit-notes).
|
60
|
+
|
61
|
+
The `refund!` method on Pay::Charge will create a credit note automatically for the provided amount. If Lago Premium is not
|
62
|
+
enabled, this method will raise Pay::Lago::Error.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# Refund $10 of a charge, and provide a reason
|
66
|
+
charge = Pay::Charge.find(1234)
|
67
|
+
charge.refund!(10_00, reason: "order_cancellation")
|
68
|
+
```
|
69
|
+
|
70
|
+
You can pass a hash of attributes optionally to configure the credit note further.
|
71
|
+
|
72
|
+
See [Lago Credit Note API](https://docs.getlago.com/api-reference/credit-notes/create) for valid attributes.
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# Subscriptions
|
2
|
+
|
3
|
+
## Adding a Subscription to a Customer
|
4
|
+
|
5
|
+
Customers can be subscribed to a plan using the `subscribe` method on Pay::Customer. Subscriptions should be given a name,
|
6
|
+
and must tie to an existing plan in Lago.
|
7
|
+
|
8
|
+
Plans can be created using the [Lago API](https://docs.getlago.com/api-reference/plans/create), or through the UI.
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
# Subscribe a customer to the plan with code "my-plan"
|
12
|
+
customer = Pay::Customer.find(1234)
|
13
|
+
customer.subscribe(name: "My Subscription", plan: "my-plan")
|
14
|
+
```
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# Subscribe a customer to the plan, and provide additional attributes
|
18
|
+
customer = Pay::Customer.find(1234)
|
19
|
+
customer.subscribe(name: "My Subscription", plan: "my-plan", status: "active", billing_time: "calendar")
|
20
|
+
```
|
21
|
+
|
22
|
+
See [Lago Subscription API](https://docs.getlago.com/api-reference/subscriptions/assign-plan) for valid attributes.
|
23
|
+
|
24
|
+
## Cancelling a Subscription
|
25
|
+
|
26
|
+
Subscriptions can be cancelled using the `cancel` method on Pay::Subscription.
|
27
|
+
|
28
|
+
This will terminate the subscription in Lago, and a credit note will automatically be issued by Lago refunding the customer
|
29
|
+
for their remaining time on the subscription. See [Terminate a subscription](https://docs.getlago.com/guide/subscriptions/terminate-subscription).
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# Cancel a Subscription
|
33
|
+
subscription = Pay::Subscription.find(1234)
|
34
|
+
subscription.cancel
|
35
|
+
```
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# Cancel a Pending Subscription
|
39
|
+
subscription = Pay::Subscription.find(1234)
|
40
|
+
subscription.cancel(status: "pending")
|
41
|
+
```
|
42
|
+
|
43
|
+
## Changing a Subscription's Plan
|
44
|
+
|
45
|
+
You can change the plan of a subscription using the `swap` method on Pay::Subscription.
|
46
|
+
|
47
|
+
Given a plan code, this method will switch the plan over at time of the next invoice.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# Switch a Subscription to "my-second-plan"
|
51
|
+
subscription = Pay::Subscription.find(1234)
|
52
|
+
subscription.swap("my-second-plan")
|
53
|
+
```
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# Switch a Subscription to "my-second-plan", and update some attributes
|
57
|
+
subscription = Pay::Subscription.find(1234)
|
58
|
+
subscription.swap("my-second-plan", name: "My Updated Subscription")
|
59
|
+
```
|
60
|
+
|
61
|
+
See [Lago Subscription API](https://docs.getlago.com/api-reference/subscriptions/assign-plan) for valid attributes.
|
62
|
+
|
63
|
+
## Trials, Pausing, Quantity etc.
|
64
|
+
|
65
|
+
Lago does not implement trial periods, pause/resume, quantities etc.
|
66
|
+
|
67
|
+
Pay will raise Pay::Lago::Error when attempting to use methods that would usually perform these functions.
|
data/docs/4_webhooks.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Webhooks
|
2
|
+
|
3
|
+
In order for Pay to automatically sync charges and subscriptions, Lago must be configured to point to Pay's Lago endpoint.
|
4
|
+
|
5
|
+
## Adding the Webhook to Lago
|
6
|
+
You can add webhooks using the [Lago UI](https://docs.getlago.com/guide/webhooks). Pay's default webhook endpoint for Lago
|
7
|
+
is `/pay/webhooks/lago` (`/pay` is replaced by the mountpoint of the Pay engine). Webhooks should use JWT.
|
8
|
+
|
9
|
+
Alternatively you can use the helper method to add the webhook to Lago.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# Create a webhook endpoint in Lago for the Pay gem.
|
13
|
+
Pay::Lago.create_webhook!("https://mypayinstance.example")
|
14
|
+
```
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# Create a webhook endpoint, where you have mounted Pay at a different path (eg. "admin/pay").
|
18
|
+
# Not necessary unless you have disabled "automount_routes".
|
19
|
+
Pay::Lago.create_webhook!("https://mypayinstance.example", "/admin/pay")
|
20
|
+
```
|
21
|
+
|
22
|
+
Please note: the given string must be a valid HTTP/S URL. Pay will automatically set the path of the URL to be the correct path,
|
23
|
+
so you only need to provide the root URL.
|
24
|
+
|
25
|
+
## List of Events Pay Implements
|
26
|
+
|
27
|
+
Pay doesn't listen to every event, because not all are relevant to the operation of the gem. You can find a full list
|
28
|
+
of events [here](https://docs.getlago.com/api-reference/webhooks/messages).
|
29
|
+
|
30
|
+
### Customer Payment Provider Created
|
31
|
+
`customer.payment_provider_created`
|
32
|
+
|
33
|
+
When this event is triggered, Pay updates the respective Pay::Customer with the new payment provider information.
|
34
|
+
|
35
|
+
### Invoice Created
|
36
|
+
`invoice.created`
|
37
|
+
|
38
|
+
When this event is triggered, Pay creates or updates a Pay::Charge for the given Invoice, and sends a reciept if so configured.
|
39
|
+
|
40
|
+
### Invoice Drafted
|
41
|
+
`invoice.drafted`
|
42
|
+
|
43
|
+
When this event is triggered, Pay creates or updates a Pay::Charge for the given Invoice, but doesn't send a reciept.
|
44
|
+
|
45
|
+
### Invoice One Off Created
|
46
|
+
`invoice.one_off_created`
|
47
|
+
|
48
|
+
Currently handles the same as [Invoice Created](#invoice-created).
|
49
|
+
|
50
|
+
### Invoice Payment Status Updated
|
51
|
+
`invoice.payment_status_updated`
|
52
|
+
|
53
|
+
When this event is triggered, it will update the payment status in the corresponding charge's data.
|
54
|
+
|
55
|
+
Does nothing if no matching charge is found.
|
56
|
+
|
57
|
+
### Subscription Started
|
58
|
+
`subscription.started`
|
59
|
+
|
60
|
+
Creates or updates a Pay::Subscription with the recieved information.
|
61
|
+
|
62
|
+
### Subscription Terminated
|
63
|
+
`subscription.terminated`
|
64
|
+
|
65
|
+
Currently handles the same as [Subscription Started](#subscription-started). However, the information provided by Lago will
|
66
|
+
cause the subscription to be considered terminated.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pay
|
2
|
+
module Lago
|
3
|
+
module Attributes
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module CustomerExtension
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
cattr_accessor :pay_lago_customer_attributes
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def pay_customer(options = {})
|
16
|
+
include CustomerExtension
|
17
|
+
|
18
|
+
self.pay_lago_customer_attributes = options[:lago_attributes]
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Pay
|
2
|
+
module Lago
|
3
|
+
class Billable
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
attr_reader :pay_customer
|
7
|
+
|
8
|
+
delegate :processor_id,
|
9
|
+
:processor_id?,
|
10
|
+
:email,
|
11
|
+
:customer_name,
|
12
|
+
:card_token,
|
13
|
+
to: :pay_customer
|
14
|
+
|
15
|
+
def initialize(pay_customer)
|
16
|
+
@pay_customer = pay_customer
|
17
|
+
end
|
18
|
+
|
19
|
+
def pay_external_id
|
20
|
+
processor_id || pay_customer.to_gid.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a hash of attributes for the Lago::Customer object
|
24
|
+
def customer_attributes(external_id)
|
25
|
+
owner = pay_customer.owner
|
26
|
+
|
27
|
+
attributes = case owner.class.pay_lago_customer_attributes
|
28
|
+
when Symbol
|
29
|
+
owner.send(owner.class.pay_lago_customer_attributes, pay_customer)
|
30
|
+
when Proc
|
31
|
+
owner.class.pay_lago_customer_attributes.call(pay_customer)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Guard against attributes being returned nil
|
35
|
+
attributes ||= {}
|
36
|
+
|
37
|
+
{external_id: external_id, email: email, name: customer_name}.merge(attributes)
|
38
|
+
end
|
39
|
+
|
40
|
+
def customer
|
41
|
+
begin
|
42
|
+
lago_customer = Lago.client.customers.get(uri_escape(pay_external_id))
|
43
|
+
rescue ::Lago::Api::HttpError
|
44
|
+
lago_customer = Lago.client.customers.create(customer_attributes(pay_external_id))
|
45
|
+
end
|
46
|
+
pay_customer.update!(processor_id: lago_customer.external_id) unless processor_id == lago_customer.external_id
|
47
|
+
lago_customer
|
48
|
+
rescue ::Lago::Api::HttpError => e
|
49
|
+
raise Pay::Lago::Error, e
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_customer!(**attributes)
|
53
|
+
new_attributes = Lago.openstruct_to_h(customer).except(:lago_id)
|
54
|
+
Lago.client.customers.create(
|
55
|
+
new_attributes.merge(attributes.except(:external_id))
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def charge(amount = nil, addon: nil, options: {})
|
60
|
+
lago_customer = customer
|
61
|
+
lago_addon = addon.is_a?(String) ? Lago.client.add_ons.get(addon) : pay_default_addon
|
62
|
+
amount ||= lago_addon.amount_cents
|
63
|
+
|
64
|
+
attributes = {
|
65
|
+
external_customer_id: processor_id,
|
66
|
+
currency: options[:currency] || lago_customer.currency,
|
67
|
+
fees: [
|
68
|
+
{
|
69
|
+
add_on_code: lago_addon.code,
|
70
|
+
unit_amount_cents: amount
|
71
|
+
}.merge(options.except(:amount_cents, :add_on_code, :currency))
|
72
|
+
]
|
73
|
+
}
|
74
|
+
|
75
|
+
invoice = Lago.client.invoices.create(attributes)
|
76
|
+
Pay::Lago::Charge.sync(invoice.lago_id, object: invoice)
|
77
|
+
rescue ::Lago::Api::HttpError => e
|
78
|
+
raise Pay::Lago::Error, e
|
79
|
+
end
|
80
|
+
|
81
|
+
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
82
|
+
# Make to generate a processor_id
|
83
|
+
lago_customer = customer
|
84
|
+
pay_subscription = create_placeholder_subscription(name, plan)
|
85
|
+
external_id = pay_subscription.to_gid.to_s
|
86
|
+
|
87
|
+
attributes = options.merge(
|
88
|
+
external_customer_id: lago_customer.external_id,
|
89
|
+
name: name, external_id: external_id, plan_code: plan
|
90
|
+
)
|
91
|
+
|
92
|
+
begin
|
93
|
+
subscription = Lago.client.subscriptions.create(attributes)
|
94
|
+
rescue ::Lago::Api::HttpError
|
95
|
+
pay_subscription.destroy!
|
96
|
+
raise Pay::Lago::Error, e
|
97
|
+
end
|
98
|
+
|
99
|
+
pay_subscription.update!(processor_id: external_id)
|
100
|
+
Pay::Lago::Subscription.sync(lago_customer.external_id, external_id, object: subscription)
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_payment_method(_token = nil, default: true)
|
104
|
+
Pay::Lago::PaymentMethod.sync(pay_customer: pay_customer)
|
105
|
+
end
|
106
|
+
|
107
|
+
def processor_subscription(subscription_id, options = {})
|
108
|
+
Pay::Lago::Subscription.get_subscription(processor_id, subscription_id)
|
109
|
+
end
|
110
|
+
|
111
|
+
def trial_end_date(subscription)
|
112
|
+
raise Pay::Lago::Error.new("Lago subscriptions do not implement trials.")
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def create_placeholder_subscription(name, plan)
|
118
|
+
pay_customer.subscriptions.create!(
|
119
|
+
processor_id: NanoId.generate,
|
120
|
+
name: name,
|
121
|
+
processor_plan: plan,
|
122
|
+
quantity: 0,
|
123
|
+
status: "incomplete"
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def pay_default_addon
|
128
|
+
begin
|
129
|
+
Lago.client.add_ons.get("pay_default_addon")
|
130
|
+
rescue ::Lago::Api::HttpError
|
131
|
+
Lago.client.add_ons.create(
|
132
|
+
name: "Default Charge",
|
133
|
+
code: "pay_default_addon",
|
134
|
+
amount_cents: 100,
|
135
|
+
amount_currency: "USD"
|
136
|
+
)
|
137
|
+
end
|
138
|
+
rescue ::Lago::Api::HttpError => e
|
139
|
+
raise Pay::Lago::Error, e
|
140
|
+
end
|
141
|
+
|
142
|
+
def uri_escape(uri)
|
143
|
+
return "" unless uri.is_a? String
|
144
|
+
URI.encode_www_form_component uri
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Pay
|
2
|
+
module Lago
|
3
|
+
class Charge
|
4
|
+
attr_reader :pay_charge
|
5
|
+
|
6
|
+
delegate :processor_id, :owner, to: :pay_charge
|
7
|
+
|
8
|
+
def self.sync(charge_id, object: nil, try: 0, retries: 1)
|
9
|
+
object ||= Lago.client.invoices.get(charge_id)
|
10
|
+
|
11
|
+
pay_customer = Pay::Customer.find_by(processor: :lago, processor_id: object.customer.external_id)
|
12
|
+
if pay_customer.blank?
|
13
|
+
Rails.logger.debug "Pay::Customer #{object.customer} is not in the database while syncing Lago Invoice #{object.lago_id}"
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
attrs = {
|
18
|
+
processor_id: object.lago_id,
|
19
|
+
amount: object.total_amount_cents,
|
20
|
+
data: Lago.openstruct_to_h(object)
|
21
|
+
}
|
22
|
+
|
23
|
+
if object.subscriptions.present?
|
24
|
+
subscription = object.subscriptions.first
|
25
|
+
attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: subscription.try(:lago_id))
|
26
|
+
end
|
27
|
+
|
28
|
+
# Update or create the charge
|
29
|
+
if (pay_charge = pay_customer.charges.find_by(processor_id: charge_id))
|
30
|
+
pay_charge.with_lock do
|
31
|
+
pay_charge.update!(attrs)
|
32
|
+
end
|
33
|
+
pay_charge
|
34
|
+
else
|
35
|
+
pay_customer.charges.create!(attrs)
|
36
|
+
end
|
37
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
38
|
+
try += 1
|
39
|
+
if try <= retries
|
40
|
+
sleep 0.1
|
41
|
+
retry
|
42
|
+
else
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(pay_charge)
|
48
|
+
@pay_charge = pay_charge
|
49
|
+
end
|
50
|
+
|
51
|
+
def charge
|
52
|
+
Lago.client.invoices.get(processor_id)
|
53
|
+
rescue ::Lago::Api::HttpError => e
|
54
|
+
raise Pay::Lago::Error, e
|
55
|
+
end
|
56
|
+
|
57
|
+
def update_charge!(**attributes)
|
58
|
+
Lago.client.invoices.update({metadata: []}.merge(attributes), charge.lago_id)
|
59
|
+
rescue ::Lago::Api::HttpError => e
|
60
|
+
raise Pay::Lago::Error, e
|
61
|
+
end
|
62
|
+
|
63
|
+
def refund!(amount_to_refund, **options)
|
64
|
+
attributes = {
|
65
|
+
refund_amount_cents: amount_to_refund,
|
66
|
+
invoice_id: processor_id,
|
67
|
+
items: [{fee_id: charge.fees.first.try(:lago_id), amount_cents: amount_to_refund}]
|
68
|
+
}
|
69
|
+
begin
|
70
|
+
Lago.client.credit_notes.create(options.merge(attributes))
|
71
|
+
rescue ::Lago::Api::HttpError => e
|
72
|
+
if e.error_code == 403
|
73
|
+
raise Pay::Lago::Error.new("Creating a credit note requires Lago Premium.")
|
74
|
+
else
|
75
|
+
raise Pay::Lago::Error, e
|
76
|
+
end
|
77
|
+
end
|
78
|
+
pay_charge.update!(amount_refunded: pay_charge.amount_refunded.to_i + amount_to_refund)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pay
|
2
|
+
module Lago
|
3
|
+
class Engine < ::Rails::Engine
|
4
|
+
engine_name "pay_lago"
|
5
|
+
|
6
|
+
initializer "pay_lago.processors" do |app|
|
7
|
+
ActiveSupport.on_load(:active_record) do
|
8
|
+
include Pay::Lago::Attributes
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
config.before_initialize do
|
13
|
+
Pay::Lago.configure_webhooks if Pay::Lago.enabled?
|
14
|
+
end
|
15
|
+
|
16
|
+
config.to_prepare do
|
17
|
+
# Include Concerns
|
18
|
+
Pay::Customer.include Pay::Lago::PayCustomerExtensions
|
19
|
+
Pay::Charge.include Pay::Lago::PayExtensions
|
20
|
+
Pay::Subscription.include Pay::Lago::PayExtensions
|
21
|
+
|
22
|
+
# Prepend Pay::Lago extensions
|
23
|
+
Pay::Webhook.prepend Pay::Lago::WebhookExtensions
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Pay
|
2
|
+
module Lago
|
3
|
+
class PaymentMethod
|
4
|
+
attr_reader :pay_payment_method
|
5
|
+
|
6
|
+
delegate :customer, :processor_id, to: :pay_payment_method
|
7
|
+
|
8
|
+
# Lago doesn't provide PaymentMethod IDs, so we have to lookup via the Customer
|
9
|
+
def self.sync(pay_customer:)
|
10
|
+
payment_method = pay_customer.default_payment_method || pay_customer.build_default_payment_method
|
11
|
+
lago_customer = pay_customer.customer
|
12
|
+
payment_data = lago_customer.billing_configuration
|
13
|
+
|
14
|
+
payment_method_attr = {
|
15
|
+
processor_id: payment_data.provider_customer_id || NanoId.generate,
|
16
|
+
type: "card",
|
17
|
+
data: Lago.openstruct_to_h(payment_data)
|
18
|
+
}
|
19
|
+
|
20
|
+
payment_method.update!(payment_method_attr)
|
21
|
+
payment_method
|
22
|
+
rescue ::Lago::Api::HttpError => e
|
23
|
+
raise Pay::Lago::Error, e
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(pay_payment_method)
|
27
|
+
@pay_payment_method = pay_payment_method
|
28
|
+
end
|
29
|
+
|
30
|
+
def make_default!
|
31
|
+
end
|
32
|
+
|
33
|
+
def detach
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|