pay 3.0.23 → 4.0.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +1 -1
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +1 -1
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +1 -1
  6. data/app/jobs/pay/customer_sync_job.rb +1 -3
  7. data/app/mailers/pay/application_mailer.rb +1 -1
  8. data/app/mailers/pay/user_mailer.rb +3 -3
  9. data/app/models/pay/charge.rb +23 -0
  10. data/app/models/pay/customer.rb +2 -6
  11. data/app/models/pay/merchant.rb +6 -0
  12. data/app/models/pay/subscription.rb +35 -8
  13. data/app/views/pay/user_mailer/receipt.html.erb +6 -6
  14. data/app/views/pay/user_mailer/refund.html.erb +6 -6
  15. data/config/locales/en.yml +31 -24
  16. data/config/routes.rb +3 -3
  17. data/lib/pay/attributes.rb +28 -2
  18. data/lib/pay/billable/sync_customer.rb +3 -3
  19. data/lib/pay/braintree/billable.rb +61 -48
  20. data/lib/pay/braintree/subscription.rb +8 -3
  21. data/lib/pay/braintree/webhooks/subscription_canceled.rb +6 -1
  22. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +2 -2
  23. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  24. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -1
  25. data/lib/pay/braintree.rb +6 -2
  26. data/lib/pay/currency.rb +8 -2
  27. data/lib/pay/engine.rb +22 -4
  28. data/lib/pay/fake_processor/billable.rb +11 -8
  29. data/lib/pay/fake_processor/subscription.rb +11 -3
  30. data/lib/pay/paddle/billable.rb +0 -4
  31. data/lib/pay/paddle/subscription.rb +2 -2
  32. data/lib/pay/paddle/webhooks/signature_verifier.rb +45 -41
  33. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +7 -2
  34. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  35. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +7 -7
  36. data/lib/pay/paddle/webhooks/subscription_updated.rb +15 -15
  37. data/lib/pay/paddle.rb +14 -2
  38. data/lib/pay/receipts.rb +106 -32
  39. data/lib/pay/stripe/billable.rb +58 -21
  40. data/lib/pay/stripe/charge.rb +93 -11
  41. data/lib/pay/stripe/merchant.rb +1 -1
  42. data/lib/pay/stripe/subscription.rb +132 -30
  43. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
  44. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
  45. data/lib/pay/stripe/webhooks/payment_action_required.rb +6 -6
  46. data/lib/pay/stripe/webhooks/subscription_renewing.rb +6 -10
  47. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -1
  48. data/lib/pay/stripe.rb +10 -1
  49. data/lib/pay/version.rb +1 -1
  50. data/lib/pay.rb +39 -4
  51. data/lib/tasks/pay.rake +1 -1
  52. metadata +3 -4
  53. data/lib/pay/merchant.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5755d6421fc4b0366728f278b32de2f9f1bf6186cafe4c62cf9c5a188204d0d6
4
- data.tar.gz: 5a4cf13ffbdf85017401dd9aca4ddc3d7fd48fea249868ff10c04bfa383e39d1
3
+ metadata.gz: '09c79f10c2e9258ebfd7a54a58283d0d14eb33121b5f51f4a5b563088f672d41'
4
+ data.tar.gz: 14af03dc31b7b65f9975c90876d1ffb764009e3cae1038abe2bce37822901c49
5
5
  SHA512:
6
- metadata.gz: 966be7e8c643bb6bfa65f21d111d76e35d461a500630434f6b37907c9001f823ae3aa7193f763a7b0af76ef35806de91bd8acfaff1eef219972c8284a96835d3
7
- data.tar.gz: b0dd861df1aa7cb64ab45b267de205251e4a8885a113219a0eb5c3d4fe087332fae129ca7fa1b619d7b75aef6a53698f1665d977c52a472a3d31bce90ff255d5
6
+ metadata.gz: bb583c78584c50bbf21d8d15f7a97449073864cb7064c6ae265639d3f75a97d528868d9db1b2b5d4a8e4e1036d19dd55a4ecdd49f96b9f24149f8e38c4cd4eab
7
+ data.tar.gz: 95f41a1f851f09d9dae598a7d2f795668f4c1b5f28b0fec2ac05e7aed1fd95ad5aefdc5f1fab28f4cbf9a16b6385d55d40bb0f78562906335d8373eb4d76cdee
data/README.md CHANGED
@@ -49,7 +49,7 @@ Want to add a new payment provider? Contributions are welcome.
49
49
  * **Marketplaces**
50
50
  * [Stripe Connect](docs/marketplaces/stripe_connect.md)
51
51
  * **Contributing**
52
- * [Adding A Payment Processor](docs/contributing/8_adding_a_payment_processor.md)
52
+ * [Adding A Payment Processor](docs/contributing/adding_a_payment_processor.md)
53
53
 
54
54
  ## 🙏 Contributing
55
55
 
@@ -17,7 +17,7 @@ module Pay
17
17
  def queue_event(event)
18
18
  return unless Pay::Webhooks.delegator.listening?("braintree.#{event.kind}")
19
19
 
20
- record = Pay::Webhook.create(
20
+ record = Pay::Webhook.create!(
21
21
  processor: :braintree,
22
22
  event_type: event.kind,
23
23
  event: {bt_signature: params[:bt_signature], bt_payload: params[:bt_payload]}
@@ -17,7 +17,7 @@ module Pay
17
17
  def queue_event(event)
18
18
  return unless Pay::Webhooks.delegator.listening?("paddle.#{params[:alert_name]}")
19
19
 
20
- record = Pay::Webhook.create(processor: :paddle, event_type: params[:alert_name], event: event)
20
+ record = Pay::Webhook.create!(processor: :paddle, event_type: params[:alert_name], event: event)
21
21
  Pay::Webhooks::ProcessJob.perform_later(record)
22
22
  end
23
23
 
@@ -18,7 +18,7 @@ module Pay
18
18
  def queue_event(event)
19
19
  return unless Pay::Webhooks.delegator.listening?("stripe.#{event.type}")
20
20
 
21
- record = Pay::Webhook.create(processor: :stripe, event_type: event.type, event: event)
21
+ record = Pay::Webhook.create!(processor: :stripe, event_type: event.type, event: event)
22
22
  Pay::Webhooks::ProcessJob.perform_later(record)
23
23
  end
24
24
 
@@ -1,11 +1,9 @@
1
1
  module Pay
2
2
  class CustomerSyncJob < ApplicationJob
3
- queue_as :default
4
-
5
3
  def perform(pay_customer_id)
6
4
  Pay::Customer.find(pay_customer_id).update_customer!
7
5
  rescue ActiveRecord::RecordNotFound
8
- Rails.logger.info "Couldn't find a Pay::Customer with ID = #{id}"
6
+ Rails.logger.info "Couldn't find a Pay::Customer with ID = #{pay_customer_id}"
9
7
  end
10
8
  end
11
9
  end
@@ -1,6 +1,6 @@
1
1
  module Pay
2
2
  class ApplicationMailer < ActionMailer::Base
3
- default from: Pay.support_email
3
+ default from: Pay.support_email || ApplicationMailer.default_params[:from]
4
4
  layout "mailer"
5
5
  end
6
6
  end
@@ -1,8 +1,8 @@
1
1
  module Pay
2
- class UserMailer < ApplicationMailer
2
+ class UserMailer < Pay.parent_mailer.constantize
3
3
  def receipt
4
- if params[:charge].respond_to? :receipt
5
- attachments[params[:charge].filename] = params[:charge].receipt
4
+ if params[:pay_charge].respond_to? :receipt
5
+ attachments[params[:pay_charge].filename] = params[:pay_charge].receipt
6
6
  end
7
7
 
8
8
  mail to: to
@@ -17,6 +17,7 @@ module Pay
17
17
 
18
18
  # Store the payment method kind (card, paypal, etc)
19
19
  store_accessor :data, :paddle_receipt_url
20
+ store_accessor :data, :stripe_receipt_url
20
21
  store_accessor :data, :stripe_account
21
22
 
22
23
  # Payment method attributes
@@ -29,6 +30,18 @@ module Pay
29
30
  store_accessor :data, :username # Venmo
30
31
  store_accessor :data, :bank
31
32
 
33
+ store_accessor :data, :amount_captured
34
+ store_accessor :data, :invoice_id
35
+ store_accessor :data, :payment_intent_id
36
+ store_accessor :data, :period_start
37
+ store_accessor :data, :period_end
38
+ store_accessor :data, :line_items
39
+ store_accessor :data, :subtotal # subtotal amount in cents
40
+ store_accessor :data, :tax # total tax amount in cents
41
+ store_accessor :data, :discounts # array of discount IDs applied to the Stripe Invoice
42
+ store_accessor :data, :total_discount_amounts # array of discount details
43
+ store_accessor :data, :total_tax_amounts # array of tax details for each jurisdiction
44
+
32
45
  # Helpers for payment processors
33
46
  %w[braintree stripe paddle fake_processor].each do |processor_name|
34
47
  define_method "#{processor_name}?" do
@@ -38,6 +51,8 @@ module Pay
38
51
  scope processor_name, -> { where(processor: processor_name) }
39
52
  end
40
53
 
54
+ delegate :capture, :credit_note!, :credit_notes, to: :payment_processor
55
+
41
56
  def self.find_by_processor_and_id(processor, processor_id)
42
57
  joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
43
58
  end
@@ -54,6 +69,10 @@ module Pay
54
69
  payment_processor.charge
55
70
  end
56
71
 
72
+ def captured?
73
+ amount_captured > 0
74
+ end
75
+
57
76
  def refund!(refund_amount = nil)
58
77
  refund_amount ||= amount
59
78
  payment_processor.refund!(refund_amount)
@@ -111,5 +130,9 @@ module Pay
111
130
  payment_method_type&.titleize
112
131
  end
113
132
  end
133
+
134
+ def line_items
135
+ Array.wrap(super)
136
+ end
114
137
  end
115
138
  end
@@ -41,12 +41,8 @@ module Pay
41
41
  end
42
42
 
43
43
  def subscribed?(name: Pay.default_product_name, processor_plan: nil)
44
- subscription = subscription(name: name)
45
-
46
- return false if subscription.nil?
47
- return subscription.active? if processor_plan.nil?
48
-
49
- subscription.active? && subscription.processor_plan == processor_plan
44
+ query = {name: name, processor_plan: processor_plan}.compact
45
+ subscriptions.active.where(query).exists?
50
46
  end
51
47
 
52
48
  def on_trial?(name: Pay.default_product_name, plan: nil)
@@ -16,5 +16,11 @@ module Pay
16
16
  return if processor.blank?
17
17
  @pay_processor ||= self.class.pay_processor_for(processor).new(self)
18
18
  end
19
+
20
+ def onboarding_complete?
21
+ ActiveModel::Type::Boolean.new.cast(
22
+ (data.presence || {})["onboarding_complete"]
23
+ )
24
+ end
19
25
  end
20
26
  end
@@ -11,7 +11,7 @@ module Pay
11
11
  scope :on_trial, -> { where.not(trial_ends_at: nil).where("#{table_name}.trial_ends_at > ?", Time.zone.now) }
12
12
  scope :cancelled, -> { where.not(ends_at: nil) }
13
13
  scope :on_grace_period, -> { cancelled.where("#{table_name}.ends_at > ?", Time.zone.now) }
14
- scope :active, -> { where(ends_at: nil).or(on_grace_period).or(on_trial) }
14
+ scope :active, -> { where(status: ["trialing", "active"], ends_at: nil).or(on_grace_period).or(on_trial) }
15
15
  scope :incomplete, -> { where(status: :incomplete) }
16
16
  scope :past_due, -> { where(status: :past_due) }
17
17
  scope :with_active_customer, -> { joins(:customer).merge(Customer.active) }
@@ -24,6 +24,10 @@ module Pay
24
24
  store_accessor :data, :paddle_cancel_url
25
25
  store_accessor :data, :paddle_paused_from
26
26
  store_accessor :data, :stripe_account
27
+ store_accessor :data, :metered
28
+ store_accessor :data, :subscription_items
29
+ store_accessor :data, :pause_behavior
30
+ store_accessor :data, :pause_resumes_at
27
31
 
28
32
  attribute :prorate, :boolean, default: true
29
33
 
@@ -31,15 +35,10 @@ module Pay
31
35
  validates :name, presence: true
32
36
  validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
33
37
  validates :processor_plan, presence: true
34
- validates :quantity, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 1}
38
+ validates :quantity, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
35
39
  validates :status, presence: true
36
40
 
37
- delegate :on_grace_period?,
38
- :paused?,
39
- :pause,
40
- :cancel,
41
- :cancel_now!,
42
- to: :payment_processor
41
+ delegate_missing_to :payment_processor
43
42
 
44
43
  # Helper methods for payment processors
45
44
  %w[braintree stripe paddle fake_processor].each do |processor_name|
@@ -50,6 +49,24 @@ module Pay
50
49
  scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
51
50
  end
52
51
 
52
+ def self.with_metered_items
53
+ case Pay::Adapter.current_adapter
54
+ when "sqlite3"
55
+ where("json_extract(data, '$.\"metered\"') = true")
56
+ # For SQLite 3.38+ we could use the arrows
57
+ # where("data->'metered' = ?", "true")
58
+ when "mysql2"
59
+ where("data->'$.\"metered\"' = true")
60
+ when "postgresql", "postgis"
61
+ # Single quotes are important for json keys apparently
62
+ where("data->>'metered' = 'true'")
63
+ end
64
+ end
65
+
66
+ def metered_items?
67
+ !!metered
68
+ end
69
+
53
70
  def self.find_by_processor_and_id(processor, processor_id)
54
71
  joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
55
72
  end
@@ -62,6 +79,10 @@ module Pay
62
79
  @payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
63
80
  end
64
81
 
82
+ def sync!
83
+ self.class.pay_processor_for(customer.processor).sync(processor_id)
84
+ end
85
+
65
86
  def no_prorate
66
87
  self.prorate = false
67
88
  end
@@ -138,6 +159,12 @@ module Pay
138
159
  end
139
160
  end
140
161
 
162
+ def pause_resumes_at
163
+ if (resumes_at = super)
164
+ Time.zone.parse(resumes_at)
165
+ end
166
+ end
167
+
141
168
  private
142
169
 
143
170
  def cancel_if_active
@@ -5,13 +5,13 @@ Questions? Please reply to this email.<br/>
5
5
  ------------------------------------<br/>
6
6
  RECEIPT - SUBSCRIPTION<br/>
7
7
  <br/>
8
- Amount: <%= params[:charge].amount_with_currency %><br/>
8
+ Amount: <%= params[:pay_charge].amount_with_currency %><br/>
9
9
  <br/>
10
- Charged to: <%= params[:charge].charged_to %><br/>
11
- Transaction ID: <%= params[:charge].id %><br/>
12
- Date: <%= params[:charge].created_at %><br/>
13
- <% if params[:charge].customer.owner.try(:extra_billing_info?) %>
14
- <%= params[:charge].customer.owner.extra_billing_info %><br/>
10
+ Charged to: <%= params[:pay_charge].charged_to %><br/>
11
+ Transaction ID: <%= params[:pay_charge].id %><br/>
12
+ Date: <%= params[:pay_charge].created_at %><br/>
13
+ <% if params[:pay_charge].customer.owner.try(:extra_billing_info?) %>
14
+ <%= params[:pay_charge].customer.owner.extra_billing_info %><br/>
15
15
  <% end %>
16
16
  <br/>
17
17
  <br/>
@@ -6,13 +6,13 @@ Questions? Please reply to this email.<br/>
6
6
  ------------------------------------<br/>
7
7
  RECEIPT - REFUND<br/>
8
8
  <br/>
9
- Amount: <%= params[:charge].amount_refunded_with_currency %><br/>
9
+ Amount: <%= params[:pay_charge].amount_refunded_with_currency %><br/>
10
10
  <br/>
11
- Refunded to: <%= params[:charge].charged_to %><br/>
12
- Transaction ID: <%= params[:charge].id %><br/>
13
- Date: <%= params[:charge].created_at %><br/>
14
- <% if params[:charge].customer.owner.try(:extra_billing_info?) %>
15
- <%= params[:charge].customer.owner.extra_billing_info %><br/>
11
+ Refunded to: <%= params[:pay_charge].charged_to %><br/>
12
+ Transaction ID: <%= params[:pay_charge].id %><br/>
13
+ Date: <%= params[:pay_charge].created_at %><br/>
14
+ <% if params[:pay_charge].customer.owner.try(:extra_billing_info?) %>
15
+ <%= params[:pay_charge].customer.owner.extra_billing_info %><br/>
16
16
  <% end %>
17
17
  <br/>
18
18
  <br/>
@@ -1,37 +1,44 @@
1
1
  en:
2
2
  pay:
3
3
  successful:
4
- header: Payment Successful
5
- description: This payment was already successfully confirmed.
4
+ header: "Payment Successful"
5
+ description: "This payment was already successfully confirmed."
6
6
  cancelled:
7
- header: Payment Cancelled
8
- description: This payment was cancelled.
7
+ header: "Payment Cancelled"
8
+ description: "This payment was cancelled."
9
9
  requires_action:
10
- header: Confirm your %{amount} payment
11
- description: Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.
12
- full_name: Full name
13
- card: Credit or debit card
14
- button: Pay %{amount}
15
- name_missing: Please provide your name.
16
- success: The payment was successful.
17
- all_rights_reserved: All rights reserved.
18
- back: Go back
19
- receipt:
20
- date: Date
21
- account_billed: Account Billed
22
- product: Product
23
- amount: Amount
24
- charged_to: Charged to
25
- additional_info: Additional Info
26
- refunded: Refunded
27
- paid: Paid
28
- invoice:
10
+ header: "Confirm your %{amount} payment"
11
+ description: "Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below."
12
+ full_name: "Full name"
13
+ card: "Credit or debit card"
14
+ button: "Pay %{amount}"
15
+ name_missing: "Please provide your name."
16
+ success: "The payment was successful."
17
+ all_rights_reserved: "All rights reserved."
18
+ back: "Go back"
19
+ refund: "Refund"
20
+ line_items:
29
21
  amount: "Amount"
30
- product: "Product"
22
+ description: "Description"
31
23
  quantity: "Quantity"
32
24
  subtotal: "Subtotal"
25
+ discount: "Discount"
26
+ tax: "Tax"
33
27
  total: "Total"
34
28
  unit_price: "Unit Price"
29
+ percent_discount: "%{name} (%{percent}% off)"
30
+ amount_discount: "%{name} (%{amount} off)"
31
+ receipt:
32
+ date: "Date"
33
+ number: "Receipt Number"
34
+ paid: "Paid"
35
+ payment_method: "Payment Method"
36
+ amount_paid: "Amount paid"
37
+ refunded: "Refunded"
38
+ invoice:
39
+ number: "Invoice Number"
40
+ date: "Date"
41
+ payment_method: "Payment method"
35
42
  errors:
36
43
  action_required: "This payment attempt failed because additional action is required before it can be completed."
37
44
  invalid_payment: "This payment attempt failed because of an invalid payment method."
data/config/routes.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Pay::Engine.routes.draw do
4
4
  resources :payments, only: [:show], module: :pay
5
- post "webhooks/stripe", to: "pay/webhooks/stripe#create"
6
- post "webhooks/braintree", to: "pay/webhooks/braintree#create"
7
- post "webhooks/paddle", to: "pay/webhooks/paddle#create"
5
+ post "webhooks/stripe", to: "pay/webhooks/stripe#create" if Pay::Stripe.enabled?
6
+ post "webhooks/braintree", to: "pay/webhooks/braintree#create" if Pay::Braintree.enabled?
7
+ post "webhooks/paddle", to: "pay/webhooks/paddle#create" if Pay::Paddle.enabled?
8
8
  end
@@ -8,6 +8,10 @@ module Pay
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
+ cattr_accessor :pay_default_payment_processor
12
+ cattr_accessor :pay_stripe_customer_attributes
13
+ cattr_accessor :pay_braintree_customer_attributes
14
+
11
15
  has_many :pay_customers, class_name: "Pay::Customer", as: :owner, inverse_of: :owner
12
16
  has_many :charges, through: :pay_customers, class_name: "Pay::Charge"
13
17
  has_many :subscriptions, through: :pay_customers, class_name: "Pay::Subscription"
@@ -35,6 +39,24 @@ module Pay
35
39
  reload_payment_processor
36
40
  end
37
41
 
42
+ def add_payment_processor(processor_name, allow_fake: false, **attributes)
43
+ raise Pay::Error, "Processor `#{processor_name}` is not allowed" if processor_name.to_s == "fake_processor" && !allow_fake
44
+
45
+ pay_customer = pay_customers.active.where(processor: processor_name).first_or_initialize
46
+ pay_customer.update!(attributes)
47
+ pay_customer
48
+ end
49
+
50
+ def payment_processor
51
+ current_processor = super
52
+
53
+ if current_processor.blank? && self.class.pay_default_payment_processor.present?
54
+ set_payment_processor(self.class.pay_default_payment_processor, allow_fake: true)
55
+ else
56
+ current_processor
57
+ end
58
+ end
59
+
38
60
  def cancel_active_pay_subscriptions!
39
61
  subscriptions.active.each(&:cancel_now!)
40
62
  end
@@ -61,12 +83,16 @@ module Pay
61
83
  end
62
84
 
63
85
  class_methods do
64
- def pay_customer
86
+ def pay_customer(options = {})
65
87
  include Billable::SyncCustomer
66
88
  include CustomerExtension
89
+
90
+ self.pay_default_payment_processor = options[:default_payment_processor]
91
+ self.pay_stripe_customer_attributes = options[:stripe_attributes]
92
+ self.pay_braintree_customer_attributes = options[:braintree_attributes]
67
93
  end
68
94
 
69
- def pay_merchant
95
+ def pay_merchant(options = {})
70
96
  include MerchantExtension
71
97
  end
72
98
  end
@@ -8,7 +8,7 @@ module Pay
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
- after_update :enqeue_sync_email_job, if: :pay_should_sync_customer?
11
+ after_update :enqeue_customer_sync_job, if: :pay_should_sync_customer?
12
12
  end
13
13
 
14
14
  def pay_should_sync_customer?
@@ -17,8 +17,8 @@ module Pay
17
17
 
18
18
  private
19
19
 
20
- def enqeue_sync_email_job
21
- if saved_change_to_email?
20
+ def enqeue_customer_sync_job
21
+ if pay_should_sync_customer?
22
22
  # Queue job to update each payment processor for this customer
23
23
  pay_customers.pluck(:id).each do |pay_customer_id|
24
24
  CustomerSyncJob.perform_later(pay_customer_id)
@@ -15,6 +15,24 @@ module Pay
15
15
  @pay_customer = pay_customer
16
16
  end
17
17
 
18
+ # Returns a hash of attributes for the Stripe::Customer object
19
+ def customer_attributes
20
+ owner = pay_customer.owner
21
+
22
+ attributes = case owner.class.pay_braintree_customer_attributes
23
+ when Symbol
24
+ owner.send(owner.class.pay_braintree_customer_attributes, pay_customer)
25
+ when Proc
26
+ owner.class.pay_braintree_customer_attributes.call(pay_customer)
27
+ end
28
+
29
+ # Guard against attributes being returned nil
30
+ attributes ||= {}
31
+
32
+ first_name, last_name = customer_name.split(" ", 2)
33
+ {email: email, first_name: first_name, last_name: last_name}.merge(attributes)
34
+ end
35
+
18
36
  # Retrieve the Braintree::Customer object
19
37
  #
20
38
  # - If no processor_id is present, creates a Customer.
@@ -30,8 +48,7 @@ module Pay
30
48
 
31
49
  customer
32
50
  else
33
- first_name, last_name = customer_name.split(" ", 2)
34
- result = gateway.customer.create(email: email, first_name: first_name, last_name: last_name, payment_method_nonce: payment_method_token)
51
+ result = gateway.customer.create(customer_attributes.merge(payment_method_nonce: payment_method_token))
35
52
  raise Pay::Braintree::Error, result unless result.success?
36
53
  pay_customer.update!(processor_id: result.customer.id)
37
54
 
@@ -49,10 +66,10 @@ module Pay
49
66
  end
50
67
 
51
68
  # Syncs name and email to Braintree::Customer
52
- def update_customer!
53
- return unless processor_id?
54
- first_name, last_name = customer_name.split(" ", 2)
55
- gateway.customer.update(processor_id, first_name: first_name, last_name: last_name, email: email)
69
+ # You can also pass in other attributes that will be merged into the default attributes
70
+ def update_customer!(**attributes)
71
+ customer unless processor_id?
72
+ gateway.customer.update(processor_id, customer_attributes.merge(attributes))
56
73
  end
57
74
 
58
75
  def charge(amount, options = {})
@@ -135,10 +152,6 @@ module Pay
135
152
  raise Pay::Braintree::Error, e
136
153
  end
137
154
 
138
- def update_email!
139
- gateway.customer.update(processor_id, email: email, first_name: try(:first_name), last_name: try(:last_name))
140
- end
141
-
142
155
  def trial_end_date(subscription)
143
156
  return unless subscription.trial_period
144
157
  # Braintree returns dates without time zones, so we'll assume they're UTC
@@ -174,44 +187,44 @@ module Pay
174
187
 
175
188
  def save_payment_method(payment_method, default:)
176
189
  attributes = case payment_method
177
- when ::Braintree::CreditCard, ::Braintree::ApplePayCard, ::Braintree::GooglePayCard, ::Braintree::SamsungPayCard, ::Braintree::VisaCheckoutCard
178
- {
179
- payment_method_type: :card,
180
- brand: payment_method.card_type,
181
- last4: payment_method.last_4,
182
- exp_month: payment_method.expiration_month,
183
- exp_year: payment_method.expiration_year
184
- }
185
-
186
- when ::Braintree::PayPalAccount
187
- {
188
- payment_method_type: :paypal,
189
- brand: "PayPal",
190
- email: payment_method.email
191
- }
192
- when ::Braintree::VenmoAccount
193
- {
194
- payment_method_type: :venmo,
195
- brand: "Venmo",
196
- username: payment_method.username
197
- }
198
- when ::Braintree::UsBankAccount
199
- {
200
- payment_method_type: "us_bank_account",
201
- bank: payment_method.bank_name,
202
- last4: payment_method.last_4
203
- }
204
- else
205
- {
206
- payment_method_type: payment_method.class.name.demodulize.underscore,
207
- brand: payment_method.try(:card_type),
208
- last4: payment_method.try(:last_4),
209
- exp_month: payment_method.try(:expiration_month),
210
- exp_year: payment_method.try(:expiration_year),
211
- bank: payment_method.try(:bank_name),
212
- username: payment_method.try(:username),
213
- email: payment_method.try(:email)
214
- }
190
+ when ::Braintree::CreditCard, ::Braintree::ApplePayCard, ::Braintree::GooglePayCard, ::Braintree::SamsungPayCard, ::Braintree::VisaCheckoutCard
191
+ {
192
+ payment_method_type: :card,
193
+ brand: payment_method.card_type,
194
+ last4: payment_method.last_4,
195
+ exp_month: payment_method.expiration_month,
196
+ exp_year: payment_method.expiration_year
197
+ }
198
+
199
+ when ::Braintree::PayPalAccount
200
+ {
201
+ payment_method_type: :paypal,
202
+ brand: "PayPal",
203
+ email: payment_method.email
204
+ }
205
+ when ::Braintree::VenmoAccount
206
+ {
207
+ payment_method_type: :venmo,
208
+ brand: "Venmo",
209
+ username: payment_method.username
210
+ }
211
+ when ::Braintree::UsBankAccount
212
+ {
213
+ payment_method_type: "us_bank_account",
214
+ bank: payment_method.bank_name,
215
+ last4: payment_method.last_4
216
+ }
217
+ else
218
+ {
219
+ payment_method_type: payment_method.class.name.demodulize.underscore,
220
+ brand: payment_method.try(:card_type),
221
+ last4: payment_method.try(:last_4),
222
+ exp_month: payment_method.try(:expiration_month),
223
+ exp_year: payment_method.try(:expiration_year),
224
+ bank: payment_method.try(:bank_name),
225
+ username: payment_method.try(:username),
226
+ email: payment_method.try(:email)
227
+ }
215
228
  end
216
229
 
217
230
  pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.token).first_or_initialize
@@ -27,7 +27,7 @@ module Pay
27
27
  gateway.subscription.find(processor_id)
28
28
  end
29
29
 
30
- def cancel
30
+ def cancel(**options)
31
31
  subscription = processor_subscription
32
32
 
33
33
  if on_trial?
@@ -43,9 +43,14 @@ module Pay
43
43
  raise Pay::Braintree::Error, e
44
44
  end
45
45
 
46
- def cancel_now!
46
+ def cancel_now!(**options)
47
47
  gateway.subscription.cancel(processor_subscription.id)
48
- pay_subscription.update(status: :canceled, ends_at: Time.current)
48
+ ends_at = Time.current
49
+ pay_subscription.update!(
50
+ status: :canceled,
51
+ trial_ends_at: (ends_at if pay_subscription.trial_ends_at?),
52
+ ends_at: ends_at
53
+ )
49
54
  rescue ::Braintree::BraintreeError => e
50
55
  raise Pay::Braintree::Error, e
51
56
  end
@@ -11,7 +11,12 @@ module Pay
11
11
  pay_subscription = Pay::Subscription.find_by_processor_and_id(:braintree, subscription.id)
12
12
  return unless pay_subscription.present?
13
13
 
14
- pay_subscription.update!(ends_at: Time.current, status: :canceled)
14
+ ends_at = Time.current
15
+ pay_subscription.update!(
16
+ status: :canceled,
17
+ trial_ends_at: (ends_at if pay_subscription.trial_ends_at?),
18
+ ends_at: ends_at
19
+ )
15
20
  end
16
21
  end
17
22
  end
@@ -14,8 +14,8 @@ module Pay
14
14
  pay_customer = pay_subscription.customer
15
15
  pay_charge = Pay::Braintree::Billable.new(pay_customer).save_transaction(subscription.transactions.first)
16
16
 
17
- if pay_charge && Pay.send_emails
18
- Pay::UserMailer.with(pay_customer: pay_customer, charge: pay_charge).receipt.deliver_later
17
+ if pay_charge && Pay.send_email?(:receipt, pay_charge)
18
+ Pay.mailer.with(pay_customer: pay_customer, pay_charge: pay_charge).receipt.deliver_later
19
19
  end
20
20
  end
21
21
  end