pay 9.0.1 → 10.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 351dfe4e2001a3d892ba872ca305a44c49fde456d78fa1aaebf505fe603d3bc1
4
- data.tar.gz: a9bdd0f9c7b81325bba64cf63e610a520867678a51c6ea1c30c29a43b47c90e4
3
+ metadata.gz: 97c43263ab73cd849da8c65e8a4f1235aabb28b6f35c56c5441c077838374186
4
+ data.tar.gz: c0161223fdbd12eee31ef928639bba40e9af1a63e2e6cdeee3e2a69fce106de4
5
5
  SHA512:
6
- metadata.gz: 9bf920b25c5c845e31aa639dfa725ae5e3beaa226892518bd6c025b7805cb70cb6e1c211e743204d9b5fea14b93c6506c5d58ec59725a189457b8db3dee97893
7
- data.tar.gz: 1cdd1a4d26048b9683fc4a83df5f327c7ef4792b66b831ed0464dc9f035df9a8e72e35b8592cf19c103f56faa5b1777b2cee8b97e07f054a88b79b2bd177c3e0
6
+ metadata.gz: 1fd5fdc16c976d6d33e908210c54de32cdbfcea6e0d641b58eb5f6e52fb5ff217bb59ac6ae3bb7118f8e59f5f6bcc2f2bd88fd61736d01c9fff386ebf24937ef
7
+ data.tar.gz: ffee6759b0bae293ffadb173158a57bac35e9f10150a38846499e2ffe635380a64606847098303a9422addd23e7e372d08669929b973c4135df2f2f81a3a0d2c
@@ -131,8 +131,9 @@ module Pay
131
131
  end
132
132
 
133
133
  braintree_plan = find_braintree_plan(plan)
134
+ prorate = options.fetch(:prorate) { true }
134
135
 
135
- if would_change_billing_frequency?(braintree_plan) && prorate?
136
+ if would_change_billing_frequency?(braintree_plan) && prorate
136
137
  swap_across_frequencies(braintree_plan)
137
138
  return
138
139
  end
@@ -143,7 +144,7 @@ module Pay
143
144
  never_expires: true,
144
145
  number_of_billing_cycles: nil,
145
146
  options: {
146
- prorate_charges: prorate?
147
+ prorate_charges: prorate
147
148
  }
148
149
  })
149
150
  raise Error, "Braintree failed to swap plans: #{result.message}" unless result.success?
@@ -23,20 +23,8 @@ module Pay
23
23
  store_accessor :data, :username # Venmo
24
24
  store_accessor :data, :bank
25
25
 
26
- store_accessor :data, :amount_captured
27
- store_accessor :data, :invoice_id
28
- store_accessor :data, :payment_intent_id
29
- store_accessor :data, :period_start
30
- store_accessor :data, :period_end
31
- store_accessor :data, :line_items
32
- store_accessor :data, :subtotal # subtotal amount in cents
33
- store_accessor :data, :tax # total tax amount in cents
34
- store_accessor :data, :discounts # array of discount IDs applied to the Stripe Invoice
35
- store_accessor :data, :total_discount_amounts # array of discount details
36
- store_accessor :data, :total_tax_amounts # array of tax details for each jurisdiction
37
- store_accessor :data, :credit_notes # array of credit notes for the Stripe Invoice
38
- store_accessor :data, :refunds # array of refunds
39
- store_accessor :data, :balance_transaction
26
+ store_accessor :data, :subtotal
27
+ store_accessor :data, :tax
40
28
 
41
29
  # Helpers for payment processors
42
30
  %w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
@@ -51,8 +39,9 @@ module Pay
51
39
  joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
52
40
  end
53
41
 
54
- def captured?
55
- amount_captured > 0
42
+ def sync!(**options)
43
+ self.class.sync(processor_id, **options)
44
+ reload
56
45
  end
57
46
 
58
47
  def refunded?
@@ -113,9 +102,5 @@ module Pay
113
102
  payment_method_type&.titleize
114
103
  end
115
104
  end
116
-
117
- def line_items
118
- Array.wrap(super)
119
- end
120
105
  end
121
106
  end
@@ -1,6 +1,10 @@
1
1
  module Pay
2
2
  module FakeProcessor
3
3
  class Charge < Pay::Charge
4
+ def self.sync(processor_id)
5
+ true
6
+ end
7
+
4
8
  def api_record
5
9
  self
6
10
  end
@@ -68,6 +68,16 @@ module Pay
68
68
  end
69
69
  end
70
70
 
71
+ def save
72
+ ls_type, ls_id = processor_id.split(":", 2)
73
+ case ls_type
74
+ when "order"
75
+ self.class.sync_order(ls_id)
76
+ when "subscription_invoice"
77
+ self.class.sync_subscription_invoice(ls_id)
78
+ end
79
+ end
80
+
71
81
  def api_record
72
82
  ls_type, ls_id = processor_id.split(":", 2)
73
83
  case ls_type
@@ -1,6 +1,9 @@
1
1
  module Pay
2
2
  module PaddleBilling
3
3
  class Subscription < Pay::Subscription
4
+ store_accessor :data, :paddle_update_url
5
+ store_accessor :data, :paddle_cancel_url
6
+
4
7
  def self.sync_from_transaction(transaction_id)
5
8
  transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
6
9
  sync(transaction.subscription_id) if transaction.subscription_id
@@ -1,6 +1,9 @@
1
1
  module Pay
2
2
  module PaddleClassic
3
3
  class Subscription < Pay::Subscription
4
+ store_accessor :data, :paddle_update_url
5
+ store_accessor :data, :paddle_cancel_url
6
+
4
7
  def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
5
8
  # Passthrough is not return from this API, so we can't use that
6
9
  object ||= PaddleClassic.client.users.list(subscription_id: subscription_id).data.try(:first)
@@ -128,7 +131,7 @@ module Pay
128
131
  def swap(plan, **options)
129
132
  raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
130
133
 
131
- attributes = {plan_id: plan, prorate: prorate}
134
+ attributes = {plan_id: plan, prorate: options.fetch(:prorate) { true }}
132
135
  attributes[:quantity] = quantity if quantity?
133
136
  PaddleClassic.client.users.update(subscription_id: processor_id, **attributes)
134
137
 
@@ -1,11 +1,21 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  class Charge < Pay::Charge
4
+ EXPAND = ["balance_transaction", "refunds"]
5
+
6
+ delegate :amount_captured, :payment_intent, to: :stripe_object
7
+
8
+ store_accessor :data, :stripe_invoice
4
9
  store_accessor :data, :stripe_receipt_url
5
10
 
11
+ def self.sync_payment_intent(id, stripe_account: nil)
12
+ payment_intent = ::Stripe::PaymentIntent.retrieve({id: id}, {stripe_account: stripe_account}.compact)
13
+ sync(payment_intent.latest_charge, stripe_account: stripe_account)
14
+ end
15
+
6
16
  def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
7
17
  # Skip loading the latest charge details from the API if we already have it
8
- object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: ["balance_transaction", "invoice.total_discount_amounts.discount", "invoice.total_tax_amounts.tax_rate", "refunds"]}, {stripe_account: stripe_account}.compact)
18
+ object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: EXPAND}, {stripe_account: stripe_account}.compact)
9
19
  if object.customer.blank?
10
20
  Rails.logger.debug "Stripe Charge #{object.id} does not have a customer"
11
21
  return
@@ -17,68 +27,34 @@ module Pay
17
27
  return
18
28
  end
19
29
 
20
- refunds = []
21
- object.refunds.auto_paging_each { |refund| refunds << refund }
22
-
23
30
  payment_method = object.payment_method_details.try(object.payment_method_details.type)
24
31
  attrs = {
32
+ object: object.to_hash,
25
33
  amount: object.amount,
26
- amount_captured: object.amount_captured,
27
34
  amount_refunded: object.amount_refunded,
28
35
  application_fee_amount: object.application_fee_amount,
29
- balance_transaction: object.balance_transaction,
30
36
  bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
31
37
  brand: payment_method.try(:brand)&.capitalize,
32
38
  created_at: Time.at(object.created),
33
39
  currency: object.currency,
34
- discounts: [],
35
40
  exp_month: payment_method.try(:exp_month).to_s,
36
41
  exp_year: payment_method.try(:exp_year).to_s,
37
42
  last4: payment_method.try(:last4).to_s,
38
- line_items: [],
39
43
  metadata: object.metadata,
40
- payment_intent_id: object.payment_intent,
41
44
  payment_method_type: object.payment_method_details.type,
42
45
  stripe_account: pay_customer.stripe_account,
43
- stripe_receipt_url: object.receipt_url,
44
- total_tax_amounts: [],
45
- refunds: refunds.sort_by! { |r| r["created"] }
46
+ stripe_receipt_url: object.receipt_url
46
47
  }
47
48
 
48
49
  # Associate charge with subscription if we can
49
- if object.invoice
50
- invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve({id: object.invoice, expand: ["total_discount_amounts.discount", "total_tax_amounts.tax_rate"]}, {stripe_account: stripe_account}.compact))
51
- attrs[:invoice_id] = invoice.id
52
- attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: invoice.subscription)
53
-
54
- attrs[:period_start] = Time.at(invoice.period_start)
55
- attrs[:period_end] = Time.at(invoice.period_end)
50
+ invoice_payments = ::Stripe::InvoicePayment.list({payment: {type: :payment_intent, payment_intent: object.payment_intent}, status: :paid, expand: ["data.invoice.total_discount_amounts.discount"]}, {stripe_account: stripe_account}.compact)
51
+ if invoice_payments.any? && (invoice = invoice_payments.first.invoice)
52
+ attrs[:stripe_invoice] = invoice.to_hash
56
53
  attrs[:subtotal] = invoice.subtotal
57
- attrs[:tax] = invoice.tax
58
- attrs[:discounts] = invoice.discounts
59
- attrs[:total_tax_amounts] = invoice.total_tax_amounts.map(&:to_hash)
60
- attrs[:total_discount_amounts] = invoice.total_discount_amounts.map(&:to_hash)
61
-
62
- invoice.lines.auto_paging_each do |line_item|
63
- # Currency is tied to the charge, so storing it would be duplication
64
- attrs[:line_items] << {
65
- id: line_item.id,
66
- description: line_item.description,
67
- price_id: line_item.price&.id,
68
- quantity: line_item.quantity,
69
- unit_amount: line_item.price&.unit_amount,
70
- amount: line_item.amount,
71
- discounts: line_item.discounts,
72
- tax_amounts: line_item.tax_amounts,
73
- proration: line_item.proration,
74
- period_start: Time.at(line_item.period.start),
75
- period_end: Time.at(line_item.period.end)
76
- }
54
+ attrs[:tax] = invoice.total - invoice.total_excluding_tax.to_i
55
+ if (subscription = invoice.parent.try(:subscription_details).try(:subscription))
56
+ attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: subscription)
77
57
  end
78
- # Charges without invoices
79
- else
80
- attrs[:period_start] = Time.at(object.created)
81
- attrs[:period_end] = Time.at(object.created)
82
58
  end
83
59
 
84
60
  # Update or create the charge
@@ -90,16 +66,14 @@ module Pay
90
66
  end
91
67
  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
92
68
  try += 1
93
- if try <= retries
94
- sleep 0.1
95
- retry
96
- else
97
- raise
98
- end
69
+ raise unless try <= retries
70
+
71
+ sleep 0.1
72
+ retry
99
73
  end
100
74
 
101
75
  def api_record
102
- ::Stripe::Charge.retrieve({id: processor_id, expand: ["customer", "invoice.subscription"]}, stripe_options)
76
+ ::Stripe::Charge.retrieve({id: processor_id, expand: EXPAND}, stripe_options)
103
77
  rescue ::Stripe::StripeError => e
104
78
  raise Pay::Stripe::Error, e
105
79
  end
@@ -116,7 +90,7 @@ module Pay
116
90
  def refund!(amount_to_refund, **options)
117
91
  amount_to_refund ||= amount
118
92
 
119
- if invoice_id.present?
93
+ if stripe_invoice.present?
120
94
  description = options.delete(:description) || I18n.t("pay.refund")
121
95
  lines = [{type: :custom_line_item, description: description, quantity: 1, unit_amount: amount_to_refund}]
122
96
  credit_note!(**options.merge(refund_amount: amount_to_refund, lines: lines))
@@ -130,8 +104,9 @@ module Pay
130
104
 
131
105
  # Adds a credit note to a Stripe Invoice
132
106
  def credit_note!(**options)
133
- raise Pay::Stripe::Error, "no Stripe invoice_id on Pay::Charge" if invoice_id.blank?
134
- ::Stripe::CreditNote.create({invoice: invoice_id}.merge(options), stripe_options)
107
+ raise Pay::Stripe::Error, "no Stripe Invoice on Pay::Charge" if stripe_invoice.blank?
108
+
109
+ ::Stripe::CreditNote.create({invoice: stripe_invoice.id}.merge(options), stripe_options)
135
110
  rescue ::Stripe::StripeError => e
136
111
  raise Pay::Stripe::Error, e
137
112
  end
@@ -141,13 +116,28 @@ module Pay
141
116
  # capture
142
117
  # capture(amount_to_capture: 15_00)
143
118
  def capture(**options)
144
- raise Pay::Stripe::Error, "no payment_intent_id on charge" unless payment_intent_id.present?
145
- ::Stripe::PaymentIntent.capture(payment_intent_id, options, stripe_options)
119
+ raise Pay::Stripe::Error, "no payment_intent on charge" unless payment_intent.present?
120
+
121
+ ::Stripe::PaymentIntent.capture(payment_intent, options, stripe_options)
146
122
  self.class.sync(processor_id)
147
123
  rescue ::Stripe::StripeError => e
148
124
  raise Pay::Stripe::Error, e
149
125
  end
150
126
 
127
+ def captured?
128
+ amount_captured > 0
129
+ end
130
+
131
+ def stripe_invoice
132
+ if (value = data.dig("stripe_invoice"))
133
+ ::Stripe::Invoice.construct_from(value)
134
+ end
135
+ end
136
+
137
+ def stripe_object
138
+ ::Stripe::Charge.construct_from(object) if object?
139
+ end
140
+
151
141
  private
152
142
 
153
143
  # Options for Stripe requests
@@ -58,8 +58,7 @@ module Pay
58
58
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
59
59
  quantity = options.delete(:quantity)
60
60
  opts = {
61
- expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge"],
62
- items: [plan: plan, quantity: quantity]
61
+ items: [price: plan, quantity: quantity]
63
62
  }.merge(options)
64
63
 
65
64
  # Load the Stripe customer to verify it exists and update payment method if needed
@@ -73,7 +72,8 @@ module Pay
73
72
 
74
73
  # No trial, payment method requires SCA
75
74
  if options[:payment_behavior].to_s != "default_incomplete" && subscription.incomplete?
76
- Pay::Payment.new(stripe_sub.latest_invoice.payment_intent).validate
75
+ payment_intent_id = stripe_sub.latest_invoice.payments.first.payment.payment_intent
76
+ Pay::Payment.from_id(payment_intent_id).validate
77
77
  end
78
78
 
79
79
  subscription
@@ -23,6 +23,7 @@ module Pay
23
23
  end
24
24
 
25
25
  attributes = {
26
+ object: object.to_hash,
26
27
  application_fee_percent: object.application_fee_percent,
27
28
  created_at: Time.at(object.created),
28
29
  processor_plan: object.items.first.price.id,
@@ -30,12 +31,11 @@ module Pay
30
31
  status: object.status,
31
32
  stripe_account: pay_customer.stripe_account,
32
33
  metadata: object.metadata,
33
- subscription_items: [],
34
34
  metered: false,
35
35
  pause_behavior: object.pause_collection&.behavior,
36
36
  pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil),
37
- current_period_start: (object.current_period_start ? Time.at(object.current_period_start) : nil),
38
- current_period_end: (object.current_period_end ? Time.at(object.current_period_end) : nil)
37
+ current_period_start: (object.items.first.current_period_start ? Time.at(object.items.first.current_period_start) : nil),
38
+ current_period_end: (object.items.first.current_period_end ? Time.at(object.items.first.current_period_end) : nil)
39
39
  }
40
40
 
41
41
  # Subscriptions that have ended should have their trial ended at the
@@ -49,13 +49,9 @@ module Pay
49
49
  attributes[:trial_ends_at] = Time.at(trial_ended_at)
50
50
  end
51
51
 
52
- # Record subscription items to db
53
52
  object.items.auto_paging_each do |subscription_item|
54
- if !attributes[:metered] && (subscription_item.to_hash.dig(:price, :recurring, :usage_type) == "metered")
55
- attributes[:metered] = true
56
- end
57
-
58
- attributes[:subscription_items] << subscription_item.to_hash.slice(:id, :price, :metadata, :quantity)
53
+ next if attributes[:metered]
54
+ attributes[:metered] = true if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
59
55
  end
60
56
 
61
57
  attributes[:ends_at] = if object.ended_at
@@ -66,7 +62,7 @@ module Pay
66
62
  Time.at(object.cancel_at)
67
63
  elsif object.cancel_at_period_end
68
64
  # Subscriptions cancelling in the future
69
- Time.at(object.current_period_end)
65
+ Time.at(object.items.first.current_period_end)
70
66
  end
71
67
 
72
68
  # Sync payment method if directly attached to subscription
@@ -87,7 +83,7 @@ module Pay
87
83
  # Any other pause status (or no pause at all) should have nil for start
88
84
  if pay_subscription.pause_behavior != attributes[:pause_behavior]
89
85
  attributes[:pause_starts_at] = if attributes[:pause_behavior] == "void"
90
- Time.at(object.current_period_end)
86
+ Time.at(object.items.first.current_period_end)
91
87
  end
92
88
  end
93
89
 
@@ -102,8 +98,11 @@ module Pay
102
98
  pay_subscription.api_record = object
103
99
 
104
100
  # Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
105
- if (charge = object.try(:latest_invoice).try(:charge)) && charge.try(:status) == "succeeded"
106
- Pay::Stripe::Charge.sync(charge.id, stripe_account: pay_subscription.stripe_account)
101
+ if (invoice = object.try(:latest_invoice))
102
+ Array(invoice.try(:payments)).each do |invoice_payment|
103
+ next unless invoice_payment.status == "paid"
104
+ Pay::Stripe::Charge.sync_payment_intent(invoice_payment.payment.payment_intent, stripe_account: pay_subscription.stripe_account)
105
+ end
107
106
  end
108
107
 
109
108
  pay_subscription
@@ -121,23 +120,27 @@ module Pay
121
120
  def self.expand_options
122
121
  {
123
122
  expand: [
123
+ "discounts",
124
124
  "default_payment_method",
125
125
  "pending_setup_intent",
126
- "latest_invoice.payment_intent",
127
- "latest_invoice.charge",
128
- "latest_invoice.total_discount_amounts.discount",
129
- "latest_invoice.total_tax_amounts.tax_rate"
126
+ "latest_invoice.confirmation_secret",
127
+ "latest_invoice.payments",
128
+ "latest_invoice.total_discount_amounts.discount"
130
129
  ]
131
130
  }
132
131
  end
133
132
 
133
+ def stripe_object
134
+ ::Stripe::Subscription.construct_from(object)
135
+ end
136
+
134
137
  def api_record(**options)
135
138
  @api_record ||= ::Stripe::Subscription.retrieve(options.with_defaults(id: processor_id).merge(expand_options), {stripe_account: stripe_account}.compact)
136
139
  end
137
140
 
138
141
  # Returns a SetupIntent or PaymentIntent client secret for the subscription
139
142
  def client_secret
140
- api_record&.pending_setup_intent&.client_secret || api_record&.latest_invoice&.payment_intent&.client_secret
143
+ api_record&.pending_setup_intent&.client_secret || api_record&.latest_invoice&.confirmation_secret&.client_secret
141
144
  end
142
145
 
143
146
  # Sets the default_payment_method on a subscription
@@ -160,7 +163,7 @@ module Pay
160
163
  cancel_now!
161
164
  else
162
165
  @api_record = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}.merge(expand_options), stripe_options)
163
- update(ends_at: (on_trial? ? trial_ends_at : Time.at(@api_record.current_period_end)))
166
+ update(ends_at: (on_trial? ? trial_ends_at : Time.at(@api_record.items.first.current_period_end)))
164
167
  end
165
168
  rescue ::Stripe::StripeError => e
166
169
  raise Pay::Stripe::Error, e
@@ -184,7 +187,7 @@ module Pay
184
187
  # For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available
185
188
  # Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update
186
189
  def change_quantity(quantity, **options)
187
- subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.dig("id")
190
+ subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.id
188
191
  if subscription_item_id
189
192
  ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
190
193
  @api_record = nil
@@ -234,7 +237,7 @@ module Pay
234
237
  update(
235
238
  pause_behavior: behavior,
236
239
  pause_resumes_at: (@api_record.pause_collection&.resumes_at ? Time.at(@api_record.pause_collection&.resumes_at) : nil),
237
- pause_starts_at: ((behavior == "void") ? Time.at(@api_record.current_period_end) : nil)
240
+ pause_starts_at: ((behavior == "void") ? Time.at(@api_record.items.first.current_period_end) : nil)
238
241
  )
239
242
  end
240
243
 
@@ -277,6 +280,7 @@ module Pay
277
280
  def swap(plan, **options)
278
281
  raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
279
282
 
283
+ prorate = options.fetch(:prorate) { true }
280
284
  proration_behavior = options.delete(:proration_behavior) || (prorate ? "always_invoice" : "none")
281
285
 
282
286
  @api_record = ::Stripe::Subscription.update(
@@ -292,8 +296,8 @@ module Pay
292
296
  )
293
297
 
294
298
  # Validate that swap was successful and handle SCA if needed
295
- if (payment_intent = @api_record.latest_invoice.payment_intent)
296
- Pay::Payment.new(payment_intent).validate
299
+ if (payment_intent_id = @api_record.latest_invoice.payments.first.payment.payment_intent)
300
+ Pay::Payment.from_id(payment_intent_id).validate
297
301
  end
298
302
 
299
303
  sync!(object: @api_record)
@@ -301,33 +305,19 @@ module Pay
301
305
  raise Pay::Stripe::Error, e
302
306
  end
303
307
 
304
- # Creates a metered billing usage record
305
- #
306
- # Uses the first subscription_item ID unless `subscription_item_id: "si_1234"` is passed
307
- #
308
- # create_usage_record(quantity: 4, action: :increment)
309
- # create_usage_record(subscription_item_id: "si_1234", quantity: 100, action: :set)
310
- def create_usage_record(**options)
311
- subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
312
- ::Stripe::SubscriptionItem.create_usage_record(subscription_item_id, options, stripe_options)
313
- end
314
-
315
- # Returns usage record summaries for a subscription item
316
- def usage_record_summaries(**options)
317
- subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
318
- ::Stripe::SubscriptionItem.list_usage_record_summaries(subscription_item_id, options, stripe_options)
308
+ def subscription_items
309
+ stripe_object.items
319
310
  end
320
311
 
321
312
  # Returns the first metered subscription item
322
313
  def metered_subscription_item
323
- subscription_items.find do |subscription_item|
324
- subscription_item.dig("price", "recurring", "usage_type") == "metered"
314
+ subscription_items.auto_paging_each do |subscription_item|
315
+ return subscription_item if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
325
316
  end
326
317
  end
327
318
 
328
- # Returns an upcoming invoice for a subscription
329
- def upcoming_invoice(**options)
330
- ::Stripe::Invoice.upcoming(options.merge(subscription: processor_id), stripe_options)
319
+ def preview_invoice(**options)
320
+ ::Stripe::Invoice.create_preview(options.merge(subscription: processor_id), stripe_options)
331
321
  end
332
322
 
333
323
  # Retries the latest invoice for a Past Due subscription and attempts to pay it
@@ -27,12 +27,6 @@ module Pay
27
27
  # Callbacks
28
28
  before_destroy :cancel_if_active
29
29
 
30
- store_accessor :data, :paddle_update_url
31
- store_accessor :data, :paddle_cancel_url
32
- store_accessor :data, :subscription_items
33
-
34
- attribute :prorate, :boolean, default: true
35
-
36
30
  # Validations
37
31
  validates :name, presence: true
38
32
  validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
@@ -58,10 +52,6 @@ module Pay
58
52
  reload
59
53
  end
60
54
 
61
- def no_prorate
62
- self.prorate = false
63
- end
64
-
65
55
  def skip_trial
66
56
  self.trial_ends_at = nil
67
57
  end
@@ -24,6 +24,7 @@ en:
24
24
  subtotal: "Subtotal"
25
25
  discount: "Discount"
26
26
  tax: "Tax"
27
+ total: "Total excluding tax"
27
28
  total: "Total"
28
29
  unit_price: "Unit Price"
29
30
  percent_discount: "%{name} (%{percent}% off)"
@@ -0,0 +1,7 @@
1
+ class AddObjectToPayModels < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_column :pay_charges, :object, Pay::Adapter.json_column_type
4
+ add_column :pay_customers, :object, Pay::Adapter.json_column_type
5
+ add_column :pay_subscriptions, :object, Pay::Adapter.json_column_type
6
+ end
7
+ end
data/lib/pay/receipts.rb CHANGED
@@ -31,17 +31,13 @@ module Pay
31
31
  ]
32
32
  ]
33
33
 
34
- # Unit price is stored with the line item
35
- # Negative amounts shouldn't display quantity
36
- # Sort by line_items by period_end? oldest to newest
37
- if line_items.any?
38
- line_items.each do |li|
39
- items << [li["description"], li["quantity"], Pay::Currency.format(li["unit_amount"], currency: currency), Pay::Currency.format(li["amount"], currency: currency)]
40
-
41
- Array.wrap(li["discounts"]).each do |discount_id|
42
- if (discount = total_discount_amounts.find { |d| d.dig("discount", "id") == discount_id })
43
- items << [discount_description(discount), nil, nil, Pay::Currency.format(-discount["amount"], currency: currency)]
44
- end
34
+ if stripe_invoice
35
+ stripe_invoice.lines.auto_paging_each do |line|
36
+ items << [line.description, line.quantity, Pay::Currency.format(line.pricing.unit_amount_decimal, currency: line.currency), Pay::Currency.format(line.amount, currency: line.currency)]
37
+
38
+ line.discounts.each do |discount_id|
39
+ discount = stripe_invoice.total_discount_amounts.find { |d| d.discount.id == discount_id }
40
+ items << [discount_description(discount), nil, nil, Pay::Currency.format(-discount.amount, currency: currency)]
45
41
  end
46
42
  end
47
43
  else
@@ -49,42 +45,47 @@ module Pay
49
45
  end
50
46
 
51
47
  # If no subtotal, we will display the total
52
- items << [nil, nil, I18n.t("pay.line_items.subtotal"), Pay::Currency.format(subtotal || amount, currency: currency)]
48
+ items << [nil, nil, I18n.t("pay.line_items.subtotal"), Pay::Currency.format(stripe_invoice&.subtotal || amount, currency: currency)]
53
49
 
54
50
  # Discounts on the invoice
55
- Array.wrap(discounts).each do |discount_id|
56
- if (discount = total_discount_amounts.find { |d| d.dig("discount", "id") == discount_id })
57
- items << [nil, nil, discount_description(discount), Pay::Currency.format(-discount["amount"], currency: currency)]
58
- end
51
+ stripe_invoice&.discounts&.each do |discount_id|
52
+ discount = stripe_invoice.total_discount_amounts.find { |d| d.discount.id == discount_id }
53
+ items << [nil, nil, discount_description(discount), Pay::Currency.format(-discount.amount, currency: currency)]
54
+ end
55
+
56
+ # Total excluding tax
57
+ if stripe_invoice
58
+ items << [nil, nil, I18n.t("pay.line_items.total"), Pay::Currency.format(stripe_invoice.total_excluding_tax, currency: currency)]
59
59
  end
60
60
 
61
61
  # Tax rates
62
- Array.wrap(total_tax_amounts).each do |tax_amount|
63
- next if tax_amount["amount"].zero?
64
- items << [nil, nil, tax_description(tax_amount), Pay::Currency.format(tax_amount["amount"], currency: currency)]
62
+ stripe_invoice&.total_taxes&.each do |tax|
63
+ next if tax.amount.zero?
64
+ # tax_rate = ::Stripe::TaxRate.retrieve(tax.tax_rate_details.tax_rate)
65
+ items << [nil, nil, I18n.t("pay.line_items.tax"), Pay::Currency.format(tax.amount, currency: currency)]
65
66
  end
66
67
 
68
+ # Total
67
69
  items << [nil, nil, I18n.t("pay.line_items.total"), Pay::Currency.format(amount, currency: currency)]
68
70
  items
69
71
  end
70
72
 
71
73
  def discount_description(discount)
72
- coupon = discount.dig("discount", "coupon")
73
- name = coupon.dig("name")
74
+ coupon = discount.discount.coupon
75
+ name = coupon.name
74
76
 
75
- if (percent = coupon["percent_off"])
77
+ if (percent = coupon.percent_off)
76
78
  I18n.t("pay.line_items.percent_discount", name: name, percent: ActiveSupport::NumberHelper.number_to_rounded(percent, strip_insignificant_zeros: true))
77
79
  else
78
- I18n.t("pay.line_items.amount_discount", name: name, amount: Pay::Currency.format(coupon["amount_off"], currency: coupon["currency"]))
80
+ I18n.t("pay.line_items.amount_discount", name: name, amount: Pay::Currency.format(coupon.amount_off, currency: coupon.currency))
79
81
  end
80
82
  end
81
83
 
82
- def tax_description(tax_amount)
83
- tax_rate = tax_amount["tax_rate"]
84
- percent = "#{ActiveSupport::NumberHelper.number_to_rounded(tax_rate["percentage"], strip_insignificant_zeros: true)}%"
85
- percent += " inclusive" if tax_rate["inclusive"]
86
- "#{tax_rate["display_name"]} - #{tax_rate["jurisdiction"]} (#{percent})"
87
- end
84
+ # def tax_description(tax_rate)
85
+ # percent = "#{ActiveSupport::NumberHelper.number_to_rounded(tax_rate.percentage, strip_insignificant_zeros: true)}%"
86
+ # percent += " inclusive" if tax_rate.inclusive
87
+ # "#{tax_rate.display_name} - #{tax_rate.jurisdiction} (#{percent})"
88
+ # end
88
89
 
89
90
  def receipt_line_items
90
91
  line_items = pdf_line_items
@@ -94,15 +95,15 @@ module Pay
94
95
 
95
96
  if refunded?
96
97
  # If we have a list of individual refunds, add each entry
97
- if refunds&.any?
98
- refunds.each do |refund|
99
- next unless refund["status"] == "succeeded"
100
- refunded_at = Time.at(refund["created"]).to_date
101
- line_items << [nil, nil, I18n.t("pay.receipt.refunded_on", date: I18n.l(refunded_at, format: :long)), Pay::Currency.format(refund["amount"], currency: refund["currency"])]
102
- end
103
- else
104
- line_items << [nil, nil, I18n.t("pay.receipt.refunded"), Pay::Currency.format(amount_refunded, currency: currency)]
105
- end
98
+ # if refunds&.any?
99
+ # refunds.each do |refund|
100
+ # next unless refund["status"] == "succeeded"
101
+ # refunded_at = Time.at(refund["created"]).to_date
102
+ # line_items << [nil, nil, I18n.t("pay.receipt.refunded_on", date: I18n.l(refunded_at, format: :long)), Pay::Currency.format(refund["amount"], currency: refund["currency"])]
103
+ # end
104
+ # else
105
+ line_items << [nil, nil, I18n.t("pay.receipt.refunded"), Pay::Currency.format(amount_refunded, currency: currency)]
106
+ # end
106
107
  end
107
108
 
108
109
  line_items
@@ -11,6 +11,18 @@ module Pay
11
11
 
12
12
  stripe_customer = pay_customer.api_record
13
13
 
14
+ attributes = {
15
+ object: stripe_customer.to_hash
16
+ }
17
+
18
+ # Sync invoice credit balance and currency
19
+ if stripe_customer.invoice_credit_balance.present?
20
+ attributes[:invoice_credit_balance] = stripe_customer.invoice_credit_balance
21
+ attributes[:currency] = stripe_customer.currency
22
+ end
23
+
24
+ pay_customer.update(attributes)
25
+
14
26
  # Sync default card
15
27
  if (payment_method_id = stripe_customer.invoice_settings.default_payment_method)
16
28
  Pay::Stripe::PaymentMethod.sync(payment_method_id, stripe_account: event.try(:account))
@@ -19,14 +31,6 @@ module Pay
19
31
  # No default payment method set
20
32
  pay_customer.payment_methods.update_all(default: false)
21
33
  end
22
-
23
- # Sync invoice credit balance and currency
24
- if stripe_customer.invoice_credit_balance.present?
25
- pay_customer.update(
26
- invoice_credit_balance: stripe_customer.invoice_credit_balance,
27
- currency: stripe_customer.currency
28
- )
29
- end
30
34
  end
31
35
  end
32
36
  end
data/lib/pay/stripe.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  module Pay
2
2
  module Stripe
3
3
  class Error < Pay::Error
4
- delegate :message, to: :cause
5
4
  end
6
5
 
7
6
  module Webhooks
@@ -27,7 +26,7 @@ module Pay
27
26
 
28
27
  extend Env
29
28
 
30
- REQUIRED_VERSION = "~> 13"
29
+ REQUIRED_VERSION = "~> 15"
31
30
 
32
31
  # A list of database model names that include Pay
33
32
  # 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 = "9.0.1"
2
+ VERSION = "10.0.1"
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: 9.0.1
4
+ version: 10.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes
@@ -9,7 +9,7 @@ authors:
9
9
  - Collin Jilbert
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-03-27 00:00:00.000000000 Z
12
+ date: 1980-01-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: 6.0.0
20
+ version: 7.0.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: 6.0.0
27
+ version: 7.0.0
28
28
  description: Stripe, Paddle, and Braintree payments for Ruby on Rails apps
29
29
  email:
30
30
  - jason@thecharnes.com
@@ -99,6 +99,7 @@ files:
99
99
  - config/locales/en.yml
100
100
  - config/routes.rb
101
101
  - db/migrate/1_create_pay_tables.rb
102
+ - db/migrate/20250415151129_add_object_to_pay_models.rb
102
103
  - db/migrate/2_add_pay_sti_columns.rb
103
104
  - lib/generators/pay/email_views_generator.rb
104
105
  - lib/generators/pay/views_generator.rb
@@ -178,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
178
179
  - !ruby/object:Gem::Version
179
180
  version: '0'
180
181
  requirements: []
181
- rubygems_version: 3.6.6
182
+ rubygems_version: 3.6.8
182
183
  specification_version: 4
183
184
  summary: Payments engine for Ruby on Rails
184
185
  test_files: []