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,30 @@
1
+ module Pay
2
+ module Billable
3
+ module SyncCustomer
4
+ # Syncs customer details to the payment processor.
5
+ # This way they're kept in sync and email notifications are
6
+ # always sent to the correct email address after an update.
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ after_update :enqeue_sync_email_job, if: :pay_should_sync_customer?
12
+ end
13
+
14
+ def pay_should_sync_customer?
15
+ try(:saved_change_to_email?)
16
+ end
17
+
18
+ private
19
+
20
+ def enqeue_sync_email_job
21
+ if saved_change_to_email?
22
+ # Queue job to update each payment processor for this customer
23
+ pay_customers.pluck(:id).each do |pay_customer_id|
24
+ CustomerSyncJob.perform_later(id)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,40 +1,42 @@
1
1
  module Pay
2
2
  module Braintree
3
3
  class Billable
4
- attr_reader :billable
4
+ attr_reader :pay_customer
5
5
 
6
6
  delegate :processor_id,
7
7
  :processor_id?,
8
8
  :email,
9
9
  :customer_name,
10
- :card_token,
11
- to: :billable
10
+ :payment_method_token,
11
+ :payment_method_token?,
12
+ to: :pay_customer
12
13
 
13
- def initialize(billable)
14
- @billable = billable
14
+ def initialize(pay_customer)
15
+ @pay_customer = pay_customer
15
16
  end
16
17
 
17
- # Handles Billable#customer
18
+ # Retrieve the Braintree::Customer object
18
19
  #
19
- # Returns Braintree::Customer
20
+ # - If no processor_id is present, creates a Customer.
21
+ # - When 'payment_method_token' is present, it will also set the default payment method
20
22
  def customer
21
23
  if processor_id?
22
24
  customer = gateway.customer.find(processor_id)
23
- update_card card_token if card_token.present?
25
+
26
+ if payment_method_token?
27
+ add_payment_method(payment_method_token, default: true)
28
+ pay_customer.payment_method_token = nil
29
+ end
30
+
24
31
  customer
25
32
  else
26
- result = gateway.customer.create(
27
- email: email,
28
- first_name: try(:first_name),
29
- last_name: try(:last_name),
30
- payment_method_nonce: card_token
31
- )
33
+ result = gateway.customer.create(email: email, first_name: try(:first_name), last_name: try(:last_name), payment_method_nonce: payment_method_token)
32
34
  raise Pay::Braintree::Error, result unless result.success?
35
+ pay_customer.update!(processor_id: result.customer.id)
33
36
 
34
- billable.update(processor: "braintree", processor_id: result.customer.id)
35
-
36
- if card_token.present?
37
- update_card_on_file result.customer.payment_methods.last
37
+ if payment_method_token?
38
+ save_payment_method(result.customer.payment_methods.last, default: true)
39
+ pay_customer.payment_method_token = nil
38
40
  end
39
41
 
40
42
  result.customer
@@ -45,14 +47,12 @@ module Pay
45
47
  raise Pay::Braintree::Error, e
46
48
  end
47
49
 
48
- # Handles Billable#charge
49
- #
50
- # Returns a Pay::Charge
51
50
  def charge(amount, options = {})
52
51
  args = {
53
52
  amount: amount.to_i / 100.0,
54
53
  customer_id: customer.id,
55
- options: {submit_for_settlement: true}
54
+ options: {submit_for_settlement: true},
55
+ custom_fields: options.delete(:metadata)
56
56
  }.merge(options)
57
57
 
58
58
  result = gateway.transaction.sale(args)
@@ -65,9 +65,6 @@ module Pay
65
65
  raise Pay::Braintree::Error, e
66
66
  end
67
67
 
68
- # Handles Billable#subscribe
69
- #
70
- # Returns Pay::Subscription
71
68
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
72
69
  token = customer.payment_methods.find(&:default?).try(:token)
73
70
  raise Pay::Error, "Customer has no default payment method" if token.nil?
@@ -77,6 +74,7 @@ module Pay
77
74
  options.merge!(trial_period: true, trial_duration: trial_period_days, trial_duration_unit: :day)
78
75
  end
79
76
 
77
+ metadata = options.delete(:metadata)
80
78
  subscription_options = options.merge(
81
79
  payment_method_token: token,
82
80
  plan_id: plan
@@ -85,32 +83,44 @@ module Pay
85
83
  result = gateway.subscription.create(subscription_options)
86
84
  raise Pay::Braintree::Error, result unless result.success?
87
85
 
88
- billable.create_pay_subscription(result.subscription, "braintree", name, plan, status: :active)
86
+ pay_customer.subscriptions.create!(
87
+ name: name,
88
+ processor_id: result.subscription.id,
89
+ processor_plan: plan,
90
+ status: :active,
91
+ trial_ends_at: trial_end_date(result.subscription),
92
+ ends_at: nil,
93
+ metadata: metadata
94
+ )
89
95
  rescue ::Braintree::AuthorizationError => e
90
96
  raise Pay::Braintree::AuthorizationError, e
91
97
  rescue ::Braintree::BraintreeError => e
92
98
  raise Pay::Braintree::Error, e
93
99
  end
94
100
 
95
- # Handles Billable#update_card
96
- #
97
- # Returns true if successful
98
- def update_card(token)
101
+ def add_payment_method(token, default: false)
99
102
  customer unless processor_id?
100
103
 
101
104
  result = gateway.payment_method.create(
102
105
  customer_id: processor_id,
103
106
  payment_method_nonce: token,
104
107
  options: {
105
- make_default: true,
108
+ make_default: default,
106
109
  verify_card: true
107
110
  }
108
111
  )
109
112
  raise Pay::Braintree::Error, result unless result.success?
110
113
 
111
- update_card_on_file result.payment_method
112
- update_subscriptions_to_payment_method(result.payment_method.token)
113
- true
114
+ pay_payment_method = save_payment_method(result.payment_method, default: default)
115
+
116
+ # Update existing subscriptions to the new payment method
117
+ pay_customer.subscriptions.each do |subscription|
118
+ if subscription.active?
119
+ gateway.subscription.update(subscription.processor_id, {payment_method_token: token})
120
+ end
121
+ end
122
+
123
+ pay_payment_method
114
124
  rescue ::Braintree::AuthorizationError => e
115
125
  raise Pay::Braintree::AuthorizationError, e
116
126
  rescue ::Braintree::BraintreeError => e
@@ -127,42 +137,26 @@ module Pay
127
137
  subscription.first_billing_date.end_of_day
128
138
  end
129
139
 
130
- def update_subscriptions_to_payment_method(token)
131
- billable.subscriptions.braintree.each do |subscription|
132
- if subscription.active?
133
- gateway.subscription.update(subscription.processor_id, {payment_method_token: token})
134
- end
135
- end
136
- end
137
-
138
140
  def processor_subscription(subscription_id, options = {})
139
141
  gateway.subscription.find(subscription_id)
140
142
  end
141
143
 
142
- def braintree_invoice!(options = {})
143
- # pass
144
- end
145
-
146
- def braintree_upcoming_invoice
147
- # pass
148
- end
149
-
150
144
  def save_transaction(transaction)
151
145
  attrs = card_details_for_braintree_transaction(transaction)
152
146
  attrs[:amount] = transaction.amount.to_f * 100
147
+ attrs[:metadata] = transaction.custom_fields
148
+ attrs[:currency] = transaction.currency_iso_code
149
+ attrs[:application_fee_amount] = transaction.service_fee_amount
153
150
 
154
151
  # Associate charge with subscription if we can
155
152
  if transaction.subscription_id
156
- attrs[:subscription] = Pay::Subscription.find_by(processor: :braintree, processor_id: transaction.subscription_id)
153
+ pay_subscription = pay_customer.subscriptions.find_by(processor_id: transaction.subscription_id)
154
+ attrs[:subscription] = pay_subscription
155
+ attrs[:metadata] = pay_subscription.metadata
157
156
  end
158
157
 
159
- charge = billable.charges.find_or_initialize_by(
160
- processor: :braintree,
161
- processor_id: transaction.id,
162
- currency: transaction.currency_iso_code,
163
- application_fee_amount: transaction.service_fee_amount
164
- )
165
- charge.update(attrs)
158
+ charge = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
159
+ charge.update!(attrs)
166
160
  charge
167
161
  end
168
162
 
@@ -172,74 +166,98 @@ module Pay
172
166
  Pay.braintree_gateway
173
167
  end
174
168
 
175
- def update_card_on_file(payment_method)
176
- case payment_method
177
- when ::Braintree::CreditCard
178
- billable.update!(
179
- card_type: payment_method.card_type,
180
- card_last4: payment_method.last_4,
181
- card_exp_month: payment_method.expiration_month,
182
- card_exp_year: payment_method.expiration_year
183
- )
184
-
185
- when ::Braintree::PayPalAccount
186
- billable.update!(
187
- card_type: "PayPal",
188
- card_last4: payment_method.email
189
- )
169
+ def save_payment_method(payment_method, default:)
170
+ attributes = case payment_method
171
+ when ::Braintree::CreditCard, ::Braintree::ApplePayCard, ::Braintree::GooglePayCard, ::Braintree::SamsungPayCard, ::Braintree::VisaCheckoutCard
172
+ {
173
+ payment_method_type: :card,
174
+ brand: payment_method.card_type,
175
+ last4: payment_method.last_4,
176
+ exp_month: payment_method.expiration_month,
177
+ exp_year: payment_method.expiration_year
178
+ }
179
+
180
+ when ::Braintree::PayPalAccount
181
+ {
182
+ payment_method_type: :paypal,
183
+ brand: "PayPal",
184
+ email: payment_method.email
185
+ }
186
+ when ::Braintree::VenmoAccount
187
+ {
188
+ payment_method_type: :venmo,
189
+ brand: "Venmo",
190
+ username: payment_method.username
191
+ }
192
+ when ::Braintree::UsBankAccount
193
+ {
194
+ payment_method_type: "us_bank_account",
195
+ bank: payment_method.bank_name,
196
+ last4: payment_method.last_4
197
+ }
198
+ else
199
+ {
200
+ payment_method_type: payment_method.class.name.demodulize.underscore
201
+ }
190
202
  end
191
203
 
192
- # Clear the card token so we don't accidentally update twice
193
- billable.card_token = nil
204
+ pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.token).first_or_initialize
205
+
206
+ pay_customer.payment_methods.update_all(default: false) if default
207
+ pay_payment_method.update!(attributes.merge(default: default))
208
+
209
+ # Reload the Rails association
210
+ pay_customer.reload_default_payment_method if default
211
+
212
+ pay_payment_method
194
213
  end
195
214
 
196
215
  def card_details_for_braintree_transaction(transaction)
197
216
  case transaction.payment_instrument_type
198
- when "credit_card", "samsung_pay_card", "masterpass_card", "visa_checkout_card"
199
- payment_method = transaction.send("#{transaction.payment_instrument_type}_details")
200
- {
201
- card_type: payment_method.card_type,
202
- card_last4: payment_method.last_4,
203
- card_exp_month: payment_method.expiration_month,
204
- card_exp_year: payment_method.expiration_year
205
- }
217
+ when "android_pay_card", "apple_pay_card", "credit_card", "google_pay_card", "samsung_pay_card", "visa_checkout_card"
218
+ # Lookup the attribute with the payment method details by name
219
+ attribute_name = transaction.payment_instrument_type
206
220
 
207
- when "paypal_account"
208
- {
209
- card_type: "PayPal",
210
- card_last4: transaction.paypal_details.payer_email,
211
- card_exp_month: nil,
212
- card_exp_year: nil
213
- }
221
+ # The attribute name for Apple and Google Pay don't include _card for some reason
222
+ if ["apple_pay_card", "google_pay_card"].include?(transaction.payment_instrument_type)
223
+ attribute_name = attribute_name.split("_card").first
224
+
225
+ # Android Pay was renamed to Google Pay, but test nonces still use android_pay_card
226
+ elsif attribute_name == "android_pay_card"
227
+ attribute_name = "google_pay"
228
+ end
229
+
230
+ # Retrieve payment method details from transaction
231
+ payment_method = transaction.send("#{attribute_name}_details")
214
232
 
215
- when "android_pay_card"
216
- payment_method = transaction.android_pay_details
217
233
  {
218
- card_type: payment_method.source_card_type,
219
- card_last4: payment_method.source_card_last_4,
220
- card_exp_month: payment_method.expiration_month,
221
- card_exp_year: payment_method.expiration_year
234
+ payment_method_type: :card,
235
+ brand: payment_method.card_type,
236
+ last4: payment_method.last_4,
237
+ exp_month: payment_method.expiration_month,
238
+ exp_year: payment_method.expiration_year
222
239
  }
223
240
 
224
- when "venmo_account"
241
+ when "paypal_account"
225
242
  {
226
- card_type: "Venmo",
227
- card_last4: transaction.venmo_account_details.username,
228
- card_exp_month: nil,
229
- card_exp_year: nil
243
+ payment_method_type: :paypal,
244
+ brand: "PayPal",
245
+ last4: transaction.paypal_details.payer_email,
246
+ exp_month: nil,
247
+ exp_year: nil
230
248
  }
231
249
 
232
- when "apple_pay_card"
233
- payment_method = transaction.apple_pay_details
250
+ when "venmo_account"
234
251
  {
235
- card_type: payment_method.card_type,
236
- card_last4: payment_method.last_4,
237
- card_exp_month: payment_method.expiration_month,
238
- card_exp_year: payment_method.expiration_year
252
+ payment_method_type: :venmo,
253
+ brand: "Venmo",
254
+ last4: transaction.venmo_account_details.username,
255
+ exp_month: nil,
256
+ exp_year: nil
239
257
  }
240
258
 
241
259
  else
242
- {}
260
+ {payment_method_type: "unknown"}
243
261
  end
244
262
  end
245
263
  end
@@ -0,0 +1,33 @@
1
+ module Pay
2
+ module Braintree
3
+ class PaymentMethod
4
+ attr_reader :pay_payment_method
5
+
6
+ delegate :customer, :processor_id, to: :pay_payment_method
7
+
8
+ def initialize(pay_payment_method)
9
+ @pay_payment_method = pay_payment_method
10
+ end
11
+
12
+ # Sets payment method as default on Stripe
13
+ def make_default!
14
+ result = gateway.customer.update(customer.processor_id, default_payment_method_token: processor_id)
15
+ raise Pay::Braintree::Error, result unless result.success?
16
+ result.success?
17
+ end
18
+
19
+ # Remove payment method
20
+ def detach
21
+ result = gateway.payment_method.delete(processor_id)
22
+ raise Pay::Braintree::Error, result unless result.success?
23
+ result.success?
24
+ end
25
+
26
+ private
27
+
28
+ def gateway
29
+ Pay.braintree_gateway
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,11 +4,11 @@ module Pay
4
4
  attr_reader :pay_subscription
5
5
 
6
6
  delegate :active?,
7
+ :customer,
7
8
  :canceled?,
8
9
  :ends_at,
9
10
  :name,
10
11
  :on_trial?,
11
- :owner,
12
12
  :processor_id,
13
13
  :processor_plan,
14
14
  :processor_subscription,
@@ -45,13 +45,13 @@ module Pay
45
45
 
46
46
  def cancel_now!
47
47
  gateway.subscription.cancel(processor_subscription.id)
48
- pay_subscription.update(status: :canceled, ends_at: Time.zone.now)
48
+ pay_subscription.update(status: :canceled, ends_at: Time.current)
49
49
  rescue ::Braintree::BraintreeError => e
50
50
  raise Pay::Braintree::Error, e
51
51
  end
52
52
 
53
53
  def on_grace_period?
54
- canceled? && Time.zone.now < ends_at
54
+ canceled? && Time.current < ends_at
55
55
  end
56
56
 
57
57
  def paused?
@@ -70,7 +70,7 @@ module Pay
70
70
  if canceled? && on_trial?
71
71
  duration = trial_ends_at.to_date - Date.today
72
72
 
73
- owner.subscribe(
73
+ customer.subscribe(
74
74
  name: name,
75
75
  plan: processor_plan,
76
76
  trial_period: true,
@@ -100,7 +100,7 @@ module Pay
100
100
  end
101
101
 
102
102
  unless active?
103
- owner.subscribe(name: name, plan: plan, trial_period: false)
103
+ customer.subscribe(name: name, plan: plan, trial_period: false)
104
104
  return
105
105
  end
106
106
 
@@ -122,12 +122,7 @@ module Pay
122
122
  prorate_charges: prorate?
123
123
  }
124
124
  })
125
-
126
- if result.success?
127
- pay_subscription.update(status: :active, processor_plan: braintree_plan.id, ends_at: nil)
128
- else
129
- raise Error, "Braintree failed to swap plans: #{result.message}"
130
- end
125
+ raise Error, "Braintree failed to swap plans: #{result.message}" unless result.success?
131
126
  rescue ::Braintree::BraintreeError => e
132
127
  raise Pay::Braintree::Error, e
133
128
  end
@@ -207,7 +202,7 @@ module Pay
207
202
 
208
203
  cancel_now!
209
204
 
210
- owner.subscribe(**options.merge(name: name, plan: plan.id))
205
+ customer.subscribe(**options.merge(name: name, plan: plan.id))
211
206
  end
212
207
  end
213
208
  end