pay 2.5.0 → 2.6.4
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.
- checksums.yaml +4 -4
- data/README.md +10 -2
- data/app/models/pay/charge.rb +22 -3
- data/app/models/pay/subscription.rb +23 -24
- data/app/views/pay/stripe/_checkout_button.html.erb +21 -0
- data/lib/pay.rb +12 -14
- data/lib/pay/billable.rb +44 -33
- data/lib/pay/billable/sync_email.rb +1 -1
- data/lib/pay/braintree.rb +34 -16
- data/lib/pay/braintree/authorization_error.rb +9 -0
- data/lib/pay/braintree/billable.rb +33 -30
- data/lib/pay/braintree/charge.rb +8 -10
- data/lib/pay/braintree/error.rb +9 -0
- data/lib/pay/braintree/subscription.rb +34 -15
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
- data/lib/pay/engine.rb +0 -22
- data/lib/pay/errors.rb +0 -44
- data/lib/pay/fake_processor.rb +8 -0
- data/lib/pay/fake_processor/billable.rb +60 -0
- data/lib/pay/fake_processor/charge.rb +21 -0
- data/lib/pay/fake_processor/error.rb +6 -0
- data/lib/pay/fake_processor/subscription.rb +55 -0
- data/lib/pay/paddle.rb +30 -16
- data/lib/pay/paddle/billable.rb +26 -22
- data/lib/pay/paddle/charge.rb +8 -12
- data/lib/pay/paddle/error.rb +9 -0
- data/lib/pay/paddle/subscription.rb +33 -18
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +1 -1
- data/lib/pay/stripe.rb +62 -14
- data/lib/pay/stripe/billable.rb +136 -69
- data/lib/pay/stripe/charge.rb +9 -15
- data/lib/pay/stripe/error.rb +9 -0
- data/lib/pay/stripe/subscription.rb +31 -11
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +1 -20
- data/lib/pay/stripe/webhooks/customer_updated.rb +1 -1
- data/lib/pay/stripe/webhooks/payment_method_updated.rb +1 -1
- data/lib/pay/version.rb +1 -1
- data/lib/pay/webhooks.rb +13 -0
- metadata +16 -56
- data/lib/pay/braintree/webhooks.rb +0 -11
- data/lib/pay/paddle/webhooks.rb +0 -9
- data/lib/pay/stripe/webhooks.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a72ff3544c6fa85675a96d650f152b8f3593211f0c205a5a10cddca5d1f7c35
|
4
|
+
data.tar.gz: b7593ef07b57654f4ee566cf2fc538b495d22a9667ec90c6695759a464968dae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 572f4f2989bce0a3afa7a483585e3e6f7e96f0487eb34bf5cb790a60a3b66bc0eeafa4ed48a011e00232770b565d1c592491eb613465054cf5de34bae58b168c
|
7
|
+
data.tar.gz: cd5059977bd91e4c72b8c51a7f8584b23d15e7bc988ca3421400e39232b781481271a57f06777c71c83cfee73bcab03312de35734c3138e3101e2aa125e4db27
|
data/README.md
CHANGED
@@ -8,9 +8,10 @@ Pay is a payments engine for Ruby on Rails 4.2 and higher.
|
|
8
8
|
|
9
9
|
**Current Payment Providers**
|
10
10
|
|
11
|
-
- Stripe ([
|
11
|
+
- Stripe ([SCA Compatible](https://stripe.com/docs/strong-customer-authentication) using API version `2020-08-27`)
|
12
|
+
- Paddle (SCA Compatible & supports PayPal)
|
12
13
|
- Braintree (supports PayPal)
|
13
|
-
-
|
14
|
+
- [Fake Processor](docs/fake_processor.md)
|
14
15
|
|
15
16
|
Want to add a new payment provider? Contributions are welcome and the instructions [are here](https://github.com/jasoncharnes/pay/wiki/New-Payment-Provider).
|
16
17
|
|
@@ -48,6 +49,13 @@ And then execute:
|
|
48
49
|
bundle
|
49
50
|
```
|
50
51
|
|
52
|
+
Make sure you've configured your ActionMailer default_url_options so Pay can generate links to for features like Stripe Checkout.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# config/application.rb
|
56
|
+
config.action_mailer.default_url_options = { host: "example.com" }
|
57
|
+
```
|
58
|
+
|
51
59
|
#### Migrations
|
52
60
|
|
53
61
|
To add the migrations to your application, run the following migration:
|
data/app/models/pay/charge.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Pay
|
2
|
-
class Charge < ApplicationRecord
|
2
|
+
class Charge < Pay::ApplicationRecord
|
3
3
|
self.table_name = Pay.chargeable_table
|
4
4
|
|
5
5
|
# Only serialize for non-json columns
|
@@ -18,13 +18,32 @@ module Pay
|
|
18
18
|
validates :processor_id, presence: true
|
19
19
|
validates :card_type, presence: true
|
20
20
|
|
21
|
+
store_accessor :data, :paddle_receipt_url
|
22
|
+
|
23
|
+
# Helpers for payment processors
|
24
|
+
%w[braintree stripe paddle].each do |processor_name|
|
25
|
+
define_method "#{processor_name}?" do
|
26
|
+
processor == processor_name
|
27
|
+
end
|
28
|
+
|
29
|
+
scope processor_name, -> { where(processor: processor_name) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def payment_processor
|
33
|
+
@payment_processor ||= payment_processor_for(processor).new(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def payment_processor_for(name)
|
37
|
+
"Pay::#{name.to_s.classify}::Charge".constantize
|
38
|
+
end
|
39
|
+
|
21
40
|
def processor_charge
|
22
|
-
|
41
|
+
payment_processor.charge
|
23
42
|
end
|
24
43
|
|
25
44
|
def refund!(refund_amount = nil)
|
26
45
|
refund_amount ||= amount
|
27
|
-
|
46
|
+
payment_processor.refund!(refund_amount)
|
28
47
|
end
|
29
48
|
|
30
49
|
def charged_to
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Pay
|
2
|
-
class Subscription < ApplicationRecord
|
2
|
+
class Subscription < Pay::ApplicationRecord
|
3
3
|
self.table_name = Pay.subscription_table
|
4
4
|
|
5
5
|
STATUSES = %w[incomplete incomplete_expired trialing active past_due canceled unpaid paused]
|
@@ -27,6 +27,11 @@ module Pay
|
|
27
27
|
scope :incomplete, -> { where(status: :incomplete) }
|
28
28
|
scope :past_due, -> { where(status: :past_due) }
|
29
29
|
|
30
|
+
# TODO: Include these with a module
|
31
|
+
store_accessor :data, :paddle_update_url
|
32
|
+
store_accessor :data, :paddle_cancel_url
|
33
|
+
store_accessor :data, :paddle_paused_from
|
34
|
+
|
30
35
|
attribute :prorate, :boolean, default: true
|
31
36
|
|
32
37
|
# Helpers for payment processors
|
@@ -38,6 +43,21 @@ module Pay
|
|
38
43
|
scope processor_name, -> { where(processor: processor_name) }
|
39
44
|
end
|
40
45
|
|
46
|
+
def payment_processor
|
47
|
+
@payment_processor ||= payment_processor_for(processor).new(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def payment_processor_for(name)
|
51
|
+
"Pay::#{name.to_s.classify}::Subscription".constantize
|
52
|
+
end
|
53
|
+
|
54
|
+
delegate :on_grace_period?,
|
55
|
+
:paused?,
|
56
|
+
:pause,
|
57
|
+
:cancel,
|
58
|
+
:cancel_now!,
|
59
|
+
to: :payment_processor
|
60
|
+
|
41
61
|
def no_prorate
|
42
62
|
self.prorate = false
|
43
63
|
end
|
@@ -58,11 +78,6 @@ module Pay
|
|
58
78
|
canceled?
|
59
79
|
end
|
60
80
|
|
61
|
-
def on_grace_period?
|
62
|
-
return unless processor?
|
63
|
-
send("#{processor}_on_grace_period?")
|
64
|
-
end
|
65
|
-
|
66
81
|
def active?
|
67
82
|
["trialing", "active"].include?(status) && (ends_at.nil? || on_grace_period? || on_trial?)
|
68
83
|
end
|
@@ -79,30 +94,14 @@ module Pay
|
|
79
94
|
past_due? || incomplete?
|
80
95
|
end
|
81
96
|
|
82
|
-
def paused?
|
83
|
-
send("#{processor}_paused?")
|
84
|
-
end
|
85
|
-
|
86
|
-
def pause
|
87
|
-
send("#{processor}_pause")
|
88
|
-
end
|
89
|
-
|
90
|
-
def cancel
|
91
|
-
send("#{processor}_cancel")
|
92
|
-
end
|
93
|
-
|
94
|
-
def cancel_now!
|
95
|
-
send("#{processor}_cancel_now!")
|
96
|
-
end
|
97
|
-
|
98
97
|
def resume
|
99
|
-
|
98
|
+
payment_processor.resume
|
100
99
|
update(ends_at: nil, status: "active")
|
101
100
|
self
|
102
101
|
end
|
103
102
|
|
104
103
|
def swap(plan)
|
105
|
-
|
104
|
+
payment_processor.swap(plan)
|
106
105
|
update(processor_plan: plan, ends_at: nil)
|
107
106
|
end
|
108
107
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<%= button_tag title,
|
2
|
+
id: "checkout-#{session.id}",
|
3
|
+
class: local_assigns[:class],
|
4
|
+
style: (local_assigns[:class] || local_assigns[:style]) ? local_assigns[:style] : 'background-color:#6772E5;color:#FFF;padding:8px 12px;border:0;border-radius:4px;font-size:1em'
|
5
|
+
%>
|
6
|
+
<%= tag.div id: "error-for-#{session.id}" %>
|
7
|
+
|
8
|
+
<script>
|
9
|
+
(() => {
|
10
|
+
const checkoutButton = document.getElementById("checkout-<%= session.id %>");
|
11
|
+
checkoutButton.addEventListener('click', function () {
|
12
|
+
Stripe("<%= Pay::Stripe.public_key %>").redirectToCheckout({
|
13
|
+
sessionId: '<%= session.id %>'
|
14
|
+
}).then(function (result) {
|
15
|
+
if (result.error) {
|
16
|
+
document.getElementById('error-message').innerText = result.error.message;
|
17
|
+
}
|
18
|
+
});
|
19
|
+
});
|
20
|
+
})()
|
21
|
+
</script>
|
data/lib/pay.rb
CHANGED
@@ -1,22 +1,20 @@
|
|
1
1
|
require "pay/version"
|
2
2
|
require "pay/engine"
|
3
|
-
require "pay/billable"
|
4
|
-
require "pay/receipts"
|
5
|
-
require "pay/payment"
|
6
3
|
require "pay/errors"
|
7
4
|
|
8
5
|
module Pay
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
6
|
+
autoload :Billable, "pay/billable"
|
7
|
+
autoload :Env, "pay/env"
|
8
|
+
autoload :Payment, "pay/payment"
|
9
|
+
autoload :Receipts, "pay/receipts"
|
10
|
+
|
11
|
+
# Payment processors
|
12
|
+
autoload :Braintree, "pay/braintree"
|
13
|
+
autoload :FakeProcessor, "pay/fake_processor"
|
14
|
+
autoload :Paddle, "pay/paddle"
|
15
|
+
autoload :Stripe, "pay/stripe"
|
16
|
+
|
17
|
+
autoload :Webhooks, "pay/webhooks"
|
20
18
|
|
21
19
|
# Define who owns the subscription
|
22
20
|
mattr_accessor :billable_class
|
data/lib/pay/billable.rb
CHANGED
@@ -17,9 +17,6 @@ module Pay
|
|
17
17
|
|
18
18
|
included do |base|
|
19
19
|
include Pay::Billable::SyncEmail
|
20
|
-
include Pay::Stripe::Billable if defined? ::Stripe
|
21
|
-
include Pay::Braintree::Billable if defined? ::Braintree
|
22
|
-
include Pay::Paddle::Billable if defined? ::PaddlePay
|
23
20
|
|
24
21
|
has_many :charges, class_name: Pay.chargeable_class, foreign_key: :owner_id, inverse_of: :owner, as: :owner
|
25
22
|
has_many :subscriptions, class_name: Pay.subscription_class, foreign_key: :owner_id, inverse_of: :owner, as: :owner
|
@@ -27,19 +24,39 @@ module Pay
|
|
27
24
|
attribute :plan, :string
|
28
25
|
attribute :quantity, :integer
|
29
26
|
attribute :card_token, :string
|
27
|
+
attribute :pay_fake_processor_allowed, :boolean, default: false
|
28
|
+
|
29
|
+
validate :pay_fake_processor_is_allowed, if: :processor_changed?
|
30
|
+
end
|
31
|
+
|
32
|
+
def payment_processor
|
33
|
+
@payment_processor ||= payment_processor_for(processor).new(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def payment_processor_for(name)
|
37
|
+
"Pay::#{name.to_s.classify}::Billable".constantize
|
30
38
|
end
|
31
39
|
|
40
|
+
# Reset the payment processor when it changes
|
32
41
|
def processor=(value)
|
33
42
|
super(value)
|
34
43
|
self.processor_id = nil if processor_changed?
|
44
|
+
@payment_processor = nil
|
35
45
|
end
|
36
46
|
|
47
|
+
def processor
|
48
|
+
super&.inquiry
|
49
|
+
end
|
50
|
+
|
51
|
+
delegate :charge, to: :payment_processor
|
52
|
+
delegate :subscribe, to: :payment_processor
|
53
|
+
delegate :update_card, to: :payment_processor
|
54
|
+
|
37
55
|
def customer
|
38
|
-
check_for_processor
|
39
56
|
raise Pay::Error, I18n.t("errors.email_required") if email.nil?
|
40
57
|
|
41
|
-
customer =
|
42
|
-
update_card(card_token) if card_token.present?
|
58
|
+
customer = payment_processor.customer
|
59
|
+
payment_processor.update_card(card_token) if card_token.present?
|
43
60
|
customer
|
44
61
|
end
|
45
62
|
|
@@ -47,20 +64,9 @@ module Pay
|
|
47
64
|
[try(:first_name), try(:last_name)].compact.join(" ")
|
48
65
|
end
|
49
66
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
56
|
-
check_for_processor
|
57
|
-
send("create_#{processor}_subscription", name, plan, options)
|
58
|
-
end
|
59
|
-
|
60
|
-
def update_card(token)
|
61
|
-
check_for_processor
|
62
|
-
customer if processor_id.nil?
|
63
|
-
send("update_#{processor}_card", token)
|
67
|
+
def create_setup_intent
|
68
|
+
ActiveSupport::Deprecation.warn("This method will be removed in the next release. Use `@billable.payment_processor.create_setup_intent` instead.")
|
69
|
+
payment_processor.create_setup_intent
|
64
70
|
end
|
65
71
|
|
66
72
|
def on_trial?(name: Pay.default_product_name, plan: nil)
|
@@ -77,8 +83,7 @@ module Pay
|
|
77
83
|
end
|
78
84
|
|
79
85
|
def processor_subscription(subscription_id, options = {})
|
80
|
-
|
81
|
-
send("#{processor}_subscription", subscription_id, options)
|
86
|
+
payment_processor.processor_subscription(subscription_id, options)
|
82
87
|
end
|
83
88
|
|
84
89
|
def subscribed?(name: Pay.default_product_name, processor_plan: nil)
|
@@ -100,26 +105,31 @@ module Pay
|
|
100
105
|
end
|
101
106
|
|
102
107
|
def invoice!(options = {})
|
103
|
-
|
108
|
+
ActiveSupport::Deprecation.warn("This will be removed in the next release. Use `@billable.payment_processor.invoice!` instead.")
|
109
|
+
payment_processor.invoice!(options)
|
104
110
|
end
|
105
111
|
|
106
112
|
def upcoming_invoice
|
107
|
-
|
113
|
+
ActiveSupport::Deprecation.warn("This will be removed in the next release. Use `@billable.payment_processor.upcoming_invoice` instead.")
|
114
|
+
payment_processor.upcoming_invoice
|
108
115
|
end
|
109
116
|
|
110
117
|
def stripe?
|
118
|
+
ActiveSupport::Deprecation.warn("This will be removed in the next release. Use `@billable.processor.stripe?` instead.")
|
111
119
|
processor == "stripe"
|
112
120
|
end
|
113
121
|
|
114
122
|
def braintree?
|
123
|
+
ActiveSupport::Deprecation.warn("This will be removed in the next release. Use `@billable.processor.braintree?` instead.")
|
115
124
|
processor == "braintree"
|
116
125
|
end
|
117
126
|
|
118
127
|
def paypal?
|
119
|
-
|
128
|
+
card_type == "PayPal"
|
120
129
|
end
|
121
130
|
|
122
131
|
def paddle?
|
132
|
+
ActiveSupport::Deprecation.warn("This will be removed in the next release. Use `@billable.processor.paddle?` instead.")
|
123
133
|
processor == "paddle"
|
124
134
|
end
|
125
135
|
|
@@ -127,14 +137,8 @@ module Pay
|
|
127
137
|
subscription(name: name)&.has_incomplete_payment?
|
128
138
|
end
|
129
139
|
|
130
|
-
private
|
131
|
-
|
132
|
-
def check_for_processor
|
133
|
-
raise StandardError, I18n.t("errors.no_processor", class_name: self.class.name) unless processor
|
134
|
-
end
|
135
|
-
|
136
140
|
# Used for creating a Pay::Subscription in the database
|
137
|
-
def
|
141
|
+
def create_pay_subscription(subscription, processor, name, plan, options = {})
|
138
142
|
options[:quantity] ||= 1
|
139
143
|
|
140
144
|
options.merge!(
|
@@ -142,15 +146,22 @@ module Pay
|
|
142
146
|
processor: processor,
|
143
147
|
processor_id: subscription.id,
|
144
148
|
processor_plan: plan,
|
145
|
-
trial_ends_at:
|
149
|
+
trial_ends_at: payment_processor.trial_end_date(subscription),
|
146
150
|
ends_at: nil
|
147
151
|
)
|
148
152
|
subscriptions.create!(options)
|
149
153
|
end
|
150
154
|
|
155
|
+
private
|
156
|
+
|
151
157
|
def default_generic_trial?(name, plan)
|
152
158
|
# Generic trials don't have plans or custom names
|
153
159
|
plan.nil? && name == "default" && on_generic_trial?
|
154
160
|
end
|
161
|
+
|
162
|
+
def pay_fake_processor_is_allowed
|
163
|
+
return unless processor == "fake_processor"
|
164
|
+
errors.add(:processor, "must be a valid payment processor") unless pay_fake_processor_allowed?
|
165
|
+
end
|
155
166
|
end
|
156
167
|
end
|
data/lib/pay/braintree.rb
CHANGED
@@ -1,16 +1,24 @@
|
|
1
|
-
require "pay/env"
|
2
|
-
require "pay/braintree/billable"
|
3
|
-
require "pay/braintree/charge"
|
4
|
-
require "pay/braintree/subscription"
|
5
|
-
require "pay/braintree/webhooks"
|
6
|
-
|
7
1
|
module Pay
|
8
2
|
module Braintree
|
9
|
-
|
3
|
+
autoload :Billable, "pay/braintree/billable"
|
4
|
+
autoload :Charge, "pay/braintree/charge"
|
5
|
+
autoload :Subscription, "pay/braintree/subscription"
|
6
|
+
autoload :Error, "pay/braintree/error"
|
7
|
+
autoload :AuthorizationError, "pay/braintree/authorization_error"
|
8
|
+
|
9
|
+
module Webhooks
|
10
|
+
autoload :SubscriptionCanceled, "pay/braintree/webhooks/subscription_canceled"
|
11
|
+
autoload :SubscriptionChargedSuccessfully, "pay/braintree/webhooks/subscription_charged_successfully"
|
12
|
+
autoload :SubscriptionChargedUnsuccessfully, "pay/braintree/webhooks/subscription_charged_unsuccessfully"
|
13
|
+
autoload :SubscriptionExpired, "pay/braintree/webhooks/subscription_expired"
|
14
|
+
autoload :SubscriptionTrialEnded, "pay/braintree/webhooks/subscription_trial_ended"
|
15
|
+
autoload :SubscriptionWentActive, "pay/braintree/webhooks/subscription_went_active"
|
16
|
+
autoload :SubscriptionWentPastDue, "pay/braintree/webhooks/subscription_went_past_due"
|
17
|
+
end
|
10
18
|
|
11
|
-
extend
|
19
|
+
extend Env
|
12
20
|
|
13
|
-
def setup
|
21
|
+
def self.setup
|
14
22
|
Pay.braintree_gateway = ::Braintree::Gateway.new(
|
15
23
|
environment: environment.to_sym,
|
16
24
|
merchant_id: merchant_id,
|
@@ -18,25 +26,35 @@ module Pay
|
|
18
26
|
private_key: private_key
|
19
27
|
)
|
20
28
|
|
21
|
-
|
22
|
-
Pay.subscription_model.include Pay::Braintree::Subscription
|
23
|
-
Pay.billable_models.each { |model| model.include Pay::Braintree::Billable }
|
29
|
+
configure_webhooks
|
24
30
|
end
|
25
31
|
|
26
|
-
def public_key
|
32
|
+
def self.public_key
|
27
33
|
find_value_by_name(:braintree, :public_key)
|
28
34
|
end
|
29
35
|
|
30
|
-
def private_key
|
36
|
+
def self.private_key
|
31
37
|
find_value_by_name(:braintree, :private_key)
|
32
38
|
end
|
33
39
|
|
34
|
-
def merchant_id
|
40
|
+
def self.merchant_id
|
35
41
|
find_value_by_name(:braintree, :merchant_id)
|
36
42
|
end
|
37
43
|
|
38
|
-
def environment
|
44
|
+
def self.environment
|
39
45
|
find_value_by_name(:braintree, :environment) || "sandbox"
|
40
46
|
end
|
47
|
+
|
48
|
+
def self.configure_webhooks
|
49
|
+
Pay::Webhooks.configure do |events|
|
50
|
+
events.subscribe "braintree.subscription_canceled", Pay::Braintree::Webhooks::SubscriptionCanceled.new
|
51
|
+
events.subscribe "braintree.subscription_charged_successfully", Pay::Braintree::Webhooks::SubscriptionChargedSuccessfully.new
|
52
|
+
events.subscribe "braintree.subscription_charged_unsuccessfully", Pay::Braintree::Webhooks::SubscriptionChargedUnsuccessfully.new
|
53
|
+
events.subscribe "braintree.subscription_expired", Pay::Braintree::Webhooks::SubscriptionExpired.new
|
54
|
+
events.subscribe "braintree.subscription_trial_ended", Pay::Braintree::Webhooks::SubscriptionTrialEnded.new
|
55
|
+
events.subscribe "braintree.subscription_went_active", Pay::Braintree::Webhooks::SubscriptionWentActive.new
|
56
|
+
events.subscribe "braintree.subscription_went_past_due", Pay::Braintree::Webhooks::SubscriptionWentPastDue.new
|
57
|
+
end
|
58
|
+
end
|
41
59
|
end
|
42
60
|
end
|