pay 2.7.1 → 3.0.2

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -715
  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 +1 -5
  8. data/app/models/pay/charge.rb +54 -17
  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 +42 -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 +108 -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/generators/active_record/templates/billable_migration.rb +1 -0
  21. data/lib/pay/attributes.rb +74 -0
  22. data/lib/pay/billable/sync_customer.rb +30 -0
  23. data/lib/pay/braintree/billable.rb +133 -110
  24. data/lib/pay/braintree/payment_method.rb +42 -0
  25. data/lib/pay/braintree/subscription.rb +9 -12
  26. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
  27. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
  28. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  29. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
  30. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
  31. data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
  32. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
  33. data/lib/pay/braintree.rb +3 -2
  34. data/lib/pay/engine.rb +6 -1
  35. data/lib/pay/fake_processor/billable.rb +45 -21
  36. data/lib/pay/fake_processor/payment_method.rb +21 -0
  37. data/lib/pay/fake_processor/subscription.rb +11 -8
  38. data/lib/pay/fake_processor.rb +2 -1
  39. data/lib/pay/nano_id.rb +13 -0
  40. data/lib/pay/paddle/billable.rb +18 -48
  41. data/lib/pay/paddle/charge.rb +5 -5
  42. data/lib/pay/paddle/payment_method.rb +60 -0
  43. data/lib/pay/paddle/response.rb +0 -0
  44. data/lib/pay/paddle/subscription.rb +49 -8
  45. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
  46. data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
  47. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  48. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +26 -28
  49. data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
  50. data/lib/pay/paddle.rb +7 -3
  51. data/lib/pay/payment.rb +1 -1
  52. data/lib/pay/receipts.rb +35 -7
  53. data/lib/pay/stripe/billable.rb +75 -76
  54. data/lib/pay/stripe/charge.rb +44 -17
  55. data/lib/pay/stripe/merchant.rb +10 -10
  56. data/lib/pay/stripe/payment_method.rb +61 -0
  57. data/lib/pay/stripe/subscription.rb +55 -22
  58. data/lib/pay/stripe/webhooks/account_updated.rb +2 -3
  59. data/lib/pay/stripe/webhooks/charge_refunded.rb +1 -1
  60. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
  61. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +3 -1
  62. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +3 -1
  63. data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
  64. data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
  65. data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
  66. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -8
  67. data/lib/pay/stripe/webhooks/payment_method_attached.rb +15 -0
  68. data/lib/pay/stripe/webhooks/payment_method_detached.rb +12 -0
  69. data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
  70. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
  71. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -1
  72. data/lib/pay/stripe/webhooks/subscription_renewing.rb +12 -2
  73. data/lib/pay/stripe.rb +6 -3
  74. data/lib/pay/version.rb +1 -1
  75. data/lib/pay/webhooks/delegator.rb +4 -0
  76. data/lib/pay/webhooks/process_job.rb +9 -0
  77. data/lib/pay/webhooks.rb +1 -0
  78. data/lib/pay.rb +7 -78
  79. data/lib/tasks/pay.rake +20 -0
  80. metadata +23 -36
  81. data/app/models/pay.rb +0 -5
  82. data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
  83. data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
  84. data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
  85. data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -6
  86. data/db/migrate/20210309004259_add_data_to_pay_billable.rb +0 -10
  87. data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +0 -5
  88. data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +0 -7
  89. data/lib/pay/billable/sync_email.rb +0 -40
  90. data/lib/pay/billable.rb +0 -172
@@ -0,0 +1,60 @@
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
+ return unless pay_customer.subscription
11
+
12
+ payment_method = pay_customer.default_payment_method || pay_customer.build_default_payment_method
13
+ payment_method.processor_id ||= NanoId.generate
14
+
15
+ # Lookup payment method from API unless passed in
16
+ attributes ||= payment_method_details_for(subscription_id: pay_customer.subscription.processor_id)
17
+
18
+ payment_method.update!(attributes)
19
+ payment_method
20
+ rescue ::PaddlePay::PaddlePayError => e
21
+ raise Pay::Paddle::Error, e
22
+ end
23
+
24
+ def self.payment_method_details_for(subscription_id:)
25
+ subscription_user = PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
26
+ payment_information = subscription_user ? subscription_user[:payment_information] : {}
27
+
28
+ case payment_information[:payment_method]
29
+ when "card"
30
+ {
31
+ payment_method_type: :card,
32
+ brand: payment_information[:card_type],
33
+ last4: payment_information[:last_four_digits],
34
+ exp_month: payment_information[:expiry_date].split("/").first,
35
+ exp_year: payment_information[:expiry_date].split("/").last
36
+ }
37
+ when "paypal"
38
+ {
39
+ payment_method_type: :paypal,
40
+ brand: "PayPal"
41
+ }
42
+ else
43
+ {}
44
+ end
45
+ end
46
+
47
+ def initialize(pay_payment_method)
48
+ @pay_payment_method = pay_payment_method
49
+ end
50
+
51
+ # Sets payment method as default
52
+ def make_default!
53
+ end
54
+
55
+ # Remove payment method
56
+ def detach
57
+ end
58
+ end
59
+ end
60
+ 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?
@@ -79,6 +118,8 @@ module Pay
79
118
  end
80
119
 
81
120
  def swap(plan)
121
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
122
+
82
123
  attributes = {plan_id: plan, prorate: prorate}
83
124
  attributes[:quantity] = quantity if quantity?
84
125
  PaddlePay::Subscription::User.update(processor_id, attributes)
@@ -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_account: stripe_account})
27
+ ::Stripe::Customer.retrieve({id: processor_id}, stripe_options)
32
28
  else
33
- sc = ::Stripe::Customer.create({email: email, name: customer_name}, {stripe_account: stripe_account})
34
- billable.update(processor: :stripe, processor_id: sc.id, stripe_account: stripe_account)
29
+ sc = ::Stripe::Customer.create({email: email, name: customer_name}, stripe_options)
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_account: stripe_account})
41
- stripe_customer = ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, {stripe_account: stripe_account})
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
- payment_intent = ::Stripe::PaymentIntent.create(args, {stripe_account: stripe_account})
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,22 +77,18 @@ 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
92
- stripe_sub = ::Stripe::Subscription.create(opts, {stripe_account: stripe_account})
84
+ stripe_sub = ::Stripe::Subscription.create(opts, stripe_options)
93
85
 
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
-
101
- # Trial, card requires SCA
102
- elsif subscription.on_trial? && stripe_sub.pending_setup_intent
103
- Pay::Payment.new(stripe_sub.pending_setup_intent).validate
104
92
  end
105
93
 
106
94
  subscription
@@ -108,51 +96,57 @@ module Pay
108
96
  raise Pay::Stripe::Error, e
109
97
  end
110
98
 
111
- # Handles Billable#update_card
112
- #
113
- # Returns true if successful
114
- def update_card(payment_method_id)
115
- stripe_customer = customer
116
-
117
- 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)
118
102
 
119
- payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: stripe_customer.id}, {stripe_account: stripe_account})
120
- ::Stripe::Customer.update(stripe_customer.id, {invoice_settings: {default_payment_method: payment_method.id}}, {stripe_account: stripe_account})
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
121
110
 
122
- update_card_on_file(payment_method.card)
123
- true
111
+ save_payment_method(payment_method, default: default)
124
112
  rescue ::Stripe::StripeError => e
125
113
  raise Pay::Stripe::Error, e
126
114
  end
127
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
+
128
131
  def update_email!
129
- ::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, {stripe_account: stripe_account})
132
+ ::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, stripe_options)
130
133
  end
131
134
 
132
135
  def processor_subscription(subscription_id, options = {})
133
- ::Stripe::Subscription.retrieve(options.merge(id: subscription_id), {stripe_account: stripe_account})
136
+ ::Stripe::Subscription.retrieve(options.merge(id: subscription_id), stripe_options)
134
137
  end
135
138
 
136
139
  def invoice!(options = {})
137
140
  return unless processor_id?
138
- ::Stripe::Invoice.create(options.merge(customer: processor_id), {stripe_account: stripe_account}).pay
141
+ ::Stripe::Invoice.create(options.merge(customer: processor_id), stripe_options).pay
139
142
  end
140
143
 
141
144
  def upcoming_invoice
142
- ::Stripe::Invoice.upcoming({customer: processor_id}, {stripe_account: stripe_account})
143
- end
144
-
145
- # Used by webhooks when the customer or source changes
146
- def sync_card_from_stripe
147
- if (payment_method_id = customer.invoice_settings.default_payment_method)
148
- update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id, {stripe_account: stripe_account}).card
149
- else
150
- billable.update(card_type: nil, card_last4: nil)
151
- end
145
+ ::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
152
146
  end
153
147
 
154
148
  def create_setup_intent
155
- ::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, {stripe_account: stripe_account})
149
+ ::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, stripe_options)
156
150
  end
157
151
 
158
152
  def trial_end_date(stripe_sub)
@@ -160,16 +154,14 @@ module Pay
160
154
  stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
161
155
  end
162
156
 
163
- # Save the card to the database as the user's current card
164
- def update_card_on_file(card)
165
- billable.update!(
166
- card_type: card.brand.capitalize,
167
- card_last4: card.last4,
168
- card_exp_month: card.exp_month,
169
- card_exp_year: card.exp_year
170
- )
171
-
172
- billable.card_token = nil
157
+ # Syncs a customer's subscriptions from Stripe to the database
158
+ def sync_subscriptions
159
+ subscriptions = ::Stripe::Subscription.list({customer: customer}, stripe_options)
160
+ subscriptions.map do |subscription|
161
+ Pay::Stripe::Subscription.sync(subscription.id)
162
+ end
163
+ rescue ::Stripe::StripeError => e
164
+ raise Pay::Stripe::Error, e
173
165
  end
174
166
 
175
167
  # https://stripe.com/docs/api/checkout/sessions/create
@@ -203,7 +195,7 @@ module Pay
203
195
  }
204
196
  end
205
197
 
206
- ::Stripe::Checkout::Session.create(args.merge(options), {stripe_account: stripe_account})
198
+ ::Stripe::Checkout::Session.create(args.merge(options), stripe_options)
207
199
  end
208
200
 
209
201
  # https://stripe.com/docs/api/checkout/sessions/create
@@ -230,7 +222,14 @@ module Pay
230
222
  customer: processor_id,
231
223
  return_url: options.delete(:return_url) || root_url
232
224
  }
233
- ::Stripe::BillingPortal::Session.create(args.merge(options), {stripe_account: stripe_account})
225
+ ::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
226
+ end
227
+
228
+ private
229
+
230
+ # Options for Stripe requests
231
+ def stripe_options
232
+ {stripe_account: stripe_account}.compact
234
233
  end
235
234
  end
236
235
  end