pay 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -31
  3. data/Rakefile +19 -19
  4. data/app/controllers/pay/payments_controller.rb +7 -0
  5. data/app/controllers/pay/webhooks/braintree_controller.rb +19 -6
  6. data/app/mailers/pay/application_mailer.rb +2 -2
  7. data/app/mailers/pay/user_mailer.rb +12 -0
  8. data/app/models/pay/subscription.rb +41 -10
  9. data/app/views/layouts/pay/application.html.erb +11 -5
  10. data/app/views/pay/payments/show.html.erb +134 -0
  11. data/app/views/pay/user_mailer/payment_action_required.html.erb +6 -0
  12. data/config/locales/en.yml +17 -0
  13. data/config/routes.rb +3 -2
  14. data/db/migrate/20190816015720_add_status_to_subscriptions.rb +14 -0
  15. data/lib/generators/pay/email_views_generator.rb +3 -3
  16. data/lib/generators/pay/views_generator.rb +13 -0
  17. data/lib/pay/billable/sync_email.rb +3 -4
  18. data/lib/pay/billable.rb +25 -18
  19. data/lib/pay/braintree/billable.rb +88 -82
  20. data/lib/pay/braintree/charge.rb +0 -2
  21. data/lib/pay/braintree/subscription.rb +74 -72
  22. data/lib/pay/braintree.rb +6 -6
  23. data/lib/pay/engine.rb +10 -10
  24. data/lib/pay/env.rb +0 -1
  25. data/lib/pay/payment.rb +52 -0
  26. data/lib/pay/receipts.rb +7 -7
  27. data/lib/pay/stripe/billable.rb +68 -37
  28. data/lib/pay/stripe/charge.rb +0 -2
  29. data/lib/pay/stripe/subscription.rb +7 -6
  30. data/lib/pay/stripe/webhooks/charge_refunded.rb +0 -2
  31. data/lib/pay/stripe/webhooks/charge_succeeded.rb +9 -10
  32. data/lib/pay/stripe/webhooks/customer_deleted.rb +5 -7
  33. data/lib/pay/stripe/webhooks/customer_updated.rb +0 -2
  34. data/lib/pay/stripe/webhooks/payment_action_required.rb +27 -0
  35. data/lib/pay/stripe/webhooks/{source_deleted.rb → payment_method_updated.rb} +1 -3
  36. data/lib/pay/stripe/webhooks/subscription_created.rb +6 -11
  37. data/lib/pay/stripe/webhooks/subscription_deleted.rb +1 -3
  38. data/lib/pay/stripe/webhooks/subscription_renewing.rb +0 -2
  39. data/lib/pay/stripe/webhooks/subscription_updated.rb +13 -10
  40. data/lib/pay/stripe/webhooks.rb +17 -11
  41. data/lib/pay/stripe.rb +5 -5
  42. data/lib/pay/version.rb +1 -1
  43. data/lib/pay.rb +42 -15
  44. metadata +27 -39
@@ -0,0 +1,134 @@
1
+ <div id="app" class="h-full md:flex md:justify-center md:items-center">
2
+ <div class="w-full max-w-lg">
3
+ <!-- Status Messages -->
4
+ <p class="flex items-center mb-4 bg-red-100 border border-red-200 px-5 py-2 rounded-lg text-red-500" v-if="errorMessage">
5
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="flex-shrink-0 w-6 h-6">
6
+ <path class="fill-current text-red-300" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20z"/>
7
+ <path class="fill-current text-red-500" d="M12 18a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm1-5.9c-.13 1.2-1.88 1.2-2 0l-.5-5a1 1 0 0 1 1-1.1h1a1 1 0 0 1 1 1.1l-.5 5z"/>
8
+ </svg>
9
+
10
+ <span class="ml-3">{{ errorMessage }}</span>
11
+ </p>
12
+
13
+ <p class="flex items-center mb-4 bg-green-100 border border-green-200 px-5 py-4 rounded-lg text-green-700" v-if="paymentProcessed && successMessage">
14
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="flex-shrink-0 w-6 h-6">
15
+ <circle cx="12" cy="12" r="10" class="fill-current text-green-300"/>
16
+ <path class="fill-current text-green-500" d="M10 14.59l6.3-6.3a1 1 0 0 1 1.4 1.42l-7 7a1 1 0 0 1-1.4 0l-3-3a1 1 0 0 1 1.4-1.42l2.3 2.3z"/>
17
+ </svg>
18
+
19
+ <span class="ml-3">{{ successMessage }}</span>
20
+ </p>
21
+
22
+ <div class="bg-white rounded-lg shadow-xl p-4 sm:py-6 sm:px-10 mb-5">
23
+ <% if @payment.succeeded? %>
24
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "successful.header" %></h1>
25
+ <p class="mb-6"><%=t "successful.description" %></p>
26
+
27
+ <% elsif @payment.canceled? %>
28
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "cancelled.header" %></h1>
29
+ <p class="mb-6"><%=t "cancelled.description" %></p>
30
+
31
+ <% else %>
32
+ <div id="payment-elements" v-if="! paymentProcessed">
33
+ <!-- Instructions -->
34
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "requires_action.header", amount: number_to_currency(@payment.amount / 100.0) %></h1>
35
+ <p class="mb-6"><%=t "requires_action.description" %></p>
36
+
37
+ <div v-show="status == 'requires_payment_method'">
38
+ <!-- Name -->
39
+ <label for="cardholder-name" class="inline-block text-sm text-gray-700 font-semibold mb-2"><%=t "requires_action.full_name" %></label>
40
+ <input id="cardholder-name" type="text" placeholder="Jane Doe" required class="inline-block text-black bg-gray-200 border border-gray-400 rounded-lg w-full px-4 py-3 mb-3 focus:outline-none" v-model="name">
41
+
42
+ <!-- Card -->
43
+ <label for="card-element" class="inline-block text-sm text-gray-700 font-semibold mb-2"><%=t "requires_action.card" %></label>
44
+ <div id="card-element" class="bg-gray-200 border border-gray-400 rounded-lg p-4 mb-6"></div>
45
+ </div>
46
+
47
+ <!-- Pay Button -->
48
+ <button id="card-button" class="inline-block w-full px-4 py-3 mb-4 text-white rounded-lg bg-blue-400 hover:bg-blue-500" :class="{ 'bg-blue-400': paymentProcessing, 'bg-blue-600': ! paymentProcessing }" @click="confirmPayment" :disabled="paymentProcessing">
49
+ <%=t "requires_action.button", amount: number_to_currency(@payment.amount / 100.0) %>
50
+ </button>
51
+ </div>
52
+ <% end %>
53
+
54
+ <%= link_to t("back"), root_path, class: "inline-block w-full px-4 py-3 bg-gray-200 hover:bg-gray-300 text-center text-gray-700 rounded-lg" %>
55
+ </div>
56
+
57
+ <p class="text-center text-gray-500 text-sm">
58
+ © <%= Date.current.year %> <%= Pay.business_name %> <%=t "all_rights_reserved" %>
59
+ </p>
60
+ </div>
61
+ </div>
62
+
63
+ <script>
64
+ window.stripe = Stripe('<%= Pay::Stripe.public_key %>');
65
+ var app = new Vue({
66
+ el: '#app',
67
+ data: {
68
+ clientSecret: '<%= @payment.client_secret %>',
69
+ status: '<%= @payment.status %>',
70
+ name: '',
71
+ cardElement: null,
72
+ paymentProcessing: false,
73
+ paymentProcessed: false,
74
+ successMessage: '',
75
+ errorMessage: ''
76
+ },
77
+
78
+ mounted: function () {
79
+ if (this.status == "succeeded" || this.status == "canceled") {
80
+ return
81
+ }
82
+
83
+ // We can trigger SCA immediately if this payment requires action
84
+ // This makes sure a new SCA subscription doesn't have to put in their card twice
85
+ if (this.status == "requires_action") {
86
+ this.paymentProcessing = true
87
+ this.paymentProcessed = false
88
+ stripe.confirmCardPayment(this.clientSecret).then(this.handleConfirmResult.bind(this))
89
+ }
90
+
91
+ // Setup elements in case the authentication fails and user needs to put in a new card
92
+ const elements = stripe.elements();
93
+ this.cardElement = elements.create('card');
94
+ this.cardElement.mount('#card-element');
95
+ },
96
+
97
+ methods: {
98
+ confirmPayment: function () {
99
+ this.paymentProcessing = true
100
+ this.paymentProcessed = false
101
+ this.successMessage = ''
102
+ this.errorMessage = ''
103
+
104
+ stripe.confirmCardPayment(
105
+ this.clientSecret,
106
+ {
107
+ payment_method: {
108
+ card: this.cardElement,
109
+ billing_details: { name: this.name }
110
+ },
111
+ save_payment_method: true,
112
+ setup_future_usage: 'off_session',
113
+ }
114
+ ).then(this.handleConfirmResult.bind(this))
115
+ },
116
+
117
+ handleConfirmResult(result) {
118
+ this.paymentProcessing = false;
119
+ if (result.error) {
120
+ if (result.error.code === 'parameter_invalid_empty' &&
121
+ result.error.param === 'payment_method_data[billing_details][name]') {
122
+ this.errorMessage = '<%=t "requires_action.name_missing" %>'
123
+ } else {
124
+ this.errorMessage = result.error.message
125
+ this.status = result.error.payment_intent.status
126
+ }
127
+ } else {
128
+ this.paymentProcessed = true;
129
+ this.successMessage = '<%=t "requires_action.success" %>'
130
+ }
131
+ }
132
+ },
133
+ })
134
+ </script>
@@ -0,0 +1,6 @@
1
+ <h3>Extra confirmation is needed to process your payment</h3>
2
+ <p>Your <%= Pay.business_name %> subscription requires confirmation to process your payment to continue access.</p>
3
+
4
+ <p>You may confirm your payment via your account. If you have any questions, please hit reply and let us know.</p>
5
+
6
+ <p>- The <%= Pay.business_name %> Team</p>
@@ -0,0 +1,17 @@
1
+ en:
2
+ successful:
3
+ header: Payment Successful
4
+ description: This payment was already successfully confirmed.
5
+ cancelled:
6
+ header: Payment Cancelled
7
+ description: This payment was cancelled.
8
+ requires_action:
9
+ header: Confirm your %{amount} payment
10
+ description: Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.
11
+ full_name: Full name
12
+ card: Card
13
+ button: Pay %{amount}
14
+ name_missing: Please provide your name.
15
+ success: The payment was successful.
16
+ all_rights_reserved: All rights reserved.
17
+ back: Go back
data/config/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Pay::Engine.routes.draw do
4
- post 'stripe', to: 'stripe_event/webhook#event'
5
- post 'braintree', to: 'pay/webhooks/braintree#create'
4
+ resources :payments, only: [:show], module: :pay
5
+ post "webhooks/stripe", to: "stripe_event/webhook#event"
6
+ post "webhooks/braintree", to: "pay/webhooks/braintree#create"
6
7
  end
@@ -0,0 +1,14 @@
1
+ class AddStatusToSubscriptions < ActiveRecord::Migration[5.2]
2
+ def self.up
3
+ add_column :pay_subscriptions, :status, :string
4
+
5
+ # Any existing subscriptions should be marked as 'active'
6
+ # This won't actually make them active if their ends_at column is expired
7
+ Pay::Subscription.reset_column_information
8
+ Pay::Subscription.update_all(status: :active)
9
+ end
10
+
11
+ def self.down
12
+ remove_column :pay_subscriptions, :status
13
+ end
14
+ end
@@ -1,4 +1,4 @@
1
- require 'rails/generators'
1
+ require "rails/generators"
2
2
 
3
3
  module Pay
4
4
  module Generators
@@ -6,8 +6,8 @@ module Pay
6
6
  source_root File.expand_path("../../../..", __FILE__)
7
7
 
8
8
  def copy_views
9
- directory 'app/views/pay/user_mailer', 'app/views/pay/user_mailer'
9
+ directory "app/views/pay/user_mailer", "app/views/pay/user_mailer"
10
10
  end
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -0,0 +1,13 @@
1
+ require "rails/generators"
2
+
3
+ module Pay
4
+ module Generators
5
+ class ViewsGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("../../../..", __FILE__)
7
+
8
+ def copy_views
9
+ directory "app/views/pay", "app/views/pay"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -15,12 +15,11 @@ module Pay
15
15
  extend ActiveSupport::Concern
16
16
 
17
17
  included do
18
- after_update :enqeue_sync_email_job,
19
- if: :should_sync_email_with_processor?
18
+ after_update :enqeue_sync_email_job, if: :should_sync_email_with_processor?
20
19
  end
21
20
 
22
21
  def should_sync_email_with_processor?
23
- respond_to? :saved_change_to_email?
22
+ try(:saved_change_to_email?)
24
23
  end
25
24
 
26
25
  def sync_email_with_processor
@@ -32,7 +31,7 @@ module Pay
32
31
  def enqeue_sync_email_job
33
32
  # Only update if the processor id is the same
34
33
  # This prevents duplicate API hits if this is their first time
35
- if processor_id? && !processor_id_changed? && saved_change_to_email?
34
+ if processor_id? && !saved_change_to_processor_id? && saved_change_to_email?
36
35
  EmailSyncJob.perform_later(id)
37
36
  end
38
37
  end
data/lib/pay/billable.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'pay/billable/sync_email'
1
+ require "pay/billable/sync_email"
2
2
 
3
3
  module Pay
4
4
  module Billable
@@ -38,7 +38,7 @@ module Pay
38
38
  send("create_#{processor}_charge", amount_in_cents, options)
39
39
  end
40
40
 
41
- def subscribe(name: 'default', plan: 'default', **options)
41
+ def subscribe(name: "default", plan: "default", **options)
42
42
  check_for_processor
43
43
  send("create_#{processor}_subscription", name, plan, options)
44
44
  end
@@ -49,25 +49,25 @@ module Pay
49
49
  send("update_#{processor}_card", token)
50
50
  end
51
51
 
52
- def on_trial?(name: 'default', plan: nil)
52
+ def on_trial?(name: "default", plan: nil)
53
53
  return true if default_generic_trial?(name, plan)
54
54
 
55
55
  sub = subscription(name: name)
56
- return sub && sub.on_trial? if plan.nil?
56
+ return sub&.on_trial? if plan.nil?
57
57
 
58
- sub && sub.on_trial? && sub.processor_plan == plan
58
+ sub&.on_trial? && sub.processor_plan == plan
59
59
  end
60
60
 
61
61
  def on_generic_trial?
62
62
  trial_ends_at? && trial_ends_at > Time.zone.now
63
63
  end
64
64
 
65
- def processor_subscription(subscription_id)
65
+ def processor_subscription(subscription_id, options = {})
66
66
  check_for_processor
67
- send("#{processor}_subscription", subscription_id)
67
+ send("#{processor}_subscription", subscription_id, options)
68
68
  end
69
69
 
70
- def subscribed?(name: 'default', processor_plan: nil)
70
+ def subscribed?(name: "default", processor_plan: nil)
71
71
  subscription = subscription(name: name)
72
72
 
73
73
  return false if subscription.nil?
@@ -76,17 +76,17 @@ module Pay
76
76
  subscription.active? && subscription.processor_plan == processor_plan
77
77
  end
78
78
 
79
- def on_trial_or_subscribed?(name: 'default', processor_plan: nil)
79
+ def on_trial_or_subscribed?(name: "default", processor_plan: nil)
80
80
  on_trial?(name: name, plan: processor_plan) ||
81
81
  subscribed?(name: name, processor_plan: processor_plan)
82
82
  end
83
83
 
84
- def subscription(name: 'default')
84
+ def subscription(name: "default")
85
85
  subscriptions.for_name(name).last
86
86
  end
87
87
 
88
- def invoice!
89
- send("#{processor}_invoice!")
88
+ def invoice!(options = {})
89
+ send("#{processor}_invoice!", options)
90
90
  end
91
91
 
92
92
  def upcoming_invoice
@@ -105,27 +105,34 @@ module Pay
105
105
  braintree? && card_type == "PayPal"
106
106
  end
107
107
 
108
+ def has_incomplete_payment?(name: "default")
109
+ subscription(name: name)&.has_incomplete_payment?
110
+ end
111
+
108
112
  private
109
113
 
110
114
  def check_for_processor
111
115
  raise StandardError, "No payment processor selected. Make sure to set the #{Pay.billable_class}'s `processor` attribute to either 'stripe' or 'braintree'." unless processor
112
116
  end
113
117
 
114
- def create_subscription(subscription, processor, name, plan, qty = 1)
115
- subscriptions.create!(
116
- name: name || 'default',
118
+ # Used for creating a Pay::Subscription in the database
119
+ def create_subscription(subscription, processor, name, plan, options = {})
120
+ options[:quantity] ||= 1
121
+
122
+ options.merge!(
123
+ name: name || "default",
117
124
  processor: processor,
118
125
  processor_id: subscription.id,
119
126
  processor_plan: plan,
120
127
  trial_ends_at: send("#{processor}_trial_end_date", subscription),
121
- quantity: qty,
122
- ends_at: nil
128
+ ends_at: nil,
123
129
  )
130
+ subscriptions.create!(options)
124
131
  end
125
132
 
126
133
  def default_generic_trial?(name, plan)
127
134
  # Generic trials don't have plans or custom names
128
- plan.nil? && name == 'default' && on_generic_trial?
135
+ plan.nil? && name == "default" && on_generic_trial?
129
136
  end
130
137
  end
131
138
  end
@@ -16,7 +16,7 @@ module Pay
16
16
  )
17
17
  raise Pay::Error.new(result.message) unless result.success?
18
18
 
19
- update(processor: 'braintree', processor_id: result.customer.id)
19
+ update(processor: "braintree", processor_id: result.customer.id)
20
20
 
21
21
  if card_token.present?
22
22
  update_braintree_card_on_file result.customer.payment_methods.last
@@ -31,11 +31,11 @@ module Pay
31
31
  # Handles Billable#charge
32
32
  #
33
33
  # Returns a Pay::Charge
34
- def create_braintree_charge(amount, options={})
34
+ def create_braintree_charge(amount, options = {})
35
35
  args = {
36
36
  amount: amount / 100.0,
37
37
  customer_id: customer.id,
38
- options: { submit_for_settlement: true }
38
+ options: {submit_for_settlement: true},
39
39
  }.merge(options)
40
40
 
41
41
  result = gateway.transaction.sale(args)
@@ -47,10 +47,15 @@ module Pay
47
47
  # Handles Billable#subscribe
48
48
  #
49
49
  # Returns Pay::Subscription
50
- def create_braintree_subscription(name, plan, options={})
50
+ def create_braintree_subscription(name, plan, options = {})
51
51
  token = customer.payment_methods.find(&:default?).try(:token)
52
52
  raise Pay::Error, "Customer has no default payment method" if token.nil?
53
53
 
54
+ # Standardize the trial period options
55
+ if (trial_period_days = options.delete(:trial_period_days)) && trial_period_days > 0
56
+ options.merge!(trial_period: true, trial_duration: trial_period_days, trial_duration_unit: :day)
57
+ end
58
+
54
59
  subscription_options = options.merge(
55
60
  payment_method_token: token,
56
61
  plan_id: plan
@@ -59,7 +64,7 @@ module Pay
59
64
  result = gateway.subscription.create(subscription_options)
60
65
  raise Pay::Error.new(result.message) unless result.success?
61
66
 
62
- create_subscription(result.subscription, 'braintree', name, plan)
67
+ create_subscription(result.subscription, "braintree", name, plan, status: :active)
63
68
  rescue ::Braintree::BraintreeError => e
64
69
  raise Error, e.message
65
70
  end
@@ -73,7 +78,7 @@ module Pay
73
78
  payment_method_nonce: token,
74
79
  options: {
75
80
  make_default: true,
76
- verify_card: true
81
+ verify_card: true,
77
82
  }
78
83
  )
79
84
  raise Pay::Error.new(result.message) unless result.success?
@@ -87,30 +92,31 @@ module Pay
87
92
 
88
93
  def update_braintree_email!
89
94
  braintree_customer.update(
90
- email: email,
95
+ email: email,
91
96
  first_name: try(:first_name),
92
- last_name: try(:last_name),
97
+ last_name: try(:last_name),
93
98
  )
94
99
  end
95
100
 
96
101
  def braintree_trial_end_date(subscription)
97
102
  return unless subscription.trial_period
98
- Time.zone.parse(subscription.first_billing_date)
103
+ # Braintree returns dates without time zones, so we'll assume they're UTC
104
+ Time.parse(subscription.first_billing_date).end_of_day
99
105
  end
100
106
 
101
107
  def update_subscriptions_to_payment_method(token)
102
108
  subscriptions.each do |subscription|
103
109
  if subscription.active?
104
- gateway.subscription.update(subscription.processor_id, { payment_method_token: token })
110
+ gateway.subscription.update(subscription.processor_id, {payment_method_token: token})
105
111
  end
106
112
  end
107
113
  end
108
114
 
109
- def braintree_subscription(subscription_id)
115
+ def braintree_subscription(subscription_id, options = {})
110
116
  gateway.subscription.find(subscription_id)
111
117
  end
112
118
 
113
- def braintree_invoice!
119
+ def braintree_invoice!(options = {})
114
120
  # pass
115
121
  end
116
122
 
@@ -120,7 +126,7 @@ module Pay
120
126
 
121
127
  def save_braintree_transaction(transaction)
122
128
  attrs = card_details_for_braintree_transaction(transaction)
123
- attrs.merge!(amount: transaction.amount.to_f * 100)
129
+ attrs[:amount] = transaction.amount.to_f * 100
124
130
 
125
131
  charge = charges.find_or_initialize_by(
126
132
  processor: :braintree,
@@ -132,80 +138,80 @@ module Pay
132
138
 
133
139
  private
134
140
 
135
- def gateway
136
- Pay.braintree_gateway
137
- end
141
+ def gateway
142
+ Pay.braintree_gateway
143
+ end
138
144
 
139
- def update_braintree_card_on_file(payment_method)
140
- case payment_method
141
- when ::Braintree::CreditCard
142
- update!(
143
- card_type: payment_method.card_type,
144
- card_last4: payment_method.last_4,
145
- card_exp_month: payment_method.expiration_month,
146
- card_exp_year: payment_method.expiration_year
147
- )
148
-
149
- when ::Braintree::PayPalAccount
150
- update!(
151
- card_type: "PayPal",
152
- card_last4: payment_method.email
153
- )
154
- end
145
+ def update_braintree_card_on_file(payment_method)
146
+ case payment_method
147
+ when ::Braintree::CreditCard
148
+ update!(
149
+ card_type: payment_method.card_type,
150
+ card_last4: payment_method.last_4,
151
+ card_exp_month: payment_method.expiration_month,
152
+ card_exp_year: payment_method.expiration_year
153
+ )
155
154
 
156
- # Clear the card token so we don't accidentally update twice
157
- self.card_token = nil
155
+ when ::Braintree::PayPalAccount
156
+ update!(
157
+ card_type: "PayPal",
158
+ card_last4: payment_method.email
159
+ )
158
160
  end
159
161
 
160
- def card_details_for_braintree_transaction(transaction)
161
- case transaction.payment_instrument_type
162
- when "credit_card", "samsung_pay_card", "masterpass_card", "samsung_pay_card", "visa_checkout_card"
163
- payment_method = transaction.send("#{transaction.payment_instrument_type}_details")
164
- {
165
- card_type: payment_method.card_type,
166
- card_last4: payment_method.last_4,
167
- card_exp_month: payment_method.expiration_month,
168
- card_exp_year: payment_method.expiration_year,
169
- }
170
-
171
- when "paypal_account"
172
- {
173
- card_type: "PayPal",
174
- card_last4: transaction.paypal_details.payer_email,
175
- card_exp_month: nil,
176
- card_exp_year: nil,
177
- }
178
-
179
- when "android_pay_card"
180
- payment_method = transaction.android_pay_details
181
- {
182
- card_type: payment_method.source_card_type,
183
- card_last4: payment_method.source_card_last_4,
184
- card_exp_month: payment_method.expiration_month,
185
- card_exp_year: payment_method.expiration_year,
186
- }
187
-
188
- when "venmo_account"
189
- {
190
- card_type: "Venmo",
191
- card_last4: transaction.venmo_account_details.username,
192
- card_exp_month: nil,
193
- card_exp_year: nil,
194
- }
195
-
196
- when "apple_pay_card"
197
- payment_method = transaction.apple_pay_details
198
- {
199
- card_type: payment_method.card_type,
200
- card_last4: payment_method.last_4,
201
- card_exp_month: payment_method.expiration_month,
202
- card_exp_year: payment_method.expiration_year,
203
- }
204
-
205
- else
206
- {}
207
- end
162
+ # Clear the card token so we don't accidentally update twice
163
+ self.card_token = nil
164
+ end
165
+
166
+ def card_details_for_braintree_transaction(transaction)
167
+ case transaction.payment_instrument_type
168
+ when "credit_card", "samsung_pay_card", "masterpass_card", "samsung_pay_card", "visa_checkout_card"
169
+ payment_method = transaction.send("#{transaction.payment_instrument_type}_details")
170
+ {
171
+ card_type: payment_method.card_type,
172
+ card_last4: payment_method.last_4,
173
+ card_exp_month: payment_method.expiration_month,
174
+ card_exp_year: payment_method.expiration_year,
175
+ }
176
+
177
+ when "paypal_account"
178
+ {
179
+ card_type: "PayPal",
180
+ card_last4: transaction.paypal_details.payer_email,
181
+ card_exp_month: nil,
182
+ card_exp_year: nil,
183
+ }
184
+
185
+ when "android_pay_card"
186
+ payment_method = transaction.android_pay_details
187
+ {
188
+ card_type: payment_method.source_card_type,
189
+ card_last4: payment_method.source_card_last_4,
190
+ card_exp_month: payment_method.expiration_month,
191
+ card_exp_year: payment_method.expiration_year,
192
+ }
193
+
194
+ when "venmo_account"
195
+ {
196
+ card_type: "Venmo",
197
+ card_last4: transaction.venmo_account_details.username,
198
+ card_exp_month: nil,
199
+ card_exp_year: nil,
200
+ }
201
+
202
+ when "apple_pay_card"
203
+ payment_method = transaction.apple_pay_details
204
+ {
205
+ card_type: payment_method.card_type,
206
+ card_last4: payment_method.last_4,
207
+ card_exp_month: payment_method.expiration_month,
208
+ card_exp_year: payment_method.expiration_year,
209
+ }
210
+
211
+ else
212
+ {}
208
213
  end
214
+ end
209
215
  end
210
216
  end
211
217
  end
@@ -1,6 +1,5 @@
1
1
  module Pay
2
2
  module Braintree
3
-
4
3
  module Charge
5
4
  extend ActiveSupport::Concern
6
5
 
@@ -22,6 +21,5 @@ module Pay
22
21
  raise Error, e.message
23
22
  end
24
23
  end
25
-
26
24
  end
27
25
  end