pay 7.1.0 → 7.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 731897282e09b738950bdef4b213ef808bc39cae0bbc84c43840de743e9611f4
4
- data.tar.gz: 44624d892463cf3e40206fba9c812005ff4e8fab13642ed932d966c3ec02dafb
3
+ metadata.gz: b50cc63cfa61beffbc725d9f6c623c469a6357e0808ff06d3e814c4aa4e19955
4
+ data.tar.gz: ae87dbfcd24862baa2f156cb2b1b94fe836a0d113853b562cbe59835e59569c5
5
5
  SHA512:
6
- metadata.gz: d4d24fca5a2619c32d5b0a6e45b2ca5ca20654719bca47bcc377799716e4aeb54a2059fe05ea522db24394ce366c6181fb0ba4051065d1a0039f48236ff48dca
7
- data.tar.gz: 5131748acaed2da28332e6d853b66b086c1354eafc7880669ae291cec664bbed35dfa194e7ddcb4060097e42fc18f5c0e2e5f69cf1a4fe7c40501926f2b208ea
6
+ metadata.gz: 459524ff5b772a08c1ee1d994088b71f0e9a854f4b1afa76ecdb60fa8229f43ea72ed327a6dbf55d497478d033f99f1d19c4ed978c69931e27ad9ddc99fd57af
7
+ data.tar.gz: 200898e080a94d76520e0a85088d43691f64a27da3d94ac4fa74a9f14db4f6a5c1bdc16992422b3f4c4ab9b63bb35344e099799984cca0beb4cff8a6dc6e8512
data/README.md CHANGED
@@ -44,7 +44,7 @@ Want to add a new payment provider? Contributions are welcome.
44
44
  * **Payment Processors**
45
45
  * [Stripe](docs/stripe/1_overview.md)
46
46
  * [Braintree](docs/braintree/1_overview.md)
47
- * [Paddle](docs/paddle/1_overview.md)
47
+ * [Paddle](docs/paddle_billing/1_overview.md)
48
48
  * [Fake Processor](docs/fake_processor/1_overview.md)
49
49
  * **Marketplaces**
50
50
  * [Stripe Connect](docs/marketplaces/stripe_connect.md)
@@ -55,6 +55,10 @@ Want to add a new payment provider? Contributions are welcome.
55
55
 
56
56
  If you have an issue you'd like to submit, please do so using the issue tracker in GitHub. In order for us to help you in the best way possible, please be as detailed as you can.
57
57
 
58
+ For those using devcontainers, if you want to test the application with different databases:
59
+ 1. Uncomment the `DATABASE_URL` corresponding to the database type you wish to use in the `.devcontainer/devcontainer.json` file.
60
+ 2. Rebuild the devcontainer, which will configure the application to use the selected database for your development environment.
61
+
58
62
  If you'd like to open a PR please make sure the following things pass:
59
63
 
60
64
  ```ruby
@@ -45,7 +45,7 @@ module Pay
45
45
 
46
46
  # Helpers for payment processors
47
47
  %w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
48
- define_method "#{processor_name}?" do
48
+ define_method :"#{processor_name}?" do
49
49
  customer.processor == processor_name
50
50
  end
51
51
 
@@ -28,7 +28,7 @@ module Pay
28
28
  %w[stripe braintree paddle_billing paddle_classic fake_processor].each do |processor_name|
29
29
  scope processor_name, -> { where(processor: processor_name) }
30
30
 
31
- define_method "#{processor_name}?" do
31
+ define_method :"#{processor_name}?" do
32
32
  processor == processor_name
33
33
  end
34
34
  end
@@ -9,11 +9,11 @@ module Pay
9
9
 
10
10
  # Scopes
11
11
  scope :for_name, ->(name) { where(name: name) }
12
- scope :on_trial, -> { where("trial_ends_at > ?", Time.current) }
12
+ scope :on_trial, -> { where(status: ["trialing", "active"]).where("trial_ends_at > ?", Time.current) }
13
13
  scope :canceled, -> { where.not(ends_at: nil) }
14
14
  scope :cancelled, -> { canceled }
15
15
  scope :on_grace_period, -> { where("#{table_name}.ends_at IS NOT NULL AND #{table_name}.ends_at > ?", Time.current) }
16
- scope :active, -> { where(status: ["trialing", "active"]).pause_not_started.where("#{table_name}.ends_at IS NULL OR #{table_name}.ends_at > ?", Time.current).where("trial_ends_at IS NULL OR trial_ends_at > ?", Time.current) }
16
+ scope :active, -> { where(status: "active").pause_not_started.where("#{table_name}.ends_at IS NULL OR #{table_name}.ends_at > ?", Time.current).or(on_trial) }
17
17
  scope :paused, -> { where(status: "paused").or(where("pause_starts_at <= ?", Time.current)) }
18
18
  scope :pause_not_started, -> { where("pause_starts_at IS NULL OR pause_starts_at > ?", Time.current) }
19
19
  scope :active_or_paused, -> { active.or(paused) }
@@ -44,7 +44,7 @@ module Pay
44
44
 
45
45
  # Helper methods for payment processors
46
46
  %w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
47
- define_method "#{processor_name}?" do
47
+ define_method :"#{processor_name}?" do
48
48
  customer.processor == processor_name
49
49
  end
50
50
 
@@ -265,7 +265,7 @@ module Pay
265
265
  end
266
266
 
267
267
  # Retrieve payment method details from transaction
268
- payment_method = transaction.send("#{attribute_name}_details")
268
+ payment_method = transaction.send(:"#{attribute_name}_details")
269
269
 
270
270
  {
271
271
  payment_method_type: :card,
@@ -28,7 +28,8 @@ module Pay
28
28
  # Make to generate a processor_id
29
29
  customer
30
30
 
31
- attributes = options.merge(
31
+ valid_attributes = options.slice(*Pay::Charge.attribute_names.map(&:to_sym))
32
+ attributes = {
32
33
  processor_id: NanoId.generate,
33
34
  amount: amount,
34
35
  data: {
@@ -38,7 +39,7 @@ module Pay
38
39
  exp_month: Date.today.month,
39
40
  exp_year: Date.today.year
40
41
  }
41
- )
42
+ }.deep_merge(valid_attributes)
42
43
  pay_customer.charges.create!(attributes)
43
44
  end
44
45
 
@@ -37,8 +37,8 @@ module Pay
37
37
  subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
38
38
  }
39
39
 
40
- if object.payment
41
- case object.payment.method_details.type.downcase
40
+ if (details = Array.wrap(object.payments).first&.method_details)
41
+ case details.type.downcase
42
42
  when "card"
43
43
  attrs[:payment_method_type] = "card"
44
44
  attrs[:brand] = details.card.type
@@ -5,6 +5,13 @@ module Pay
5
5
 
6
6
  delegate :customer, :processor_id, to: :pay_payment_method
7
7
 
8
+ def self.sync_from_transaction(pay_customer:, transaction:)
9
+ transaction = ::Paddle::Transaction.retrieve(id: transaction)
10
+ return unless transaction.status == "completed"
11
+ return if transaction.payments.empty?
12
+ sync(pay_customer: pay_customer, attributes: transaction.payments.first)
13
+ end
14
+
8
15
  def self.sync(pay_customer:, attributes:)
9
16
  details = attributes.method_details
10
17
  attrs = {
@@ -19,7 +26,7 @@ module Pay
19
26
  attrs[:exp_year] = details.card.expiry_year
20
27
  end
21
28
 
22
- payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.stored_payment_method_id)
29
+ payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
23
30
  payment_method.update!(attrs)
24
31
  payment_method
25
32
  end
@@ -56,9 +56,13 @@ module Pay
56
56
  # Remove payment methods since customer cannot be reused after cancelling
57
57
  Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
58
58
  when "trialing"
59
- attributes[:trial_ends_at] = Time.parse(object.next_billed_at)
59
+ attributes[:trial_ends_at] = Time.parse(object.next_billed_at) if object.next_billed_at
60
60
  when "paused"
61
- attributes[:pause_starts_at] = Time.parse(object.paused_at)
61
+ attributes[:pause_starts_at] = Time.parse(object.paused_at) if object.paused_at
62
+ when "active", "past_due"
63
+ attributes[:trial_ends_at] = nil
64
+ attributes[:pause_starts_at] = nil
65
+ attributes[:ends_at] = nil
62
66
  end
63
67
 
64
68
  case object.scheduled_change&.action
@@ -105,7 +109,7 @@ module Pay
105
109
  )
106
110
  pay_subscription.update(
107
111
  status: response.status,
108
- ends_at: response.scheduled_change.effective_at
112
+ ends_at: response.scheduled_change&.effective_at || Time.current
109
113
  )
110
114
  rescue ::Paddle::Error => e
111
115
  raise Pay::PaddleBilling::Error, e
@@ -44,12 +44,15 @@ module Pay
44
44
  status: object.state || object.status
45
45
  }
46
46
 
47
- # If paused or delete while on trial, set ends_at to match
48
47
  case attributes[:status]
49
48
  when "trialing"
50
49
  attributes[:trial_ends_at] = Time.zone.parse(object.next_bill_date)
51
50
  attributes[:ends_at] = nil
51
+ when "active", "past_due"
52
+ attributes[:trial_ends_at] = nil
53
+ attributes[:ends_at] = nil
52
54
  when "paused", "deleted"
55
+ # If paused or delete while on trial, set ends_at to match
53
56
  attributes[:trial_ends_at] = nil
54
57
  attributes[:ends_at] = Time.zone.parse(object.next_bill_date)
55
58
  end
@@ -29,7 +29,7 @@ module Pay
29
29
  refunds = []
30
30
  object.refunds.auto_paging_each { |refund| refunds << refund }
31
31
 
32
- payment_method = object.payment_method_details.send(object.payment_method_details.type)
32
+ payment_method = object.payment_method_details.try(object.payment_method_details.type)
33
33
  attrs = {
34
34
  amount: object.amount,
35
35
  amount_captured: object.amount_captured,
@@ -60,7 +60,7 @@ module Pay
60
60
 
61
61
  # Extracts payment method details from a Stripe::PaymentMethod object
62
62
  def self.extract_attributes(payment_method)
63
- details = payment_method.send(payment_method.type)
63
+ details = payment_method.try(payment_method.type)
64
64
 
65
65
  {
66
66
  payment_method_type: payment_method.type,
@@ -207,7 +207,7 @@ module Pay
207
207
  # cancel_now!(prorate: true)
208
208
  # cancel_now!(invoice_now: true)
209
209
  def cancel_now!(**options)
210
- return if canceled?
210
+ return if canceled? && ends_at.past?
211
211
 
212
212
  @stripe_subscription = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
213
213
  pay_subscription.update(ends_at: Time.current, status: :canceled)
@@ -342,13 +342,13 @@ module Pay
342
342
  # create_usage_record(quantity: 4, action: :increment)
343
343
  # create_usage_record(subscription_item_id: "si_1234", quantity: 100, action: :set)
344
344
  def create_usage_record(**options)
345
- subscription_item_id = options.fetch(:subscription_item_id, metered_subscription_item&.dig("id"))
345
+ subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
346
346
  ::Stripe::SubscriptionItem.create_usage_record(subscription_item_id, options, stripe_options)
347
347
  end
348
348
 
349
349
  # Returns usage record summaries for a subscription item
350
350
  def usage_record_summaries(**options)
351
- subscription_item_id = options.fetch(:subscription_item_id, metered_subscription_item&.dig("id"))
351
+ subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
352
352
  ::Stripe::SubscriptionItem.list_usage_record_summaries(subscription_item_id, options, stripe_options)
353
353
  end
354
354
 
data/lib/pay/stripe.rb CHANGED
@@ -30,7 +30,7 @@ module Pay
30
30
 
31
31
  extend Env
32
32
 
33
- REQUIRED_VERSION = "~> 10"
33
+ REQUIRED_VERSION = "~> 11"
34
34
 
35
35
  # A list of database model names that include Pay
36
36
  # Used for safely looking up models with client_reference_id
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "7.1.0"
2
+ VERSION = "7.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.0
4
+ version: 7.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-12-06 00:00:00.000000000 Z
13
+ date: 2024-05-16 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -104,14 +104,6 @@ files:
104
104
  - lib/pay/fake_processor/merchant.rb
105
105
  - lib/pay/fake_processor/payment_method.rb
106
106
  - lib/pay/fake_processor/subscription.rb
107
- - lib/pay/lemon_squeezy.rb
108
- - lib/pay/lemon_squeezy/billable.rb
109
- - lib/pay/lemon_squeezy/charge.rb
110
- - lib/pay/lemon_squeezy/error.rb
111
- - lib/pay/lemon_squeezy/payment_method.rb
112
- - lib/pay/lemon_squeezy/subscription.rb
113
- - lib/pay/lemon_squeezy/webhooks/subscription.rb
114
- - lib/pay/lemon_squeezy/webhooks/transaction_completed.rb
115
107
  - lib/pay/nano_id.rb
116
108
  - lib/pay/paddle_billing.rb
117
109
  - lib/pay/paddle_billing/billable.rb
@@ -184,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
176
  - !ruby/object:Gem::Version
185
177
  version: '0'
186
178
  requirements: []
187
- rubygems_version: 3.4.22
179
+ rubygems_version: 3.5.6
188
180
  signing_key:
189
181
  specification_version: 4
190
182
  summary: Payments engine for Ruby on Rails
@@ -1,90 +0,0 @@
1
- module Pay
2
- module PaddleBilling
3
- class Billable
4
- attr_reader :pay_customer
5
-
6
- delegate :processor_id,
7
- :processor_id?,
8
- :email,
9
- :customer_name,
10
- :card_token,
11
- to: :pay_customer
12
-
13
- def initialize(pay_customer)
14
- @pay_customer = pay_customer
15
- end
16
-
17
- def customer_attributes
18
- {email: email, name: customer_name}
19
- end
20
-
21
- # Retrieves a Paddle::Customer object
22
- #
23
- # Finds an existing Paddle::Customer if processor_id exists
24
- # Creates a new Paddle::Customer using `email` and `customer_name` if empty processor_id
25
- #
26
- # Returns a Paddle::Customer object
27
- def customer
28
- if processor_id?
29
- ::Paddle::Customer.retrieve(id: processor_id)
30
- else
31
- sc = ::Paddle::Customer.create(email: email, name: customer_name)
32
- pay_customer.update!(processor_id: sc.id)
33
- sc
34
- end
35
- rescue ::Paddle::Error => e
36
- raise Pay::PaddleBilling::Error, e
37
- end
38
-
39
- # Syncs name and email to Paddle::Customer
40
- # You can also pass in other attributes that will be merged into the default attributes
41
- def update_customer!(**attributes)
42
- customer unless processor_id?
43
- attrs = customer_attributes.merge(attributes)
44
- ::Paddle::Customer.update(id: processor_id, **attrs)
45
- end
46
-
47
- def charge(amount, options = {})
48
- return Pay::Error unless options
49
-
50
- items = options[:items]
51
- opts = options.except(:items).merge(customer_id: processor_id)
52
- transaction = ::Paddle::Transaction.create(items: items, **opts)
53
-
54
- attrs = {
55
- amount: transaction.details.totals.grand_total,
56
- created_at: transaction.created_at,
57
- currency: transaction.currency_code,
58
- metadata: transaction.details.line_items&.first&.id
59
- }
60
-
61
- charge = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
62
- charge.update(attrs)
63
- charge
64
- rescue ::Paddle::Error => e
65
- raise Pay::PaddleBilling::Error, e
66
- end
67
-
68
- def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
69
- # pass
70
- end
71
-
72
- # Paddle does not use payment method tokens. The method signature has it here
73
- # to have a uniform API with the other payment processors.
74
- def add_payment_method(token = nil, default: true)
75
- Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer)
76
- end
77
-
78
- def trial_end_date(subscription)
79
- return unless subscription.state == "trialing"
80
- Time.zone.parse(subscription.next_payment[:date]).end_of_day
81
- end
82
-
83
- def processor_subscription(subscription_id, options = {})
84
- ::Paddle::Subscription.retrieve(id: subscription_id, **options)
85
- rescue ::Paddle::Error => e
86
- raise Pay::PaddleBilling::Error, e
87
- end
88
- end
89
- end
90
- end
@@ -1,68 +0,0 @@
1
- module Pay
2
- module PaddleBilling
3
- class Charge
4
- attr_reader :pay_charge
5
-
6
- delegate :processor_id, :customer, to: :pay_charge
7
-
8
- def initialize(pay_charge)
9
- @pay_charge = pay_charge
10
- end
11
-
12
- def self.sync(charge_id, object: nil, try: 0, retries: 1)
13
- # Skip loading the latest charge details from the API if we already have it
14
- object ||= ::Paddle::Transaction.retrieve(id: charge_id)
15
-
16
- # Ignore transactions that aren't completed
17
- return unless object.status == "completed"
18
-
19
- # Ignore charges without a Customer
20
- return if object.customer_id.blank?
21
-
22
- pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
23
- return unless pay_customer
24
-
25
- # Ignore transactions that are payment method changes
26
- # But update the customer's payment method
27
- if object.origin == "subscription_payment_method_change"
28
- Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
29
- return
30
- end
31
-
32
- attrs = {
33
- amount: object.details.totals.grand_total,
34
- created_at: object.created_at,
35
- currency: object.currency_code,
36
- metadata: object.details.line_items&.first&.id,
37
- subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
38
- }
39
-
40
- if object.payment
41
- case object.payment.method_details.type.downcase
42
- when "card"
43
- attrs[:payment_method_type] = "card"
44
- attrs[:brand] = details.card.type
45
- attrs[:exp_month] = details.card.expiry_month
46
- attrs[:exp_year] = details.card.expiry_year
47
- attrs[:last4] = details.card.last4
48
- when "paypal"
49
- attrs[:payment_method_type] = "paypal"
50
- end
51
-
52
- # Update customer's payment method
53
- Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
54
- end
55
-
56
- # Update or create the charge
57
- if (pay_charge = pay_customer.charges.find_by(processor_id: object.id))
58
- pay_charge.with_lock do
59
- pay_charge.update!(attrs)
60
- end
61
- pay_charge
62
- else
63
- pay_customer.charges.create!(attrs.merge(processor_id: object.id))
64
- end
65
- end
66
- end
67
- end
68
- end
@@ -1,7 +0,0 @@
1
- module Pay
2
- module PaddleBilling
3
- class Error < Pay::Error
4
- delegate :message, to: :cause
5
- end
6
- end
7
- end
@@ -1,40 +0,0 @@
1
- module Pay
2
- module PaddleBilling
3
- class PaymentMethod
4
- attr_reader :pay_payment_method
5
-
6
- delegate :customer, :processor_id, to: :pay_payment_method
7
-
8
- def self.sync(pay_customer:, attributes:)
9
- details = attributes.method_details
10
- attrs = {
11
- type: details.type.downcase
12
- }
13
-
14
- case details.type.downcase
15
- when "card"
16
- attrs[:brand] = details.card.type
17
- attrs[:last4] = details.card.last4
18
- attrs[:exp_month] = details.card.expiry_month
19
- attrs[:exp_year] = details.card.expiry_year
20
- end
21
-
22
- payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.stored_payment_method_id)
23
- payment_method.update!(attrs)
24
- payment_method
25
- end
26
-
27
- def initialize(pay_payment_method)
28
- @pay_payment_method = pay_payment_method
29
- end
30
-
31
- # Sets payment method as default
32
- def make_default!
33
- end
34
-
35
- # Remove payment method
36
- def detach
37
- end
38
- end
39
- end
40
- end
@@ -1,185 +0,0 @@
1
- module Pay
2
- module PaddleBilling
3
- class Subscription
4
- attr_reader :pay_subscription
5
-
6
- delegate :active?,
7
- :canceled?,
8
- :on_grace_period?,
9
- :on_trial?,
10
- :ends_at,
11
- :name,
12
- :owner,
13
- :pause_starts_at,
14
- :pause_starts_at?,
15
- :processor_id,
16
- :processor_plan,
17
- :processor_subscription,
18
- :prorate,
19
- :prorate?,
20
- :quantity,
21
- :quantity?,
22
- :trial_ends_at,
23
- to: :pay_subscription
24
-
25
- def self.sync_from_transaction(transaction_id)
26
- transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
27
- sync(transaction.subscription_id) if transaction.subscription_id
28
- end
29
-
30
- def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
31
- # Passthrough is not return from this API, so we can't use that
32
- object ||= ::Paddle::Subscription.retrieve(id: subscription_id)
33
-
34
- pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
35
- return unless pay_customer
36
-
37
- attributes = {
38
- current_period_end: object.current_billing_period&.ends_at,
39
- current_period_start: object.current_billing_period&.starts_at,
40
- ends_at: (object.canceled_at ? Time.parse(object.canceled_at) : nil),
41
- metadata: object.custom_data,
42
- paddle_cancel_url: object.management_urls&.cancel,
43
- paddle_update_url: object.management_urls&.update_payment_method,
44
- pause_starts_at: (object.paused_at ? Time.parse(object.paused_at) : nil),
45
- status: object.status
46
- }
47
-
48
- if object.items&.first
49
- item = object.items.first
50
- attributes[:processor_plan] = item.price.id
51
- attributes[:quantity] = item.quantity
52
- end
53
-
54
- case attributes[:status]
55
- when "canceled"
56
- # Remove payment methods since customer cannot be reused after cancelling
57
- Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
58
- when "trialing"
59
- attributes[:trial_ends_at] = Time.parse(object.next_billed_at)
60
- when "paused"
61
- attributes[:pause_starts_at] = Time.parse(object.paused_at)
62
- end
63
-
64
- case object.scheduled_change&.action
65
- when "cancel"
66
- attributes[:ends_at] = Time.parse(object.scheduled_change.effective_at)
67
- when "pause"
68
- attributes[:pause_starts_at] = Time.parse(object.scheduled_change.effective_at)
69
- when "resume"
70
- attributes[:pause_resumes_at] = Time.parse(object.scheduled_change.effective_at)
71
- end
72
-
73
- # Update or create the subscription
74
- if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: subscription_id))
75
- pay_subscription.with_lock do
76
- pay_subscription.update!(attributes)
77
- end
78
- pay_subscription
79
- else
80
- pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: subscription_id))
81
- end
82
- end
83
-
84
- def initialize(pay_subscription)
85
- @pay_subscription = pay_subscription
86
- end
87
-
88
- def subscription(**options)
89
- @paddle_billing_subscription ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
90
- end
91
-
92
- # Get a transaction to update payment method
93
- def payment_method_transaction
94
- ::Paddle::Subscription.get_transaction(id: processor_id)
95
- end
96
-
97
- # If a subscription is paused, cancel immediately
98
- # Otherwise, cancel at period end
99
- def cancel(**options)
100
- return if canceled?
101
-
102
- response = ::Paddle::Subscription.cancel(
103
- id: processor_id,
104
- effective_from: options.fetch(:effective_from, (paused? ? "immediately" : "next_billing_period"))
105
- )
106
- pay_subscription.update(
107
- status: response.status,
108
- ends_at: response.scheduled_change.effective_at
109
- )
110
- rescue ::Paddle::Error => e
111
- raise Pay::PaddleBilling::Error, e
112
- end
113
-
114
- def cancel_now!(**options)
115
- cancel(options.merge(effective_from: "immediately"))
116
- rescue ::Paddle::Error => e
117
- raise Pay::PaddleBilling::Error, e
118
- end
119
-
120
- def change_quantity(quantity, **options)
121
- items = [{
122
- price_id: processor_plan,
123
- quantity: quantity
124
- }]
125
-
126
- ::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
127
- rescue ::Paddle::Error => e
128
- raise Pay::PaddleBilling::Error, e
129
- end
130
-
131
- # A subscription could be set to cancel or pause in the future
132
- # It is considered on grace period until the cancel or pause time begins
133
- def on_grace_period?
134
- (canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
135
- end
136
-
137
- def paused?
138
- pay_subscription.status == "paused"
139
- end
140
-
141
- def pause
142
- response = ::Paddle::Subscription.pause(id: processor_id)
143
- pay_subscription.update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
144
- rescue ::Paddle::Error => e
145
- raise Pay::PaddleBilling::Error, e
146
- end
147
-
148
- def resumable?
149
- paused?
150
- end
151
-
152
- def resume
153
- unless resumable?
154
- raise StandardError, "You can only resume paused subscriptions."
155
- end
156
-
157
- # Paddle Billing API only allows "resuming" subscriptions when they are paused
158
- # So cancel the scheduled change if it is in the future
159
- if paused? && pause_starts_at? && Time.current < pause_starts_at
160
- ::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
161
- else
162
- ::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
163
- end
164
-
165
- pay_subscription.update(status: :active, pause_starts_at: nil)
166
- rescue ::Paddle::Error => e
167
- raise Pay::PaddleBilling::Error, e
168
- end
169
-
170
- def swap(plan, **options)
171
- items = [{
172
- price_id: plan,
173
- quantity: quantity || 1
174
- }]
175
-
176
- ::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
177
- pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
178
- end
179
-
180
- # Retries the latest invoice for a Past Due subscription
181
- def retry_failed_payment
182
- end
183
- end
184
- end
185
- end
@@ -1,11 +0,0 @@
1
- module Pay
2
- module PaddleBilling
3
- module Webhooks
4
- class Subscription
5
- def call(event)
6
- Pay::PaddleBilling::Subscription.sync(event.id, object: event)
7
- end
8
- end
9
- end
10
- end
11
- end
@@ -1,11 +0,0 @@
1
- module Pay
2
- module PaddleBilling
3
- module Webhooks
4
- class TransactionCompleted
5
- def call(event)
6
- Pay::PaddleBilling::Charge.sync(event.id)
7
- end
8
- end
9
- end
10
- end
11
- end
@@ -1,138 +0,0 @@
1
- module Pay
2
- module LemonSqueezy
3
- autoload :Billable, "pay/stripe/billable"
4
- autoload :Charge, "pay/stripe/charge"
5
- autoload :Error, "pay/stripe/error"
6
- autoload :Merchant, "pay/stripe/merchant"
7
- autoload :PaymentMethod, "pay/stripe/payment_method"
8
- autoload :Subscription, "pay/stripe/subscription"
9
-
10
- module Webhooks
11
- autoload :AccountUpdated, "pay/stripe/webhooks/account_updated"
12
- autoload :ChargeRefunded, "pay/stripe/webhooks/charge_refunded"
13
- autoload :ChargeSucceeded, "pay/stripe/webhooks/charge_succeeded"
14
- autoload :CheckoutSessionCompleted, "pay/stripe/webhooks/checkout_session_completed"
15
- autoload :CheckoutSessionAsyncPaymentSucceeded, "pay/stripe/webhooks/checkout_session_async_payment_succeeded"
16
- autoload :CustomerDeleted, "pay/stripe/webhooks/customer_deleted"
17
- autoload :CustomerUpdated, "pay/stripe/webhooks/customer_updated"
18
- autoload :PaymentActionRequired, "pay/stripe/webhooks/payment_action_required"
19
- autoload :PaymentFailed, "pay/stripe/webhooks/payment_failed"
20
- autoload :PaymentIntentSucceeded, "pay/stripe/webhooks/payment_intent_succeeded"
21
- autoload :PaymentMethodAttached, "pay/stripe/webhooks/payment_method_attached"
22
- autoload :PaymentMethodDetached, "pay/stripe/webhooks/payment_method_detached"
23
- autoload :PaymentMethodUpdated, "pay/stripe/webhooks/payment_method_updated"
24
- autoload :SubscriptionCreated, "pay/stripe/webhooks/subscription_created"
25
- autoload :SubscriptionDeleted, "pay/stripe/webhooks/subscription_deleted"
26
- autoload :SubscriptionRenewing, "pay/stripe/webhooks/subscription_renewing"
27
- autoload :SubscriptionUpdated, "pay/stripe/webhooks/subscription_updated"
28
- autoload :SubscriptionTrialWillEnd, "pay/stripe/webhooks/subscription_trial_will_end"
29
- end
30
-
31
- extend Env
32
-
33
- REQUIRED_VERSION = "~> 1"
34
-
35
- def self.enabled?
36
- return false unless Pay.enabled_processors.include?(:lemonsqueezy) && defined?(::Lemonzsqueezy)
37
-
38
- Pay::Engine.version_matches?(required: REQUIRED_VERSION, current: ::Lemonsqueezy::VERSION) || (raise "[Pay] lemonsqueezy gem must be version #{REQUIRED_VERSION}")
39
- end
40
-
41
- def self.setup
42
- ::Stripe.api_key = private_key
43
-
44
- # Used by Stripe to identify Pay for support
45
- ::Stripe.set_app_info("PayRails", partner_id: "pp_partner_IqhY0UExnJYLxg", version: Pay::VERSION, url: "https://github.com/pay-rails/pay")
46
-
47
- # Automatically retry requests that fail
48
- # This automatically includes idempotency keys in the request to guarantee that retires are safe
49
- # https://github.com/stripe/stripe-ruby#configuring-automatic-retries
50
- ::Stripe.max_network_retries = 2
51
- end
52
-
53
- def self.public_key
54
- find_value_by_name(:stripe, :public_key)
55
- end
56
-
57
- def self.private_key
58
- find_value_by_name(:stripe, :private_key)
59
- end
60
-
61
- def self.signing_secret
62
- find_value_by_name(:stripe, :signing_secret)
63
- end
64
-
65
- def self.configure_webhooks
66
- Pay::Webhooks.configure do |events|
67
- # Listen to the charge event to make sure we get non-subscription
68
- # purchases as well. Invoice is only for subscriptions and manual creation
69
- # so it does not include individual charges.
70
- events.subscribe "stripe.charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
71
- events.subscribe "stripe.charge.refunded", Pay::Stripe::Webhooks::ChargeRefunded.new
72
-
73
- events.subscribe "stripe.payment_intent.succeeded", Pay::Stripe::Webhooks::PaymentIntentSucceeded.new
74
-
75
- # Warn user of upcoming charges for their subscription. This is handy for
76
- # notifying annual users their subscription will renew shortly.
77
- # This probably should be ignored for monthly subscriptions.
78
- events.subscribe "stripe.invoice.upcoming", Pay::Stripe::Webhooks::SubscriptionRenewing.new
79
-
80
- # Payment action is required to process an invoice
81
- events.subscribe "stripe.invoice.payment_action_required", Pay::Stripe::Webhooks::PaymentActionRequired.new
82
-
83
- # If an invoice payment fails, we want to notify the user via email to update their payment details
84
- events.subscribe "stripe.invoice.payment_failed", Pay::Stripe::Webhooks::PaymentFailed.new
85
-
86
- # If a subscription is manually created on Stripe, we want to sync
87
- events.subscribe "stripe.customer.subscription.created", Pay::Stripe::Webhooks::SubscriptionCreated.new
88
-
89
- # If the plan, quantity, or trial ending date is updated on Stripe, we want to sync
90
- events.subscribe "stripe.customer.subscription.updated", Pay::Stripe::Webhooks::SubscriptionUpdated.new
91
-
92
- # When a customers subscription is canceled, we want to update our records
93
- events.subscribe "stripe.customer.subscription.deleted", Pay::Stripe::Webhooks::SubscriptionDeleted.new
94
-
95
- # When a customers subscription trial period is 3 days from ending or ended immediately this event is fired
96
- events.subscribe "stripe.customer.subscription.trial_will_end", Pay::Stripe::Webhooks::SubscriptionTrialWillEnd.new
97
-
98
- # Monitor changes for customer's default card changing and invoice credit updates
99
- events.subscribe "stripe.customer.updated", Pay::Stripe::Webhooks::CustomerUpdated.new
100
-
101
- # If a customer was deleted in Stripe, their subscriptions should be cancelled
102
- events.subscribe "stripe.customer.deleted", Pay::Stripe::Webhooks::CustomerDeleted.new
103
-
104
- # If a customer's payment source was deleted in Stripe, we should update as well
105
- events.subscribe "stripe.payment_method.attached", Pay::Stripe::Webhooks::PaymentMethodAttached.new
106
- events.subscribe "stripe.payment_method.updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
107
- events.subscribe "stripe.payment_method.card_automatically_updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
108
- events.subscribe "stripe.payment_method.detached", Pay::Stripe::Webhooks::PaymentMethodDetached.new
109
-
110
- # If an account is updated in stripe, we should update it as well
111
- events.subscribe "stripe.account.updated", Pay::Stripe::Webhooks::AccountUpdated.new
112
-
113
- # Handle subscriptions in Stripe Checkout Sessions
114
- events.subscribe "stripe.checkout.session.completed", Pay::Stripe::Webhooks::CheckoutSessionCompleted.new
115
- events.subscribe "stripe.checkout.session.async_payment_succeeded", Pay::Stripe::Webhooks::CheckoutSessionAsyncPaymentSucceeded.new
116
- end
117
- end
118
-
119
- def self.to_client_reference_id(record)
120
- raise ArgumentError, "#{record.class.name} does not include Pay. Allowed models: #{model_names.to_a.join(", ")}" unless model_names.include?(record.class.name)
121
- [record.class.name, record.id].join("_")
122
- end
123
-
124
- def self.find_by_client_reference_id(client_reference_id)
125
- # If there is a client reference ID, make sure we have a Pay::Customer record
126
- # client_reference_id should be in the format of "User/1"
127
- model_name, id = client_reference_id.split("_", 2)
128
-
129
- # Only allow model names that use Pay
130
- return unless model_names.include?(model_name)
131
-
132
- model_name.constantize.find(id)
133
- rescue ActiveRecord::RecordNotFound
134
- Rails.logger.error "[Pay] Unable to locate record with: #{client_reference_id}"
135
- nil
136
- end
137
- end
138
- end