pay 2.7.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pay might be problematic. Click here for more details.

Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -731
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +10 -3
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +7 -8
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +6 -3
  6. data/app/jobs/pay/{email_sync_job.rb → customer_sync_job.rb} +3 -4
  7. data/app/models/pay/application_record.rb +0 -5
  8. data/app/models/pay/charge.rb +31 -18
  9. data/app/models/pay/customer.rb +87 -0
  10. data/app/models/pay/merchant.rb +19 -0
  11. data/app/models/pay/payment_method.rb +41 -0
  12. data/app/models/pay/subscription.rb +32 -30
  13. data/app/models/pay/webhook.rb +36 -0
  14. data/app/views/layouts/pay/application.html.erb +2 -3
  15. data/app/views/pay/payments/show.html.erb +109 -81
  16. data/app/views/pay/user_mailer/receipt.html.erb +2 -2
  17. data/app/views/pay/user_mailer/refund.html.erb +2 -2
  18. data/config/locales/en.yml +1 -1
  19. data/db/migrate/1_create_pay_tables.rb +72 -0
  20. data/lib/pay/attributes.rb +74 -0
  21. data/lib/pay/billable/sync_customer.rb +30 -0
  22. data/lib/pay/braintree/billable.rb +126 -108
  23. data/lib/pay/braintree/payment_method.rb +33 -0
  24. data/lib/pay/braintree/subscription.rb +7 -12
  25. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
  26. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
  27. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  28. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
  29. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
  30. data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
  31. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
  32. data/lib/pay/braintree.rb +3 -2
  33. data/lib/pay/engine.rb +6 -1
  34. data/lib/pay/fake_processor/billable.rb +45 -21
  35. data/lib/pay/fake_processor/payment_method.rb +21 -0
  36. data/lib/pay/fake_processor/subscription.rb +11 -10
  37. data/lib/pay/fake_processor.rb +2 -1
  38. data/lib/pay/nano_id.rb +13 -0
  39. data/lib/pay/paddle/billable.rb +18 -48
  40. data/lib/pay/paddle/charge.rb +5 -5
  41. data/lib/pay/paddle/payment_method.rb +58 -0
  42. data/lib/pay/paddle/response.rb +0 -0
  43. data/lib/pay/paddle/subscription.rb +47 -8
  44. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
  45. data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
  46. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  47. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +26 -28
  48. data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
  49. data/lib/pay/paddle.rb +7 -3
  50. data/lib/pay/payment.rb +1 -1
  51. data/lib/pay/receipts.rb +35 -7
  52. data/lib/pay/stripe/billable.rb +50 -64
  53. data/lib/pay/stripe/charge.rb +18 -15
  54. data/lib/pay/stripe/merchant.rb +10 -10
  55. data/lib/pay/stripe/payment_method.rb +61 -0
  56. data/lib/pay/stripe/subscription.rb +22 -17
  57. data/lib/pay/stripe/webhooks/account_updated.rb +2 -3
  58. data/lib/pay/stripe/webhooks/charge_refunded.rb +1 -1
  59. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
  60. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +3 -1
  61. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +3 -1
  62. data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
  63. data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
  64. data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
  65. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -8
  66. data/lib/pay/stripe/webhooks/payment_method_attached.rb +2 -4
  67. data/lib/pay/stripe/webhooks/payment_method_detached.rb +1 -6
  68. data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
  69. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
  70. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -1
  71. data/lib/pay/stripe/webhooks/subscription_renewing.rb +12 -2
  72. data/lib/pay/stripe.rb +6 -3
  73. data/lib/pay/version.rb +1 -1
  74. data/lib/pay/webhooks/delegator.rb +4 -0
  75. data/lib/pay/webhooks/process_job.rb +9 -0
  76. data/lib/pay/webhooks.rb +1 -0
  77. data/lib/pay.rb +7 -78
  78. metadata +20 -37
  79. data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
  80. data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
  81. data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
  82. data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -6
  83. data/db/migrate/20210309004259_add_data_to_pay_billable.rb +0 -10
  84. data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +0 -5
  85. data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +0 -7
  86. data/db/migrate/20210714175351_add_uniqueness_to_pay_models.rb +0 -6
  87. data/lib/pay/billable/sync_email.rb +0 -40
  88. data/lib/pay/billable.rb +0 -172
  89. data/lib/pay/stripe/webhooks/payment_method_automatically_updated.rb +0 -17
@@ -0,0 +1,58 @@
1
+ module Pay
2
+ module Paddle
3
+ class PaymentMethod
4
+ attr_reader :pay_payment_method
5
+
6
+ delegate :customer, :processor_id, to: :pay_payment_method
7
+
8
+ # Paddle doesn't provide PaymentMethod IDs, so we have to lookup via the Customer
9
+ def self.sync(pay_customer:, attributes: nil)
10
+ payment_method = pay_customer.default_payment_method || pay_customer.build_default_payment_method
11
+ payment_method.processor_id ||= NanoId.generate
12
+
13
+ # Lookup payment method from API unless passed in
14
+ attributes ||= payment_method_details_for(subscription_id: pay_customer.subscription.processor_id)
15
+
16
+ payment_method.update!(attributes)
17
+ payment_method
18
+ rescue ::PaddlePay::PaddlePayError => e
19
+ raise Pay::Paddle::Error, e
20
+ end
21
+
22
+ def self.payment_method_details_for(subscription_id:)
23
+ subscription_user = PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
24
+ payment_information = subscription_user ? subscription_user[:payment_information] : {}
25
+
26
+ case payment_information[:payment_method]
27
+ when "card"
28
+ {
29
+ payment_method_type: :card,
30
+ brand: payment_information[:card_type],
31
+ last4: payment_information[:last_four_digits],
32
+ exp_month: payment_information[:expiry_date].split("/").first,
33
+ exp_year: payment_information[:expiry_date].split("/").last
34
+ }
35
+ when "paypal"
36
+ {
37
+ payment_method_type: :paypal,
38
+ brand: "PayPal"
39
+ }
40
+ else
41
+ {}
42
+ end
43
+ end
44
+
45
+ def initialize(pay_payment_method)
46
+ @pay_payment_method = pay_payment_method
47
+ end
48
+
49
+ # Sets payment method as default
50
+ def make_default!
51
+ end
52
+
53
+ # Remove payment method
54
+ def detach
55
+ end
56
+ end
57
+ end
58
+ end
File without changes
@@ -20,6 +20,49 @@ module Pay
20
20
  :trial_ends_at,
21
21
  to: :pay_subscription
22
22
 
23
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
24
+ # Passthrough is not return from this API, so we can't use that
25
+ object ||= OpenStruct.new PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
26
+
27
+ pay_customer = Pay::Customer.find_by(processor: :paddle, processor_id: object.user_id)
28
+
29
+ # If passthrough exists (only on webhooks) we can use it to create the Pay::Customer
30
+ if pay_customer.nil? && object.passthrough
31
+ owner = Pay::Paddle.owner_from_passthrough(object.passthrough)
32
+ pay_customer = owner&.set_payment_processor(:paddle, processor_id: object.user_id)
33
+ end
34
+
35
+ return unless pay_customer
36
+
37
+ attributes = {
38
+ paddle_cancel_url: object.cancel_url,
39
+ paddle_update_url: object.update_url,
40
+ processor_plan: object.plan_id || object.subscription_plan_id,
41
+ quantity: object.quantity,
42
+ status: object.state || object.status
43
+ }
44
+
45
+ # If paused or delete while on trial, set ends_at to match
46
+ case attributes[:status]
47
+ when "trialing"
48
+ attributes[:trial_ends_at] = Time.zone.parse(object.next_bill_date)
49
+ attributes[:ends_at] = nil
50
+ when "paused", "deleted"
51
+ attributes[:trial_ends_at] = nil
52
+ attributes[:ends_at] = Time.zone.parse(object.next_bill_date)
53
+ end
54
+
55
+ # Update or create the subscription
56
+ if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.subscription_id))
57
+ pay_subscription.with_lock do
58
+ pay_subscription.update!(attributes)
59
+ end
60
+ pay_subscription
61
+ else
62
+ pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.subscription_id))
63
+ end
64
+ end
65
+
23
66
  def initialize(pay_subscription)
24
67
  @pay_subscription = pay_subscription
25
68
  end
@@ -32,26 +75,22 @@ module Pay
32
75
  end
33
76
 
34
77
  def cancel
35
- subscription = processor_subscription
78
+ ends_at = on_trial? ? trial_ends_at : processor_subscription.next_payment[:date]
36
79
  PaddlePay::Subscription::User.cancel(processor_id)
37
- if on_trial?
38
- pay_subscription.update(status: :canceled, ends_at: trial_ends_at)
39
- else
40
- pay_subscription.update(status: :canceled, ends_at: Time.zone.parse(subscription.next_payment[:date]))
41
- end
80
+ pay_subscription.update(status: :canceled, ends_at: ends_at)
42
81
  rescue ::PaddlePay::PaddlePayError => e
43
82
  raise Pay::Paddle::Error, e
44
83
  end
45
84
 
46
85
  def cancel_now!
47
86
  PaddlePay::Subscription::User.cancel(processor_id)
48
- pay_subscription.update(status: :canceled, ends_at: Time.zone.now)
87
+ pay_subscription.update(status: :canceled, ends_at: Time.current)
49
88
  rescue ::PaddlePay::PaddlePayError => e
50
89
  raise Pay::Paddle::Error, e
51
90
  end
52
91
 
53
92
  def on_grace_period?
54
- canceled? && Time.zone.now < ends_at || paused? && Time.zone.now < paddle_paused_from
93
+ canceled? && Time.current < ends_at || paused? && Time.current < paddle_paused_from
55
94
  end
56
95
 
57
96
  def paused?
@@ -3,14 +3,17 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionCancelled
5
5
  def call(event)
6
- subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
6
+ pay_subscription = Pay::Subscription.find_by_processor_and_id(:paddle, event.subscription_id)
7
7
 
8
8
  # We couldn't find the subscription for some reason, maybe it's from another service
9
- return if subscription.nil?
9
+ return if pay_subscription.nil?
10
10
 
11
11
  # User canceled subscriptions have an ends_at
12
12
  # Automatically canceled subscriptions need this value set
13
- subscription.update!(ends_at: Time.zone.parse(event["cancellation_effective_date"])) if subscription.ends_at.blank? && event["cancellation_effective_date"].present?
13
+ pay_subscription.update!(ends_at: Time.zone.parse(event.cancellation_effective_date)) if pay_subscription.ends_at.blank? && event.cancellation_effective_date.present?
14
+
15
+ # Paddle doesn't allow reusing customers, so we should remove their payment methods
16
+ pay_subscription.customer.payment_methods.destroy_all
14
17
  end
15
18
  end
16
19
  end
@@ -3,46 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionCreated
5
5
  def call(event)
6
- # We may already have the subscription in the database, so we can update that record
7
- subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
8
-
9
- # Create the subscription in the database if we don't have it already
10
- if subscription.nil?
11
-
12
- # The customer could already be in the database
13
- owner = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
14
-
15
- if owner.nil?
16
- owner = Pay::Paddle.owner_from_passthrough(event["passthrough"])
17
- owner&.update!(processor: "paddle", processor_id: event["user_id"])
18
- end
19
-
20
- if owner.nil?
21
- Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{event["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
22
- return
23
- end
24
-
25
- subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id: event["subscription_id"], status: :active)
26
- end
27
-
28
- subscription.quantity = event["quantity"]
29
- subscription.processor_plan = event["subscription_plan_id"]
30
- subscription.paddle_update_url = event["update_url"]
31
- subscription.paddle_cancel_url = event["cancel_url"]
32
- subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"]) if event["status"] == "trialing"
33
-
34
- # If user was on trial, their subscription ends at the end of the trial
35
- subscription.ends_at = if ["paused", "deleted"].include?(event["status"]) && subscription.on_trial?
36
- subscription.trial_ends_at
37
-
38
- # User wasn't on trial, so subscription ends at period end
39
- elsif ["paused", "deleted"].include?(event["status"])
40
- Time.zone.parse(event["next_bill_date"])
41
-
42
- # Subscription isn't marked to cancel at period end
43
- end
44
-
45
- subscription.save!
6
+ Pay::Paddle::Subscription.sync(event.subscription_id, object: event)
46
7
  end
47
8
  end
48
9
  end
@@ -3,11 +3,11 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionPaymentRefunded
5
5
  def call(event)
6
- charge = Pay.charge_model.find_by(processor: :paddle, processor_id: event["subscription_payment_id"])
6
+ charge = Pay::Charge.find_by_processor_and_id(:paddle, event.subscription_payment_id)
7
7
  return unless charge.present?
8
8
 
9
- charge.update(amount_refunded: Integer(event["gross_refund"].to_f * 100))
10
- notify_user(charge.owner, charge)
9
+ charge.update(amount_refunded: (event.gross_refund.to_f * 100).to_i)
10
+ notify_user(charge.customer.owner, charge)
11
11
  end
12
12
 
13
13
  def notify_user(billable, charge)
@@ -3,43 +3,41 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionPaymentSucceeded
5
5
  def call(event)
6
- billable = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
6
+ pay_customer = Pay::Customer.find_by(processor: :paddle, processor_id: event.user_id)
7
7
 
8
- if billable.nil?
9
- billable = Pay::Paddle.owner_from_passthrough(event["passthrough"])
10
- billable&.update!(processor: "paddle", processor_id: event["user_id"])
8
+ if pay_customer.nil?
9
+ owner = Pay::Paddle.owner_from_passthrough(event.passthrough)
10
+ pay_customer = owner&.set_payment_processor :paddle, processor_id: event.user_id
11
11
  end
12
12
 
13
- if billable.nil?
14
- Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{event["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
13
+ if pay_customer.nil?
14
+ Rails.logger.error("[Pay] Unable to find Pay::Customer with: '#{event.passthrough}'")
15
15
  return
16
16
  end
17
17
 
18
- return if billable.charges.where(processor_id: event["subscription_payment_id"]).any?
18
+ return if pay_customer.charges.where(processor_id: event.subscription_payment_id).any?
19
19
 
20
- charge = create_charge(billable, event)
21
- notify_user(billable, charge)
20
+ charge = create_charge(pay_customer, event)
21
+ notify_user(pay_customer.owner, charge)
22
22
  end
23
23
 
24
- def create_charge(user, event)
25
- charge = user.charges.find_or_initialize_by(
26
- processor: :paddle,
27
- processor_id: event["subscription_payment_id"]
28
- )
29
-
30
- params = {
31
- amount: Integer(event["sale_gross"].to_f * 100),
32
- card_type: event["payment_method"],
33
- created_at: Time.zone.parse(event["event_time"]),
34
- currency: event["currency"],
35
- paddle_receipt_url: event["receipt_url"],
36
- subscription: Pay::Subscription.find_by(processor: :paddle, processor_id: event["subscription_id"])
37
- }
38
-
39
- payment_information = Pay::Paddle::Billable.new(user).payment_information(event["subscription_id"])
40
-
41
- charge.update(params.merge(payment_information))
42
- user.update(payment_information)
24
+ def create_charge(pay_customer, event)
25
+ payment_method_details = Pay::Paddle::PaymentMethod.payment_method_details_for(subscription_id: event.subscription_id)
26
+
27
+ attributes = {
28
+ amount: (event.sale_gross.to_f * 100).to_i,
29
+ created_at: Time.zone.parse(event.event_time),
30
+ currency: event.currency,
31
+ paddle_receipt_url: event.receipt_url,
32
+ subscription: pay_customer.subscriptions.find_by(processor_id: event.subscription_id),
33
+ metadata: Pay::Paddle.parse_passthrough(event.passthrough).except("owner_sgid")
34
+ }.merge(payment_method_details)
35
+
36
+ charge = pay_customer.charges.find_or_initialize_by(processor_id: event.subscription_payment_id)
37
+ charge.update!(attributes)
38
+
39
+ # Update customer's payment method
40
+ Pay::Paddle::PaymentMethod.sync(pay_customer: pay_customer, attributes: payment_method_details)
43
41
 
44
42
  charge
45
43
  end
@@ -3,14 +3,14 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionUpdated
5
5
  def call(event)
6
- subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
6
+ subscription = Pay::Subscription.find_by_processor_and_id(:paddle, event["subscription_id"])
7
7
 
8
8
  return if subscription.nil?
9
9
 
10
10
  case event["status"]
11
11
  when "deleted"
12
12
  subscription.status = "canceled"
13
- subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.zone.now if subscription.ends_at.blank?
13
+ subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.current if subscription.ends_at.blank?
14
14
  when "trialing"
15
15
  subscription.status = "trialing"
16
16
  subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
data/lib/pay/paddle.rb CHANGED
@@ -2,8 +2,9 @@ module Pay
2
2
  module Paddle
3
3
  autoload :Billable, "pay/paddle/billable"
4
4
  autoload :Charge, "pay/paddle/charge"
5
- autoload :Subscription, "pay/paddle/subscription"
6
5
  autoload :Error, "pay/paddle/error"
6
+ autoload :PaymentMethod, "pay/paddle/payment_method"
7
+ autoload :Subscription, "pay/paddle/subscription"
7
8
 
8
9
  module Webhooks
9
10
  autoload :SignatureVerifier, "pay/paddle/webhooks/signature_verifier"
@@ -44,9 +45,12 @@ module Pay
44
45
  options.merge(owner_sgid: owner.to_sgid.to_s).to_json
45
46
  end
46
47
 
48
+ def self.parse_passthrough(passthrough)
49
+ JSON.parse(passthrough)
50
+ end
51
+
47
52
  def self.owner_from_passthrough(passthrough)
48
- passthrough_json = JSON.parse(passthrough)
49
- GlobalID::Locator.locate_signed(passthrough_json["owner_sgid"])
53
+ GlobalID::Locator.locate_signed parse_passthrough(passthrough)["owner_sgid"]
50
54
  rescue JSON::ParserError
51
55
  nil
52
56
  end
data/lib/pay/payment.rb CHANGED
@@ -2,7 +2,7 @@ module Pay
2
2
  class Payment
3
3
  attr_reader :intent
4
4
 
5
- delegate :id, :amount, :client_secret, :status, :confirm, to: :intent
5
+ delegate :id, :amount, :client_secret, :customer, :status, :confirm, to: :intent
6
6
 
7
7
  def self.from_id(id)
8
8
  intent = id.start_with?("seti_") ? ::Stripe::SetupIntent.retrieve(id) : ::Stripe::PaymentIntent.retrieve(id)
data/lib/pay/receipts.rb CHANGED
@@ -1,14 +1,14 @@
1
1
  module Pay
2
2
  module Receipts
3
- def filename
4
- "receipt-#{created_at.strftime("%Y-%m-%d")}.pdf"
5
- end
6
-
7
3
  def product
8
4
  Pay.application_name
9
5
  end
10
6
 
11
- # Must return a file object
7
+ def receipt_filename
8
+ "receipt-#{created_at.strftime("%Y-%m-%d")}.pdf"
9
+ end
10
+ alias_method :filename, :receipt_filename
11
+
12
12
  def receipt
13
13
  receipt_pdf.render
14
14
  end
@@ -26,15 +26,43 @@ module Pay
26
26
  )
27
27
  end
28
28
 
29
+ def invoice_filename
30
+ "invoice-#{created_at.strftime("%Y-%m-%d")}.pdf"
31
+ end
32
+
33
+ def invoice
34
+ invoice_pdf.render
35
+ end
36
+
37
+ def invoice_pdf
38
+ ::Receipts::Invoice.new(
39
+ id: id,
40
+ issue_date: created_at,
41
+ due_date: created_at,
42
+ status: "<b><color rgb='#5eba7d'>PAID</color></b>",
43
+ bill_to: [
44
+ customer.customer_name,
45
+ customer.email
46
+ ].compact,
47
+ product: product,
48
+ company: {
49
+ name: Pay.business_name,
50
+ address: Pay.business_address,
51
+ email: Pay.support_email
52
+ },
53
+ line_items: line_items
54
+ )
55
+ end
56
+
29
57
  def line_items
30
58
  line_items = [
31
59
  [I18n.t("receipt.date"), created_at.to_s],
32
- [I18n.t("receipt.account_billed"), "#{owner.name} (#{owner.email})"],
60
+ [I18n.t("receipt.account_billed"), "#{customer.customer_name} (#{customer.email})"],
33
61
  [I18n.t("receipt.product"), product],
34
62
  [I18n.t("receipt.amount"), ActionController::Base.helpers.number_to_currency(amount / 100.0)],
35
63
  [I18n.t("receipt.charged_to"), charged_to]
36
64
  ]
37
- line_items << [I18n.t("receipt.additional_info"), owner.extra_billing_info] if owner.extra_billing_info?
65
+ line_items << [I18n.t("receipt.additional_info"), customer.owner.extra_billing_info] if customer.owner.extra_billing_info?
38
66
  line_items
39
67
  end
40
68
  end
@@ -3,43 +3,40 @@ module Pay
3
3
  class Billable
4
4
  include Rails.application.routes.url_helpers
5
5
 
6
- attr_reader :billable
6
+ attr_reader :pay_customer
7
7
 
8
8
  delegate :processor_id,
9
9
  :processor_id?,
10
10
  :email,
11
11
  :customer_name,
12
- :card_token,
12
+ :payment_method_token,
13
+ :payment_method_token?,
13
14
  :stripe_account,
14
- to: :billable
15
+ to: :pay_customer
15
16
 
16
- class << self
17
- def default_url_options
18
- Rails.application.config.action_mailer.default_url_options || {}
19
- end
17
+ def self.default_url_options
18
+ Rails.application.config.action_mailer.default_url_options || {}
20
19
  end
21
20
 
22
- def initialize(billable)
23
- @billable = billable
21
+ def initialize(pay_customer)
22
+ @pay_customer = pay_customer
24
23
  end
25
24
 
26
- # Handles Billable#customer
27
- #
28
- # Returns Stripe::Customer
29
25
  def customer
30
26
  stripe_customer = if processor_id?
31
- ::Stripe::Customer.retrieve(processor_id, stripe_options)
27
+ ::Stripe::Customer.retrieve({id: processor_id}, stripe_options)
32
28
  else
33
29
  sc = ::Stripe::Customer.create({email: email, name: customer_name}, stripe_options)
34
- billable.update(processor: :stripe, processor_id: sc.id, stripe_account: stripe_account)
30
+ pay_customer.update!(processor_id: sc.id, stripe_account: stripe_account)
35
31
  sc
36
32
  end
37
33
 
38
- # Update the user's card on file if a token was passed in
39
- if card_token.present?
40
- payment_method = ::Stripe::PaymentMethod.attach(card_token, {customer: stripe_customer.id}, stripe_options)
41
- stripe_customer = ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, stripe_options)
42
- update_card_on_file(payment_method.card)
34
+ if payment_method_token?
35
+ payment_method = ::Stripe::PaymentMethod.attach(payment_method_token, {customer: stripe_customer.id}, stripe_options)
36
+ pay_payment_method = save_payment_method(payment_method, default: false)
37
+ pay_payment_method.make_default!
38
+
39
+ pay_customer.payment_method_token = nil
43
40
  end
44
41
 
45
42
  stripe_customer
@@ -47,37 +44,32 @@ module Pay
47
44
  raise Pay::Stripe::Error, e
48
45
  end
49
46
 
50
- # Handles Billable#charge
51
- #
52
- # Returns Pay::Charge
53
47
  def charge(amount, options = {})
54
- stripe_customer = customer
48
+ add_payment_method(payment_method_token, default: true) if payment_method_token?
49
+
50
+ payment_method = pay_customer.default_payment_method
55
51
  args = {
56
52
  amount: amount,
57
53
  confirm: true,
58
54
  confirmation_method: :automatic,
59
55
  currency: "usd",
60
- customer: stripe_customer.id,
61
- payment_method: stripe_customer.invoice_settings.default_payment_method
56
+ customer: processor_id,
57
+ payment_method: payment_method&.processor_id
62
58
  }.merge(options)
63
59
 
64
60
  payment_intent = ::Stripe::PaymentIntent.create(args, stripe_options)
65
61
  Pay::Payment.new(payment_intent).validate
66
62
 
67
- # Create a new charge object
68
63
  charge = payment_intent.charges.first
69
64
  Pay::Stripe::Charge.sync(charge.id, object: charge)
70
65
  rescue ::Stripe::StripeError => e
71
66
  raise Pay::Stripe::Error, e
72
67
  end
73
68
 
74
- # Handles Billable#subscribe
75
- #
76
- # Returns Pay::Subscription
77
69
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
78
70
  quantity = options.delete(:quantity) || 1
79
71
  opts = {
80
- expand: ["pending_setup_intent", "latest_invoice.payment_intent"],
72
+ expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge.invoice"],
81
73
  items: [plan: plan, quantity: quantity],
82
74
  off_session: true
83
75
  }.merge(options)
@@ -85,7 +77,7 @@ module Pay
85
77
  # Inherit trial from plan unless trial override was specified
86
78
  opts[:trial_from_plan] = true unless opts[:trial_period_days]
87
79
 
88
- # Load the Stripe customer to verify it exists and update card if needed
80
+ # Load the Stripe customer to verify it exists and update payment method if needed
89
81
  opts[:customer] = customer.id
90
82
 
91
83
  # Create subscription on Stripe
@@ -94,7 +86,7 @@ module Pay
94
86
  # Save Pay::Subscription
95
87
  subscription = Pay::Stripe::Subscription.sync(stripe_sub.id, object: stripe_sub, name: name)
96
88
 
97
- # No trial, card requires SCA
89
+ # No trial, payment method requires SCA
98
90
  if subscription.incomplete?
99
91
  Pay::Payment.new(stripe_sub.latest_invoice.payment_intent).validate
100
92
  end
@@ -104,23 +96,38 @@ module Pay
104
96
  raise Pay::Stripe::Error, e
105
97
  end
106
98
 
107
- # Handles Billable#update_card
108
- #
109
- # Returns true if successful
110
- def update_card(payment_method_id)
111
- stripe_customer = customer
112
-
113
- return true if payment_method_id == stripe_customer.invoice_settings.default_payment_method
99
+ def add_payment_method(payment_method_id, default: false)
100
+ customer unless processor_id?
101
+ payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: processor_id}, stripe_options)
114
102
 
115
- payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: stripe_customer.id}, stripe_options)
116
- ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, stripe_options)
103
+ if default
104
+ ::Stripe::Customer.update(processor_id, {
105
+ invoice_settings: {
106
+ default_payment_method: payment_method.id
107
+ }
108
+ }, stripe_options)
109
+ end
117
110
 
118
- update_card_on_file(payment_method.card)
119
- true
111
+ save_payment_method(payment_method, default: default)
120
112
  rescue ::Stripe::StripeError => e
121
113
  raise Pay::Stripe::Error, e
122
114
  end
123
115
 
116
+ # Save the Stripe::PaymentMethod to the database
117
+ def save_payment_method(payment_method, default:)
118
+ pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.id).first_or_initialize
119
+
120
+ attributes = Pay::Stripe::PaymentMethod.extract_attributes(payment_method).merge(default: default)
121
+
122
+ pay_customer.payment_methods.update_all(default: false) if default
123
+ pay_payment_method.update!(attributes)
124
+
125
+ # Reload the Rails association
126
+ pay_customer.reload_default_payment_method if default
127
+
128
+ pay_payment_method
129
+ end
130
+
124
131
  def update_email!
125
132
  ::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, stripe_options)
126
133
  end
@@ -138,15 +145,6 @@ module Pay
138
145
  ::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
139
146
  end
140
147
 
141
- # Used by webhooks when the customer or source changes
142
- def sync_card_from_stripe
143
- if (payment_method_id = customer.invoice_settings.default_payment_method)
144
- update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id, stripe_options).card
145
- else
146
- billable.update(card_type: nil, card_last4: nil)
147
- end
148
- end
149
-
150
148
  def create_setup_intent
151
149
  ::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, stripe_options)
152
150
  end
@@ -156,18 +154,6 @@ module Pay
156
154
  stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
157
155
  end
158
156
 
159
- # Save the card to the database as the user's current card
160
- def update_card_on_file(card)
161
- billable.update!(
162
- card_type: card.brand.capitalize,
163
- card_last4: card.last4,
164
- card_exp_month: card.exp_month,
165
- card_exp_year: card.exp_year
166
- )
167
-
168
- billable.card_token = nil
169
- end
170
-
171
157
  # Syncs a customer's subscriptions from Stripe to the database
172
158
  def sync_subscriptions
173
159
  subscriptions = ::Stripe::Subscription.list({customer: customer}, stripe_options)