Skip to content

Commit 1c1a5c1

Browse files
authored
Use STI for Pay models (#1042)
* WIP STI for Pay models * Refactor to using STI * Add upgrade steps * Fix mysql created_at precision for tests * Refactor
1 parent a74e78a commit 1c1a5c1

File tree

91 files changed

+1189
-1388
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+1189
-1388
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
### 8.0.0
66

77
* [Breaking] Remove `pay_customer.payment_method_token` virtual attribute. Use `@pay_customer.update_payment_method(token)` instead.
8+
* [Breaking] Rename `customer` to `api_record`
9+
* [Breaking] Rename `update_customer!` to `update_api_record`
10+
* [Breaking] Rename `Pay::PaymentMethod#type` to `Pay::PaymentMethod#payment_method_type`
811
* Add Lemon Squeezy support
912
* Add `Pay.sync(params)` for automatically syncing Stripe Checkout Sessions and Paddle Billing transactions.
1013
* Lock Pay::Customer record when creating or updating Stripe customer to handle race conditions. #1027

UPGRADE.md

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
Follow this guide to upgrade older Pay versions. These may require database migrations and code changes.
44

5+
### ** Pay 7.0 to Pay 8.0**
6+
7+
```bash
8+
rails pay:install:migrations
9+
rails db:migrate
10+
```
11+
512
## **Pay 6.0 to Pay 7.0**
613

714
Pay 7 introduces some changes for Stripe and requires a few additional columns.

app/jobs/pay/customer_sync_job.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Pay
22
class CustomerSyncJob < ApplicationJob
33
def perform(pay_customer_id)
4-
Pay::Customer.find(pay_customer_id).update_customer!
4+
Pay::Customer.find(pay_customer_id).update_api_record
55
rescue ActiveRecord::RecordNotFound
66
Rails.logger.info "Couldn't find a Pay::Customer with ID = #{pay_customer_id}"
77
end

app/models/concerns/pay/routing.rb

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Pay
2+
module Routing
3+
extend ActiveSupport::Concern
4+
5+
included do
6+
include Rails.application.routes.url_helpers
7+
end
8+
9+
def default_url_options
10+
Rails.application.config.action_mailer.default_url_options || {}
11+
end
12+
end
13+
end

lib/pay/braintree/charge.rb app/models/pay/braintree/charge.rb

+5-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
module Pay
22
module Braintree
3-
class Charge
4-
attr_reader :pay_charge
5-
6-
delegate :processor_id, to: :pay_charge
7-
3+
class Charge < Pay::Charge
84
def self.sync(charge_id, object: nil, try: 0, retries: 1)
95
object ||= Pay.braintree_gateway.transaction.find(charge_id)
106

@@ -22,19 +18,16 @@ def self.sync(charge_id, object: nil, try: 0, retries: 1)
2218
end
2319
end
2420

25-
def initialize(pay_charge)
26-
@pay_charge = pay_charge
27-
end
28-
29-
def charge
21+
def api_record
3022
Pay.braintree_gateway.transaction.find(processor_id)
3123
rescue ::Braintree::Braintree::Error => e
3224
raise Pay::Braintree::Error, e
3325
end
3426

35-
def refund!(amount_to_refund)
27+
def refund!(amount_to_refund = nil)
28+
amount_to_refund ||= amount
3629
Pay.braintree_gateway.transaction.refund(processor_id, amount_to_refund / 100.0)
37-
pay_charge.update(amount_refunded: amount_to_refund)
30+
update(amount_refunded: amount_to_refund)
3831
rescue ::Braintree::BraintreeError => e
3932
raise Pay::Braintree::Error, e
4033
end

lib/pay/braintree/billable.rb app/models/pay/braintree/customer.rb

+29-50
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,34 @@
11
module Pay
22
module Braintree
3-
class Billable
4-
attr_reader :pay_customer
5-
6-
delegate :processor_id,
7-
:processor_id?,
8-
:email,
9-
:customer_name,
10-
to: :pay_customer
11-
12-
def initialize(pay_customer)
13-
@pay_customer = pay_customer
14-
end
3+
class Customer < Pay::Customer
4+
has_many :charges, dependent: :destroy, class_name: "Pay::Braintree::Charge"
5+
has_many :subscriptions, dependent: :destroy, class_name: "Pay::Braintree::Subscription"
6+
has_many :payment_methods, dependent: :destroy, class_name: "Pay::Braintree::PaymentMethod"
7+
has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::Braintree::PaymentMethod"
158

169
# Returns a hash of attributes for the Stripe::Customer object
17-
def customer_attributes
18-
owner = pay_customer.owner
19-
10+
def api_record_attributes
2011
attributes = case owner.class.pay_braintree_customer_attributes
2112
when Symbol
22-
owner.send(owner.class.pay_braintree_customer_attributes, pay_customer)
13+
owner.send(owner.class.pay_braintree_customer_attributes, self)
2314
when Proc
24-
owner.class.pay_braintree_customer_attributes.call(pay_customer)
15+
owner.class.pay_braintree_customer_attributes.call(self)
2516
end
2617

27-
# Guard against attributes being returned nil
28-
attributes ||= {}
29-
3018
first_name, last_name = customer_name.split(" ", 2)
31-
{email: email, first_name: first_name, last_name: last_name}.merge(attributes)
19+
{email: email, first_name: first_name, last_name: last_name}.merge(attributes || {})
3220
end
3321

3422
# Retrieve the Braintree::Customer object
3523
#
3624
# - If no processor_id is present, creates a Customer.
37-
def customer
25+
def api_record
3826
if processor_id?
3927
gateway.customer.find(processor_id)
4028
else
41-
result = gateway.customer.create(customer_attributes)
29+
result = gateway.customer.create(api_record_attributes)
4230
raise Pay::Braintree::Error, result unless result.success?
43-
pay_customer.update!(processor_id: result.customer.id)
31+
update!(processor_id: result.customer.id)
4432
result.customer
4533
end
4634
rescue ::Braintree::AuthorizationError => e
@@ -51,15 +39,15 @@ def customer
5139

5240
# Syncs name and email to Braintree::Customer
5341
# You can also pass in other attributes that will be merged into the default attributes
54-
def update_customer!(**attributes)
55-
customer unless processor_id?
56-
gateway.customer.update(processor_id, customer_attributes.merge(attributes))
42+
def update_api_record(**attributes)
43+
api_record unless processor_id?
44+
gateway.customer.update(processor_id, api_record_attributes.merge(attributes))
5745
end
5846

5947
def charge(amount, options = {})
6048
args = {
6149
amount: amount.to_i / 100.0,
62-
customer_id: customer.id,
50+
customer_id: processor_id || api_record.id,
6351
options: {submit_for_settlement: true},
6452
custom_fields: options.delete(:metadata)
6553
}.merge(options)
@@ -75,7 +63,7 @@ def charge(amount, options = {})
7563
end
7664

7765
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
78-
token = customer.payment_methods.find(&:default?).try(:token)
66+
token = api_record.payment_methods.find(&:default?).try(:token)
7967
raise Pay::Error, "Customer has no default payment method" if token.nil?
8068

8169
# Standardize the trial period options
@@ -89,12 +77,15 @@ def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **opt
8977
result = gateway.subscription.create(subscription_options)
9078
raise Pay::Braintree::Error, result unless result.success?
9179

92-
subscription = pay_customer.subscriptions.create!(
80+
# Braintree returns dates without time zones, so we'll assume they're UTC
81+
trial_end_date = result.subscription.trial_period.present? ? result.subscription.first_billing_date.end_of_day : nil
82+
83+
subscription = subscriptions.create!(
9384
name: name,
9485
processor_id: result.subscription.id,
9586
processor_plan: plan,
9687
status: :active,
97-
trial_ends_at: trial_end_date(result.subscription),
88+
trial_ends_at: trial_end_date,
9889
ends_at: nil,
9990
metadata: metadata
10091
)
@@ -111,10 +102,8 @@ def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **opt
111102
end
112103

113104
def add_payment_method(token, default: false)
114-
customer unless processor_id?
115-
116105
result = gateway.payment_method.create(
117-
customer_id: processor_id,
106+
customer_id: processor_id || api_record.id,
118107
payment_method_nonce: token,
119108
options: {
120109
make_default: default,
@@ -126,7 +115,7 @@ def add_payment_method(token, default: false)
126115
pay_payment_method = save_payment_method(result.payment_method, default: default)
127116

128117
# Update existing subscriptions to the new payment method
129-
pay_customer.subscriptions.each do |subscription|
118+
subscriptions.each do |subscription|
130119
if subscription.active?
131120
gateway.subscription.update(subscription.processor_id, {payment_method_token: token})
132121
end
@@ -139,16 +128,6 @@ def add_payment_method(token, default: false)
139128
raise Pay::Braintree::Error, e
140129
end
141130

142-
def trial_end_date(subscription)
143-
return unless subscription.trial_period
144-
# Braintree returns dates without time zones, so we'll assume they're UTC
145-
subscription.first_billing_date.end_of_day
146-
end
147-
148-
def processor_subscription(subscription_id, options = {})
149-
gateway.subscription.find(subscription_id)
150-
end
151-
152131
def save_transaction(transaction)
153132
attrs = card_details_for_braintree_transaction(transaction)
154133
attrs[:amount] = transaction.amount.to_f * 100
@@ -159,7 +138,7 @@ def save_transaction(transaction)
159138

160139
# Associate charge with subscription if we can
161140
if transaction.subscription_id
162-
pay_subscription = pay_customer.subscriptions.find_by(processor_id: transaction.subscription_id)
141+
pay_subscription = subscriptions.find_by(processor_id: transaction.subscription_id)
163142
pay_subscription ||= Pay::Braintree::Subscription.sync(transaction.subscription_id)
164143

165144
if pay_subscription
@@ -168,7 +147,7 @@ def save_transaction(transaction)
168147
end
169148
end
170149

171-
charge = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
150+
charge = charges.find_or_initialize_by(processor_id: transaction.id)
172151
charge.update!(attrs)
173152
charge
174153
end
@@ -219,13 +198,13 @@ def save_payment_method(payment_method, default:)
219198
}
220199
end
221200

222-
pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.token).first_or_initialize
201+
pay_payment_method = payment_methods.where(processor_id: payment_method.token).first_or_initialize
223202

224-
pay_customer.payment_methods.update_all(default: false) if default
203+
payment_methods.update_all(default: false) if default
225204
pay_payment_method.update!(attributes.merge(default: default))
226205

227206
# Reload the Rails association
228-
pay_customer.reload_default_payment_method if default
207+
reload_default_payment_method if default
229208

230209
pay_payment_method
231210
end

lib/pay/braintree/payment_method.rb app/models/pay/braintree/payment_method.rb

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
module Pay
22
module Braintree
3-
class PaymentMethod
4-
attr_reader :pay_payment_method
5-
6-
delegate :customer, :processor_id, to: :pay_payment_method
7-
3+
class PaymentMethod < Pay::PaymentMethod
84
def self.sync(id, object: nil, try: 0, retries: 1)
95
object ||= Pay.braintree_gateway.payment_method.find(id)
106

@@ -14,10 +10,6 @@ def self.sync(id, object: nil, try: 0, retries: 1)
1410
pay_customer.save_payment_method(object, default: object.default?)
1511
end
1612

17-
def initialize(pay_payment_method)
18-
@pay_payment_method = pay_payment_method
19-
end
20-
2113
# Sets payment method as default on Stripe
2214
def make_default!
2315
result = gateway.customer.update(customer.processor_id, default_payment_method_token: processor_id)

0 commit comments

Comments
 (0)