pay-lago 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +27 -0
  3. data/.gitignore +28 -0
  4. data/.rspec +3 -0
  5. data/.standard.yml +3 -0
  6. data/CHANGELOG.md +2 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +25 -0
  9. data/Gemfile.lock +292 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +85 -0
  12. data/Rakefile +32 -0
  13. data/app/controllers/pay/webhooks/lago_controller.rb +41 -0
  14. data/app/models/concerns/pay/lago/pay_customer_extensions.rb +15 -0
  15. data/app/models/concerns/pay/lago/pay_extensions.rb +15 -0
  16. data/bin/console +15 -0
  17. data/bin/rails +16 -0
  18. data/bin/setup +8 -0
  19. data/config/routes.rb +5 -0
  20. data/docs/1_customers.md +18 -0
  21. data/docs/2_charges.md +72 -0
  22. data/docs/3_subscriptions.md +67 -0
  23. data/docs/4_webhooks.md +66 -0
  24. data/lib/pay/lago/attributes.rb +24 -0
  25. data/lib/pay/lago/billable.rb +148 -0
  26. data/lib/pay/lago/charge.rb +82 -0
  27. data/lib/pay/lago/engine.rb +27 -0
  28. data/lib/pay/lago/error.rb +9 -0
  29. data/lib/pay/lago/payment_method.rb +37 -0
  30. data/lib/pay/lago/subscription.rb +134 -0
  31. data/lib/pay/lago/version.rb +7 -0
  32. data/lib/pay/lago/webhook_extensions.rb +10 -0
  33. data/lib/pay/lago/webhooks/customer_payment_provider_created.rb +18 -0
  34. data/lib/pay/lago/webhooks/invoice_created.rb +15 -0
  35. data/lib/pay/lago/webhooks/invoice_drafted.rb +11 -0
  36. data/lib/pay/lago/webhooks/invoice_one_off_created.rb +15 -0
  37. data/lib/pay/lago/webhooks/invoice_payment_status_updated.rb +14 -0
  38. data/lib/pay/lago/webhooks/subscription_started.rb +12 -0
  39. data/lib/pay/lago/webhooks/subscription_terminated.rb +12 -0
  40. data/lib/pay/lago.rb +91 -0
  41. data/pay-lago.gemspec +27 -0
  42. data/sig/pay/lago.rbs +6 -0
  43. data/test/controllers/pay/webhooks/lago_controller_test.rb +37 -0
  44. data/test/dummy/.browserslistrc +1 -0
  45. data/test/dummy/Rakefile +6 -0
  46. data/test/dummy/app/assets/config/manifest.js +4 -0
  47. data/test/dummy/app/assets/images/.keep +0 -0
  48. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  49. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  50. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  51. data/test/dummy/app/controllers/application_controller.rb +4 -0
  52. data/test/dummy/app/controllers/concerns/.keep +0 -0
  53. data/test/dummy/app/controllers/main_controller.rb +4 -0
  54. data/test/dummy/app/controllers/payment_methods_controller.rb +5 -0
  55. data/test/dummy/app/helpers/application_helper.rb +2 -0
  56. data/test/dummy/app/helpers/current_helper.rb +9 -0
  57. data/test/dummy/app/javascript/application.js +9 -0
  58. data/test/dummy/app/javascript/controllers/application.js +10 -0
  59. data/test/dummy/app/javascript/controllers/index.js +11 -0
  60. data/test/dummy/app/jobs/application_job.rb +2 -0
  61. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  62. data/test/dummy/app/models/account.rb +3 -0
  63. data/test/dummy/app/models/application_record.rb +3 -0
  64. data/test/dummy/app/models/concerns/.keep +0 -0
  65. data/test/dummy/app/models/team.rb +10 -0
  66. data/test/dummy/app/models/user.rb +3 -0
  67. data/test/dummy/app/views/layouts/application.html.erb +36 -0
  68. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  69. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  70. data/test/dummy/app/webhooks/charge_succeeded.rb +5 -0
  71. data/test/dummy/bin/importmap +4 -0
  72. data/test/dummy/bin/rails +4 -0
  73. data/test/dummy/bin/rake +4 -0
  74. data/test/dummy/bin/setup +33 -0
  75. data/test/dummy/bin/webpack +18 -0
  76. data/test/dummy/bin/webpack-dev-server +18 -0
  77. data/test/dummy/config/application.rb +25 -0
  78. data/test/dummy/config/boot.rb +6 -0
  79. data/test/dummy/config/cable.yml +9 -0
  80. data/test/dummy/config/database.yml +3 -0
  81. data/test/dummy/config/environment.rb +5 -0
  82. data/test/dummy/config/environments/development.rb +70 -0
  83. data/test/dummy/config/environments/production.rb +86 -0
  84. data/test/dummy/config/environments/test.rb +60 -0
  85. data/test/dummy/config/importmap.rb +8 -0
  86. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  87. data/test/dummy/config/initializers/assets.rb +11 -0
  88. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  89. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  90. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  91. data/test/dummy/config/initializers/inflections.rb +16 -0
  92. data/test/dummy/config/initializers/mime_types.rb +4 -0
  93. data/test/dummy/config/initializers/pay.rb +11 -0
  94. data/test/dummy/config/initializers/session_store.rb +3 -0
  95. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  96. data/test/dummy/config/locales/en.yml +23 -0
  97. data/test/dummy/config/puma.rb +47 -0
  98. data/test/dummy/config/routes.rb +4 -0
  99. data/test/dummy/config/secrets.yml +22 -0
  100. data/test/dummy/config/spring.rb +6 -0
  101. data/test/dummy/config/storage.yml +34 -0
  102. data/test/dummy/config.ru +6 -0
  103. data/test/dummy/db/migrate/20170205000000_create_users.rb +22 -0
  104. data/test/dummy/db/schema.rb +125 -0
  105. data/test/dummy/lib/assets/.keep +0 -0
  106. data/test/dummy/log/.keep +0 -0
  107. data/test/dummy/public/404.html +67 -0
  108. data/test/dummy/public/422.html +67 -0
  109. data/test/dummy/public/500.html +66 -0
  110. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  111. data/test/dummy/public/apple-touch-icon.png +0 -0
  112. data/test/dummy/public/favicon.ico +0 -0
  113. data/test/dummy/vendor/javascript/.keep +0 -0
  114. data/test/fixtures/accounts.yml +2 -0
  115. data/test/fixtures/pay/customers.yml +7 -0
  116. data/test/fixtures/teams.yml +4 -0
  117. data/test/fixtures/users.yml +11 -0
  118. data/test/pay/lago/billable_test.rb +42 -0
  119. data/test/pay/lago/charge_test.rb +23 -0
  120. data/test/pay/lago/subscription_test.rb +48 -0
  121. data/test/support/fixtures/lago/invoice.one_off_created.json +120 -0
  122. data/test/support/vcr.rb +28 -0
  123. data/test/test_helper.rb +66 -0
  124. data/test/vcr_cassettes/test_add_payment_method_to_lago.yml +56 -0
  125. data/test/vcr_cassettes/test_can_get_lago_processor_customer.yml +56 -0
  126. data/test/vcr_cassettes/test_can_make_a_lago_processor_charge.yml +163 -0
  127. data/test/vcr_cassettes/test_generates_lago_processor_id.yml +107 -0
  128. data/test/vcr_cassettes/test_lago_cannot_change_quantity.yml +108 -0
  129. data/test/vcr_cassettes/test_lago_processor_cancel.yml +212 -0
  130. data/test/vcr_cassettes/test_lago_processor_charge.yml +325 -0
  131. data/test/vcr_cassettes/test_lago_processor_on_grace_period__is_always_false.yml +108 -0
  132. data/test/vcr_cassettes/test_lago_processor_pause_raises_an_error.yml +108 -0
  133. data/test/vcr_cassettes/test_lago_processor_paused__is_always_false.yml +108 -0
  134. data/test/vcr_cassettes/test_lago_processor_refund_with_premium.yml +378 -0
  135. data/test/vcr_cassettes/test_lago_processor_refund_without_premium.yml +375 -0
  136. data/test/vcr_cassettes/test_lago_processor_resume_raises_an_error.yml +108 -0
  137. data/test/vcr_cassettes/test_lago_processor_subscribe.yml +108 -0
  138. data/test/vcr_cassettes/test_lago_processor_subscription.yml +160 -0
  139. data/test/vcr_cassettes/test_lago_processor_swap.yml +212 -0
  140. 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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Pay::Engine.routes.draw do
4
+ post "webhooks/lago", to: "pay/webhooks/lago#create" if Pay::Lago.enabled?
5
+ end
@@ -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.
@@ -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,9 @@
1
+ module Pay
2
+ module Lago
3
+ class Error < Pay::Error
4
+ def message
5
+ cause.try(:message) || to_s
6
+ end
7
+ end
8
+ end
9
+ 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