pay 7.2.1 → 8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
- data/app/controllers/pay/webhooks/stripe_controller.rb +2 -1
- data/app/jobs/pay/customer_sync_job.rb +1 -1
- data/app/models/concerns/pay/routing.rb +13 -0
- data/{lib → app/models}/pay/braintree/charge.rb +5 -12
- data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +31 -71
- data/{lib → app/models}/pay/braintree/payment_method.rb +1 -9
- data/{lib → app/models}/pay/braintree/subscription.rb +14 -52
- data/app/models/pay/charge.rb +8 -27
- data/app/models/pay/customer.rb +2 -15
- data/app/models/pay/fake_processor/charge.rb +13 -0
- data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +22 -35
- data/{lib → app/models}/pay/fake_processor/merchant.rb +2 -9
- data/app/models/pay/fake_processor/payment_method.rb +11 -0
- data/app/models/pay/fake_processor/subscription.rb +60 -0
- data/app/models/pay/lemon_squeezy/charge.rb +86 -0
- data/app/models/pay/lemon_squeezy/customer.rb +78 -0
- data/app/models/pay/lemon_squeezy/payment_method.rb +27 -0
- data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
- data/app/models/pay/merchant.rb +0 -11
- data/{lib → app/models}/pay/paddle_billing/charge.rb +2 -8
- data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +18 -35
- data/{lib → app/models}/pay/paddle_billing/payment_method.rb +2 -12
- data/{lib → app/models}/pay/paddle_billing/subscription.rb +9 -33
- data/{lib → app/models}/pay/paddle_classic/charge.rb +13 -18
- data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +9 -31
- data/{lib → app/models}/pay/paddle_classic/payment_method.rb +1 -11
- data/{lib → app/models}/pay/paddle_classic/subscription.rb +11 -36
- data/app/models/pay/payment_method.rb +0 -5
- data/{lib → app/models}/pay/stripe/charge.rb +6 -22
- data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +73 -108
- data/{lib → app/models}/pay/stripe/merchant.rb +2 -11
- data/{lib → app/models}/pay/stripe/payment_method.rb +2 -10
- data/{lib → app/models}/pay/stripe/subscription.rb +37 -71
- data/app/models/pay/subscription.rb +7 -37
- data/app/models/pay/webhook.rb +2 -0
- data/config/routes.rb +1 -0
- data/db/migrate/2_add_pay_sti_columns.rb +24 -0
- data/lib/pay/attributes.rb +11 -3
- data/lib/pay/braintree.rb +25 -6
- data/lib/pay/engine.rb +2 -0
- data/lib/pay/fake_processor.rb +2 -6
- data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
- data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
- data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
- data/lib/pay/lemon_squeezy.rb +56 -104
- data/lib/pay/paddle_billing.rb +15 -6
- data/lib/pay/paddle_classic.rb +11 -9
- data/lib/pay/receipts.rb +6 -6
- data/lib/pay/stripe/webhooks/checkout_session_completed.rb +1 -1
- data/lib/pay/stripe/webhooks/customer_updated.rb +1 -1
- data/lib/pay/stripe/webhooks/subscription_trial_will_end.rb +1 -1
- data/lib/pay/stripe.rb +21 -7
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +12 -1
- metadata +34 -38
- data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
- data/lib/pay/braintree/authorization_error.rb +0 -9
- data/lib/pay/braintree/error.rb +0 -23
- data/lib/pay/fake_processor/charge.rb +0 -21
- data/lib/pay/fake_processor/error.rb +0 -6
- data/lib/pay/fake_processor/payment_method.rb +0 -21
- data/lib/pay/fake_processor/subscription.rb +0 -90
- data/lib/pay/lemon_squeezy/billable.rb +0 -90
- data/lib/pay/lemon_squeezy/charge.rb +0 -68
- data/lib/pay/lemon_squeezy/error.rb +0 -7
- data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
- data/lib/pay/lemon_squeezy/subscription.rb +0 -185
- data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
- data/lib/pay/paddle_billing/error.rb +0 -7
- data/lib/pay/paddle_classic/error.rb +0 -7
- data/lib/pay/stripe/error.rb +0 -7
@@ -1,27 +1,6 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleBilling
|
3
|
-
class Subscription
|
4
|
-
attr_reader :pay_subscription
|
5
|
-
|
6
|
-
delegate :active?,
|
7
|
-
:canceled?,
|
8
|
-
:on_grace_period?,
|
9
|
-
:on_trial?,
|
10
|
-
:ends_at,
|
11
|
-
:name,
|
12
|
-
:owner,
|
13
|
-
:pause_starts_at,
|
14
|
-
:pause_starts_at?,
|
15
|
-
:processor_id,
|
16
|
-
:processor_plan,
|
17
|
-
:processor_subscription,
|
18
|
-
:prorate,
|
19
|
-
:prorate?,
|
20
|
-
:quantity,
|
21
|
-
:quantity?,
|
22
|
-
:trial_ends_at,
|
23
|
-
to: :pay_subscription
|
24
|
-
|
3
|
+
class Subscription < Pay::Subscription
|
25
4
|
def self.sync_from_transaction(transaction_id)
|
26
5
|
transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
|
27
6
|
sync(transaction.subscription_id) if transaction.subscription_id
|
@@ -85,12 +64,8 @@ module Pay
|
|
85
64
|
end
|
86
65
|
end
|
87
66
|
|
88
|
-
def
|
89
|
-
@
|
90
|
-
end
|
91
|
-
|
92
|
-
def subscription(**options)
|
93
|
-
@paddle_billing_subscription ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
|
67
|
+
def api_record(**options)
|
68
|
+
@api_record ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
|
94
69
|
end
|
95
70
|
|
96
71
|
# Get a transaction to update payment method
|
@@ -107,7 +82,7 @@ module Pay
|
|
107
82
|
id: processor_id,
|
108
83
|
effective_from: options.fetch(:effective_from, (paused? ? "immediately" : "next_billing_period"))
|
109
84
|
)
|
110
|
-
|
85
|
+
update(
|
111
86
|
status: response.status,
|
112
87
|
ends_at: response.scheduled_change&.effective_at || Time.current
|
113
88
|
)
|
@@ -128,6 +103,7 @@ module Pay
|
|
128
103
|
}]
|
129
104
|
|
130
105
|
::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
|
106
|
+
update(quantity: quantity)
|
131
107
|
rescue ::Paddle::Error => e
|
132
108
|
raise Pay::PaddleBilling::Error, e
|
133
109
|
end
|
@@ -139,12 +115,12 @@ module Pay
|
|
139
115
|
end
|
140
116
|
|
141
117
|
def paused?
|
142
|
-
|
118
|
+
status == "paused"
|
143
119
|
end
|
144
120
|
|
145
121
|
def pause
|
146
122
|
response = ::Paddle::Subscription.pause(id: processor_id)
|
147
|
-
|
123
|
+
update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
|
148
124
|
rescue ::Paddle::Error => e
|
149
125
|
raise Pay::PaddleBilling::Error, e
|
150
126
|
end
|
@@ -166,7 +142,7 @@ module Pay
|
|
166
142
|
::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
|
167
143
|
end
|
168
144
|
|
169
|
-
|
145
|
+
update(ends_at: nil, status: :active, pause_starts_at: nil)
|
170
146
|
rescue ::Paddle::Error => e
|
171
147
|
raise Pay::PaddleBilling::Error, e
|
172
148
|
end
|
@@ -178,7 +154,7 @@ module Pay
|
|
178
154
|
}]
|
179
155
|
|
180
156
|
::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
|
181
|
-
|
157
|
+
update(processor_plan: plan, ends_at: nil, status: :active)
|
182
158
|
end
|
183
159
|
|
184
160
|
# Retries the latest invoice for a Past Due subscription
|
@@ -1,33 +1,28 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
|
-
class Charge
|
4
|
-
|
3
|
+
class Charge < Pay::Charge
|
4
|
+
store_accessor :data, :paddle_receipt_url
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(pay_charge)
|
9
|
-
@pay_charge = pay_charge
|
10
|
-
end
|
11
|
-
|
12
|
-
def charge
|
6
|
+
def api_record
|
13
7
|
return unless customer.subscription
|
8
|
+
|
14
9
|
payments = PaddleClassic.client.payments.list(subscription_id: customer.subscription.processor_id)
|
15
10
|
charges = payments.data.select { |p| p[:id].to_s == processor_id }
|
16
11
|
charges.try(:first)
|
17
|
-
rescue ::Paddle::Error => e
|
12
|
+
rescue ::Paddle::Classic::Error => e
|
18
13
|
raise Pay::PaddleClassic::Error, e
|
19
14
|
end
|
20
15
|
|
21
|
-
def refund!(amount_to_refund)
|
16
|
+
def refund!(amount_to_refund = nil)
|
22
17
|
return unless customer.subscription
|
18
|
+
amount_to_refund ||= amount
|
19
|
+
|
23
20
|
payments = PaddleClassic.client.payments.list(subscription_id: customer.subscription.processor_id, is_paid: 1)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
rescue ::Paddle::Error => e
|
21
|
+
raise Error, "Payment not found" unless payments.total > 0
|
22
|
+
|
23
|
+
PaddleClassic.client.payments.refund(order_id: payments.data.last[:id], amount: amount_to_refund)
|
24
|
+
update(amount_refunded: amount_to_refund)
|
25
|
+
rescue ::Paddle::Classic::Error => e
|
31
26
|
raise Pay::PaddleClassic::Error, e
|
32
27
|
end
|
33
28
|
end
|
@@ -1,29 +1,18 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
|
-
class
|
4
|
-
|
3
|
+
class Customer < Pay::Customer
|
4
|
+
has_many :charges, dependent: :destroy, class_name: "Pay::PaddleClassic::Charge"
|
5
|
+
has_many :subscriptions, dependent: :destroy, class_name: "Pay::PaddleClassic::Subscription"
|
6
|
+
has_many :payment_methods, dependent: :destroy, class_name: "Pay::PaddleClassic::PaymentMethod"
|
7
|
+
has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::PaddleClassic::PaymentMethod"
|
5
8
|
|
6
|
-
|
7
|
-
:processor_id?,
|
8
|
-
:email,
|
9
|
-
:customer_name,
|
10
|
-
:card_token,
|
11
|
-
to: :pay_customer
|
12
|
-
|
13
|
-
def initialize(pay_customer)
|
14
|
-
@pay_customer = pay_customer
|
15
|
-
end
|
16
|
-
|
17
|
-
def customer
|
18
|
-
# pass
|
9
|
+
def api_record
|
19
10
|
end
|
20
11
|
|
21
|
-
def
|
22
|
-
# pass
|
12
|
+
def update_api_record
|
23
13
|
end
|
24
14
|
|
25
15
|
def charge(amount, options = {})
|
26
|
-
subscription = pay_customer.subscription
|
27
16
|
return unless subscription.processor_id
|
28
17
|
raise Pay::Error, "A charge_name is required to create a one-time charge" if options[:charge_name].nil?
|
29
18
|
|
@@ -38,7 +27,7 @@ module Pay
|
|
38
27
|
# Lookup subscription payment method details
|
39
28
|
attributes.merge! Pay::PaddleClassic::PaymentMethod.payment_method_details_for(subscription_id: subscription.processor_id)
|
40
29
|
|
41
|
-
charge =
|
30
|
+
charge = charges.find_or_initialize_by(processor_id: response[:invoice_id])
|
42
31
|
charge.update(attributes)
|
43
32
|
charge
|
44
33
|
rescue ::Paddle::Error => e
|
@@ -52,18 +41,7 @@ module Pay
|
|
52
41
|
# Paddle does not use payment method tokens. The method signature has it here
|
53
42
|
# to have a uniform API with the other payment processors.
|
54
43
|
def add_payment_method(token = nil, default: true)
|
55
|
-
Pay::PaddleClassic::PaymentMethod.sync(pay_customer:
|
56
|
-
end
|
57
|
-
|
58
|
-
def trial_end_date(subscription)
|
59
|
-
return unless subscription.state == "trialing"
|
60
|
-
Time.zone.parse(subscription.next_payment[:date]).end_of_day
|
61
|
-
end
|
62
|
-
|
63
|
-
def processor_subscription(subscription_id, options = {})
|
64
|
-
PaddleClassic.client.users.list(subscription_id: subscription_id).data.try(:first)
|
65
|
-
rescue ::Paddle::Error => e
|
66
|
-
raise Pay::PaddleClassic::Error, e
|
44
|
+
Pay::PaddleClassic::PaymentMethod.sync(pay_customer: self)
|
67
45
|
end
|
68
46
|
end
|
69
47
|
end
|
@@ -1,10 +1,6 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
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
|
8
4
|
# Paddle doesn't provide PaymentMethod IDs, so we have to lookup via the Customer
|
9
5
|
def self.sync(pay_customer:, attributes: nil)
|
10
6
|
return unless pay_customer.subscription
|
@@ -44,15 +40,9 @@ module Pay
|
|
44
40
|
end
|
45
41
|
end
|
46
42
|
|
47
|
-
def initialize(pay_payment_method)
|
48
|
-
@pay_payment_method = pay_payment_method
|
49
|
-
end
|
50
|
-
|
51
|
-
# Sets payment method as default
|
52
43
|
def make_default!
|
53
44
|
end
|
54
45
|
|
55
|
-
# Remove payment method
|
56
46
|
def detach
|
57
47
|
end
|
58
48
|
end
|
@@ -1,27 +1,6 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
|
-
class Subscription
|
4
|
-
attr_reader :pay_subscription
|
5
|
-
|
6
|
-
delegate :active?,
|
7
|
-
:canceled?,
|
8
|
-
:on_grace_period?,
|
9
|
-
:on_trial?,
|
10
|
-
:ends_at,
|
11
|
-
:name,
|
12
|
-
:owner,
|
13
|
-
:pause_starts_at,
|
14
|
-
:pause_starts_at?,
|
15
|
-
:processor_id,
|
16
|
-
:processor_plan,
|
17
|
-
:processor_subscription,
|
18
|
-
:prorate,
|
19
|
-
:prorate?,
|
20
|
-
:quantity,
|
21
|
-
:quantity?,
|
22
|
-
:trial_ends_at,
|
23
|
-
to: :pay_subscription
|
24
|
-
|
3
|
+
class Subscription < Pay::Subscription
|
25
4
|
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
26
5
|
# Passthrough is not return from this API, so we can't use that
|
27
6
|
object ||= PaddleClassic.client.users.list(subscription_id: subscription_id).data.try(:first)
|
@@ -68,11 +47,7 @@ module Pay
|
|
68
47
|
end
|
69
48
|
end
|
70
49
|
|
71
|
-
def
|
72
|
-
@pay_subscription = pay_subscription
|
73
|
-
end
|
74
|
-
|
75
|
-
def subscription(**options)
|
50
|
+
def api_record(**options)
|
76
51
|
PaddleClassic.client.users.list(subscription_id: processor_id).data.try(:first)
|
77
52
|
rescue ::Paddle::Error => e
|
78
53
|
raise Pay::PaddleClassic::Error, e
|
@@ -87,17 +62,17 @@ module Pay
|
|
87
62
|
elsif paused?
|
88
63
|
pause_starts_at
|
89
64
|
else
|
90
|
-
Time.parse(
|
65
|
+
Time.parse(api_record.next_payment.date)
|
91
66
|
end
|
92
67
|
|
93
68
|
PaddleClassic.client.users.cancel(subscription_id: processor_id)
|
94
|
-
|
69
|
+
update(
|
95
70
|
status: (ends_at.future? ? :active : :canceled),
|
96
71
|
ends_at: ends_at
|
97
72
|
)
|
98
73
|
|
99
74
|
# Remove payment methods since customer cannot be reused after cancelling
|
100
|
-
Pay::PaymentMethod.where(customer_id:
|
75
|
+
Pay::PaymentMethod.where(customer_id: customer_id).destroy_all
|
101
76
|
rescue ::Paddle::Error => e
|
102
77
|
raise Pay::PaddleClassic::Error, e
|
103
78
|
end
|
@@ -106,10 +81,10 @@ module Pay
|
|
106
81
|
return if canceled?
|
107
82
|
|
108
83
|
PaddleClassic.client.users.cancel(subscription_id: processor_id)
|
109
|
-
|
84
|
+
update(status: :canceled, ends_at: Time.current)
|
110
85
|
|
111
86
|
# Remove payment methods since customer cannot be reused after cancelling
|
112
|
-
Pay::PaymentMethod.where(customer_id:
|
87
|
+
Pay::PaymentMethod.where(customer_id: customer_id).destroy_all
|
113
88
|
rescue ::Paddle::Error => e
|
114
89
|
raise Pay::PaddleClassic::Error, e
|
115
90
|
end
|
@@ -125,12 +100,12 @@ module Pay
|
|
125
100
|
end
|
126
101
|
|
127
102
|
def paused?
|
128
|
-
|
103
|
+
status == "paused"
|
129
104
|
end
|
130
105
|
|
131
106
|
def pause
|
132
107
|
response = PaddleClassic.client.users.pause(subscription_id: processor_id)
|
133
|
-
|
108
|
+
update(status: :paused, pause_starts_at: Time.zone.parse(response.dig(:next_payment, :date)))
|
134
109
|
rescue ::Paddle::Error => e
|
135
110
|
raise Pay::PaddleClassic::Error, e
|
136
111
|
end
|
@@ -145,7 +120,7 @@ module Pay
|
|
145
120
|
end
|
146
121
|
|
147
122
|
PaddleClassic.client.users.unpause(subscription_id: processor_id)
|
148
|
-
|
123
|
+
update(ends_at: nil, status: :active, pause_starts_at: nil)
|
149
124
|
rescue ::Paddle::Error => e
|
150
125
|
raise Pay::PaddleClassic::Error, e
|
151
126
|
end
|
@@ -157,7 +132,7 @@ module Pay
|
|
157
132
|
attributes[:quantity] = quantity if quantity?
|
158
133
|
PaddleClassic.client.users.update(subscription_id: processor_id, **attributes)
|
159
134
|
|
160
|
-
|
135
|
+
update(processor_plan: plan, ends_at: nil, status: :active)
|
161
136
|
rescue ::Paddle::Error => e
|
162
137
|
raise Pay::PaddleClassic::Error, e
|
163
138
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
module Pay
|
2
2
|
class PaymentMethod < Pay::ApplicationRecord
|
3
|
-
self.inheritance_column = nil
|
4
|
-
|
5
3
|
belongs_to :customer
|
6
4
|
|
7
5
|
store_accessor :data, :brand # Visa, Mastercard, Discover, PayPal
|
@@ -12,9 +10,6 @@ module Pay
|
|
12
10
|
store_accessor :data, :username
|
13
11
|
store_accessor :data, :bank
|
14
12
|
|
15
|
-
# Aliases to share PaymentMethodAttributes
|
16
|
-
alias_attribute :payment_method_type, :type
|
17
|
-
|
18
13
|
validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
|
19
14
|
|
20
15
|
def self.find_by_processor_and_id(processor, processor_id)
|
@@ -1,16 +1,7 @@
|
|
1
1
|
module Pay
|
2
2
|
module Stripe
|
3
|
-
class Charge
|
4
|
-
|
5
|
-
|
6
|
-
delegate :amount,
|
7
|
-
:amount_captured,
|
8
|
-
:invoice_id,
|
9
|
-
:line_items,
|
10
|
-
:payment_intent_id,
|
11
|
-
:processor_id,
|
12
|
-
:stripe_account,
|
13
|
-
to: :pay_charge
|
3
|
+
class Charge < Pay::Charge
|
4
|
+
store_accessor :data, :stripe_receipt_url
|
14
5
|
|
15
6
|
def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
|
16
7
|
# Skip loading the latest charge details from the API if we already have it
|
@@ -108,11 +99,7 @@ module Pay
|
|
108
99
|
end
|
109
100
|
end
|
110
101
|
|
111
|
-
def
|
112
|
-
@pay_charge = pay_charge
|
113
|
-
end
|
114
|
-
|
115
|
-
def charge
|
102
|
+
def api_record
|
116
103
|
::Stripe::Charge.retrieve({id: processor_id, expand: ["customer", "invoice.subscription"]}, stripe_options)
|
117
104
|
rescue ::Stripe::StripeError => e
|
118
105
|
raise Pay::Stripe::Error, e
|
@@ -128,6 +115,8 @@ module Pay
|
|
128
115
|
# refund!(5_00)
|
129
116
|
# refund!(5_00, refund_application_fee: true)
|
130
117
|
def refund!(amount_to_refund, **options)
|
118
|
+
amount_to_refund ||= amount
|
119
|
+
|
131
120
|
if invoice_id.present?
|
132
121
|
description = options.delete(:description) || I18n.t("pay.refund")
|
133
122
|
lines = [{type: :custom_line_item, description: description, quantity: 1, unit_amount: amount_to_refund}]
|
@@ -135,7 +124,7 @@ module Pay
|
|
135
124
|
else
|
136
125
|
::Stripe::Refund.create(options.merge(charge: processor_id, amount: amount_to_refund), stripe_options)
|
137
126
|
end
|
138
|
-
|
127
|
+
update!(amount_refunded: amount_refunded + amount_to_refund)
|
139
128
|
rescue ::Stripe::StripeError => e
|
140
129
|
raise Pay::Stripe::Error, e
|
141
130
|
end
|
@@ -148,11 +137,6 @@ module Pay
|
|
148
137
|
raise Pay::Stripe::Error, e
|
149
138
|
end
|
150
139
|
|
151
|
-
def credit_notes(**options)
|
152
|
-
raise Pay::Stripe::Error, "no Stripe invoice_id on Pay::Charge" if invoice_id.blank?
|
153
|
-
::Stripe::CreditNote.list({invoice: invoice_id}.merge(options), stripe_options)
|
154
|
-
end
|
155
|
-
|
156
140
|
# https://stripe.com/docs/payments/capture-later
|
157
141
|
#
|
158
142
|
# capture
|