pay 2.1.3 → 2.3.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.
- checksums.yaml +4 -4
- data/README.md +161 -14
- data/Rakefile +2 -4
- data/app/controllers/pay/payments_controller.rb +3 -0
- data/app/controllers/pay/webhooks/paddle_controller.rb +36 -0
- data/app/mailers/pay/user_mailer.rb +1 -1
- data/app/models/pay/application_record.rb +6 -1
- data/app/models/pay/charge.rb +7 -0
- data/app/models/pay/subscription.rb +24 -3
- data/app/views/pay/payments/show.html.erb +1 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20170205020145_create_pay_subscriptions.rb +2 -1
- data/db/migrate/20170727235816_create_pay_charges.rb +1 -0
- data/db/migrate/20200603134434_add_data_to_pay_models.rb +17 -0
- data/lib/generators/active_record/pay_generator.rb +1 -1
- data/lib/generators/pay/orm_helpers.rb +1 -2
- data/lib/pay.rb +5 -2
- data/lib/pay/billable.rb +7 -2
- data/lib/pay/braintree.rb +1 -1
- data/lib/pay/braintree/billable.rb +14 -8
- data/lib/pay/braintree/charge.rb +4 -0
- data/lib/pay/braintree/subscription.rb +6 -0
- data/lib/pay/engine.rb +7 -0
- data/lib/pay/paddle.rb +38 -0
- data/lib/pay/paddle/billable.rb +66 -0
- data/lib/pay/paddle/charge.rb +39 -0
- data/lib/pay/paddle/subscription.rb +59 -0
- data/lib/pay/paddle/webhooks.rb +1 -0
- data/lib/pay/paddle/webhooks/signature_verifier.rb +115 -0
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +18 -0
- data/lib/pay/paddle/webhooks/subscription_created.rb +59 -0
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +21 -0
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +64 -0
- data/lib/pay/paddle/webhooks/subscription_updated.rb +34 -0
- data/lib/pay/stripe.rb +1 -0
- data/lib/pay/stripe/billable.rb +11 -3
- data/lib/pay/stripe/charge.rb +4 -0
- data/lib/pay/stripe/subscription.rb +7 -1
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +7 -7
- data/lib/pay/stripe/webhooks/payment_action_required.rb +7 -8
- data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
- data/lib/pay/version.rb +1 -1
- metadata +59 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 938efc72579bf8d24477a0e55060280a8e2d2df139860cdfe84dcc715f0eeb23
|
4
|
+
data.tar.gz: 3f00c21f027beae9a68d9da5228d9fa0165eeb2153d62e48544702565e0834d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d2ef6252a25049649fe460553766aa61122c2d3541b7eb6b4197ca4614c9110bcf9243fd5aa08ab90d32b2a224317dcf2e9275a8cd918bee4c0b8146703fd37
|
7
|
+
data.tar.gz: '0992d392de15e633d5dcd190f9ed25bd356cf1a8da548e14b2560314aafa59593e5fcb64a68d70d16d5b58a6b6df0df55eba7259bb88c92166439bc61974af0e'
|
data/README.md
CHANGED
@@ -2,14 +2,15 @@
|
|
2
2
|
|
3
3
|
## Pay - Payments engine for Ruby on Rails
|
4
4
|
|
5
|
-
[![Build Status](https://github.com/pay-rails/pay/workflows/Tests/badge.svg)](https://github.com/pay-rails/pay/actions)
|
5
|
+
[![Build Status](https://github.com/pay-rails/pay/workflows/Tests/badge.svg)](https://github.com/pay-rails/pay/actions) [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay)
|
6
6
|
|
7
7
|
Pay is a payments engine for Ruby on Rails 4.2 and higher.
|
8
8
|
|
9
9
|
**Current Payment Providers**
|
10
10
|
|
11
|
-
- Stripe ([supports SCA](https://stripe.com/docs/strong-customer-authentication)
|
11
|
+
- Stripe ([supports SCA](https://stripe.com/docs/strong-customer-authentication) using API version `2020-08-27`)
|
12
12
|
- Braintree
|
13
|
+
- Paddle
|
13
14
|
|
14
15
|
Want to add a new payment provider? Contributions are welcome and the instructions [are here](https://github.com/jasoncharnes/pay/wiki/New-Payment-Provider).
|
15
16
|
|
@@ -35,6 +36,9 @@ gem 'stripe_event', '~> 2.3'
|
|
35
36
|
# To use Braintree + PayPal, also include:
|
36
37
|
gem 'braintree', '< 3.0', '>= 2.92.0'
|
37
38
|
|
39
|
+
# To use Paddle, also include:
|
40
|
+
gem 'paddle_pay', '~> 0.0.1'
|
41
|
+
|
38
42
|
# To use Receipts
|
39
43
|
gem 'receipts', '~> 1.0.0'
|
40
44
|
```
|
@@ -78,6 +82,8 @@ class User < ActiveRecord::Base
|
|
78
82
|
end
|
79
83
|
```
|
80
84
|
|
85
|
+
An `email` attribute or method on your `Billable` model is required.
|
86
|
+
|
81
87
|
To sync over customer names, your `Billable` model should respond to the `first_name` and `last_name` methods. Pay will sync these over to your Customer objects in Stripe and Braintree.
|
82
88
|
|
83
89
|
## Configuration
|
@@ -131,10 +137,15 @@ development:
|
|
131
137
|
public_key: yyyy
|
132
138
|
merchant_id: aaaa
|
133
139
|
environment: sandbox
|
140
|
+
paddle:
|
141
|
+
vendor_id: xxxx
|
142
|
+
vendor_auth_code: yyyy
|
143
|
+
public_key_base64: MII...==
|
134
144
|
```
|
135
145
|
|
136
146
|
For Stripe, you can also use the `STRIPE_PUBLIC_KEY`, `STRIPE_PRIVATE_KEY` and `STRIPE_SIGNING_SECRET` environment variables.
|
137
147
|
For Braintree, you can also use `BRAINTREE_MERCHANT_ID`, `BRAINTREE_PUBLIC_KEY`, `BRAINTREE_PRIVATE_KEY`, and `BRAINTREE_ENVIRONMENT` environment variables.
|
148
|
+
For Paddle, you can also use `PADDLE_VENDOR_ID`, `PADDLE_VENDOR_AUTH_CODE` and `PADDLE_PUBLIC_KEY_BASE64` environment variables.
|
138
149
|
|
139
150
|
### Generators
|
140
151
|
|
@@ -194,6 +205,8 @@ user.on_generic_trial? #=> true
|
|
194
205
|
|
195
206
|
#### Creating a Charge
|
196
207
|
|
208
|
+
##### Stripe and Braintree
|
209
|
+
|
197
210
|
```ruby
|
198
211
|
user = User.find_by(email: 'michael@bluthcompany.co')
|
199
212
|
|
@@ -217,8 +230,23 @@ different currencies, etc.
|
|
217
230
|
On failure, a `Pay::Error` will be raised with details about the payment
|
218
231
|
failure.
|
219
232
|
|
233
|
+
##### Paddle
|
234
|
+
It is only possible to create immediate one-time charges on top of an existing subscription.
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
user = User.find_by(email: 'michael@bluthcompany.co')
|
238
|
+
|
239
|
+
user.processor = 'paddle'
|
240
|
+
user.charge(1500, {charge_name: "Test"}) # $15.00 USD
|
241
|
+
|
242
|
+
```
|
243
|
+
|
244
|
+
An existing subscription and a charge name are required.
|
245
|
+
|
220
246
|
#### Creating a Subscription
|
221
247
|
|
248
|
+
##### Stripe and Braintree
|
249
|
+
|
222
250
|
```ruby
|
223
251
|
user = User.find_by(email: 'michael@bluthcompany.co')
|
224
252
|
|
@@ -244,20 +272,53 @@ For example, you can pass the `quantity` option to subscribe to a plan with for
|
|
244
272
|
user.subscribe(name: "default", plan: "default", quantity: 3)
|
245
273
|
```
|
246
274
|
|
247
|
-
|
275
|
+
###### Name
|
248
276
|
|
249
277
|
Name is an internally used name for the subscription.
|
250
278
|
|
251
|
-
|
279
|
+
###### Plan
|
252
280
|
|
253
|
-
Plan is the plan ID from the payment processor.
|
281
|
+
Plan is the plan ID or price ID from the payment processor. For example: `plan_xxxxx` or `price_xxxxx`
|
254
282
|
|
255
|
-
|
283
|
+
###### Options
|
256
284
|
|
257
285
|
By default, the trial specified on the subscription will be used.
|
258
286
|
|
259
287
|
`trial_period_days: 30` can be set to override and a trial to the subscription. This works the same for Braintree and Stripe.
|
260
288
|
|
289
|
+
##### Paddle
|
290
|
+
It is currently not possible to create a subscription through the API. Instead the subscription in Pay is created by the Paddle Subscription Webhook. In order to be able to assign the subcription to the correct owner, the Paddle [passthrough parameter](https://developer.paddle.com/guides/how-tos/checkout/pass-parameters) has to be used for checkout.
|
291
|
+
|
292
|
+
To ensure that the owner cannot be tampered with, Pay uses a Signed Global ID with a purpose. The purpose string consists of "paddle_" and the subscription plan id (or product id respectively).
|
293
|
+
|
294
|
+
Javascript Checkout:
|
295
|
+
```javascript
|
296
|
+
Paddle.Checkout.open({
|
297
|
+
product: 12345,
|
298
|
+
passthrough: "<%= Pay::Paddle.passthrough(owner: current_user) %>"
|
299
|
+
});
|
300
|
+
```
|
301
|
+
|
302
|
+
Paddle Button Checkout:
|
303
|
+
```html
|
304
|
+
<a href="#!" class="paddle_button" data-product="12345" data-email="<%= current_user.email %>" data-passthrough="<%= Pay::Paddle.passthrough(owner: current_user) %>"
|
305
|
+
```
|
306
|
+
|
307
|
+
###### Passthrough
|
308
|
+
|
309
|
+
Pay providers a helper method for generating the passthrough JSON object to associate the purchase with the correct Rails model.
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
Pay::Paddle.passthrough(owner: current_user, foo: :bar)
|
313
|
+
#=> { owner_sgid: "xxxxxxxx", foo: "bar" }
|
314
|
+
|
315
|
+
# To generate manually without the helper
|
316
|
+
#=> { owner_sgid: current_user.to_sgid.to_s, foo: "bar" }.to_json
|
317
|
+
```
|
318
|
+
|
319
|
+
Pay parses the passthrough JSON string and verifies the `owner_sgid` hash to match the webhook with the correct billable record.
|
320
|
+
The passthrough parameter `owner_sgid` is only required for creating a subscription.
|
321
|
+
|
261
322
|
#### Retrieving a Subscription from the Database
|
262
323
|
|
263
324
|
```ruby
|
@@ -314,26 +375,45 @@ Plan is the plan ID from the payment processor.
|
|
314
375
|
|
315
376
|
#### Retrieving a Payment Processor Account
|
316
377
|
|
378
|
+
##### Stripe and Braintree
|
379
|
+
|
317
380
|
```ruby
|
318
381
|
user = User.find_by(email: 'george.michael@bluthcompany.co')
|
319
382
|
|
320
383
|
user.customer #> Stripe or Braintree customer account
|
321
384
|
```
|
322
385
|
|
386
|
+
##### Paddle
|
387
|
+
|
388
|
+
It is currently not possible to retrieve a payment processor account through the API.
|
389
|
+
|
323
390
|
#### Updating a Customer's Credit Card
|
324
391
|
|
392
|
+
##### Stripe and Braintree
|
393
|
+
|
325
394
|
```ruby
|
326
395
|
user = User.find_by(email: 'tobias@bluthcompany.co')
|
327
396
|
|
328
397
|
user.update_card('payment_method_id')
|
329
398
|
```
|
330
399
|
|
400
|
+
##### Paddle
|
401
|
+
|
402
|
+
Paddle provides a unique [Update URL](https://developer.paddle.com/guides/how-tos/subscriptions/update-payment-details) for each user, which allows them to update the payment method.
|
403
|
+
```ruby
|
404
|
+
user = User.find_by(email: 'tobias@bluthcompany.co')
|
405
|
+
|
406
|
+
user.subscription.paddle_update_url
|
407
|
+
```
|
408
|
+
|
409
|
+
|
410
|
+
|
331
411
|
#### Retrieving a Customer's Subscription from the Processor
|
332
412
|
|
333
413
|
```ruby
|
334
414
|
user = User.find_by(email: 'lucille@bluthcompany.co')
|
335
415
|
|
336
|
-
user.processor_subscription(subscription_id) #=> Stripe or
|
416
|
+
user.processor_subscription(subscription_id) #=> Stripe, Braintree or Paddle Subscription
|
337
417
|
```
|
338
418
|
|
339
419
|
## Subscription API
|
@@ -370,14 +450,31 @@ user = User.find_by(email: 'carl.weathers@bluthcompany.co')
|
|
370
450
|
user.subscription.active? #=> true or false
|
371
451
|
```
|
372
452
|
|
453
|
+
#### Checking to See If a Subscription Is Paused
|
454
|
+
|
455
|
+
```ruby
|
456
|
+
user = User.find_by(email: 'carl.weathers@bluthcompany.co')
|
457
|
+
|
458
|
+
user.subscription.paused? #=> true or false
|
459
|
+
```
|
460
|
+
|
373
461
|
#### Cancel a Subscription (At End of Billing Cycle)
|
374
462
|
|
463
|
+
##### Stripe, Braintree and Paddle
|
464
|
+
|
375
465
|
```ruby
|
376
466
|
user = User.find_by(email: 'oscar@bluthcompany.co')
|
377
467
|
|
378
468
|
user.subscription.cancel
|
379
469
|
```
|
380
470
|
|
471
|
+
##### Paddle
|
472
|
+
In addition to the API, Paddle provides a subscription [Cancel URL](https://developer.paddle.com/guides/how-tos/subscriptions/cancel-and-pause) that you can redirect customers to cancel their subscription.
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
user.subscription.paddle_cancel_url
|
476
|
+
```
|
477
|
+
|
381
478
|
#### Cancel a Subscription Immediately
|
382
479
|
|
383
480
|
```ruby
|
@@ -386,6 +483,16 @@ user = User.find_by(email: 'annyong@bluthcompany.co')
|
|
386
483
|
user.subscription.cancel_now!
|
387
484
|
```
|
388
485
|
|
486
|
+
#### Pause a Subscription
|
487
|
+
|
488
|
+
##### Paddle
|
489
|
+
|
490
|
+
```ruby
|
491
|
+
user = User.find_by(email: 'oscar@bluthcompany.co')
|
492
|
+
|
493
|
+
user.subscription.pause
|
494
|
+
```
|
495
|
+
|
389
496
|
#### Swap a Subscription to another Plan
|
390
497
|
|
391
498
|
```ruby
|
@@ -394,7 +501,17 @@ user = User.find_by(email: 'steve.holt@bluthcompany.co')
|
|
394
501
|
user.subscription.swap("yearly")
|
395
502
|
```
|
396
503
|
|
397
|
-
#### Resume a Subscription
|
504
|
+
#### Resume a Subscription
|
505
|
+
|
506
|
+
##### Stripe or Braintree Subscription (on Grace Period)
|
507
|
+
|
508
|
+
```ruby
|
509
|
+
user = User.find_by(email: 'steve.holt@bluthcompany.co')
|
510
|
+
|
511
|
+
user.subscription.resume
|
512
|
+
```
|
513
|
+
|
514
|
+
##### Paddle (Paused)
|
398
515
|
|
399
516
|
```ruby
|
400
517
|
user = User.find_by(email: 'steve.holt@bluthcompany.co')
|
@@ -471,8 +588,8 @@ config.routes_path = '/secret-webhook-path'
|
|
471
588
|
|
472
589
|
## Payment Providers
|
473
590
|
|
474
|
-
We support
|
475
|
-
standardize the
|
591
|
+
We support Stripe, Braintree and Paddle and make our best attempt to
|
592
|
+
standardize the three. They function differently so keep that in mind if
|
476
593
|
you plan on doing more complex payments. It would be best to stick with
|
477
594
|
a single payment provider in that case so you don't run into
|
478
595
|
discrepancies.
|
@@ -487,7 +604,22 @@ development:
|
|
487
604
|
merchant_id: zzzz
|
488
605
|
environment: sandbox
|
489
606
|
```
|
607
|
+
#### Paddle
|
608
|
+
|
609
|
+
```yaml
|
610
|
+
paddle:
|
611
|
+
vendor_id: xxxx
|
612
|
+
vendor_auth_code: yyyy
|
613
|
+
public_key_base64: MII...==
|
614
|
+
```
|
615
|
+
|
616
|
+
Paddle receipts can be retrieved by a charge receipt URL.
|
617
|
+
```ruby
|
618
|
+
user = User.find_by(email: 'annyong@bluthcompany.co')
|
490
619
|
|
620
|
+
charge = user.charges.first
|
621
|
+
charge.paddle_receipt_url
|
622
|
+
```
|
491
623
|
#### Stripe
|
492
624
|
|
493
625
|
You'll need to add your private Stripe API key to your Rails secrets `config/secrets.yml`, credentials `rails credentials:edit`
|
@@ -504,6 +636,20 @@ You can also use the `STRIPE_PRIVATE_KEY` and `STRIPE_SIGNING_SECRET` environmen
|
|
504
636
|
|
505
637
|
**To see how to use Stripe Elements JS & Devise, [click here](https://github.com/jasoncharnes/pay/wiki/Using-Stripe-Elements-and-Devise).**
|
506
638
|
|
639
|
+
You need the following event types to trigger the webhook:
|
640
|
+
|
641
|
+
```
|
642
|
+
customer.subscription.updated
|
643
|
+
customer.subscription.deleted
|
644
|
+
customer.subscription.created
|
645
|
+
payment_method.updated
|
646
|
+
invoice.payment_action_required
|
647
|
+
customer.updated
|
648
|
+
customer.deleted
|
649
|
+
charge.succeeded
|
650
|
+
charge.refunded
|
651
|
+
```
|
652
|
+
|
507
653
|
##### Strong Customer Authentication (SCA)
|
508
654
|
|
509
655
|
Our Stripe integration **requires** the use of Payment Method objects to correctly support Strong Customer Authentication with Stripe. If you've previously been using card tokens, you'll need to upgrade your Javascript integration.
|
@@ -518,9 +664,7 @@ correctly for SCA payments.
|
|
518
664
|
stripe listen --forward-to localhost:3000/pay/webhooks/stripe
|
519
665
|
```
|
520
666
|
|
521
|
-
You should use `stripe.
|
522
|
-
|
523
|
-
The Javascript will now need to use createPaymentMethod instead of createToken. https://stripe.com/docs/js/payment_intents/create_payment_method
|
667
|
+
You should use `stripe.confirmCardSetup` on the client to collect card information anytime you want to save the card and charge them later (adding a card, then charging them on the next page for example). Use `stripe.confirmCardPayment` if you'd like to charge the customer immediately (think checking out of a shopping cart).
|
524
668
|
|
525
669
|
The Javascript also needs to have a PaymentIntent or SetupIntent created server-side and the ID passed into the Javascript to do this. That way it knows how to safely handle the card tokenization if it meets the SCA requirements.
|
526
670
|
|
@@ -552,7 +696,10 @@ If you have an issue you'd like to submit, please do so using the issue tracker
|
|
552
696
|
|
553
697
|
If you'd like to open a PR please make sure the following things pass:
|
554
698
|
|
555
|
-
|
699
|
+
```ruby
|
700
|
+
bin/rails db:test:prepare
|
701
|
+
bin/rails test
|
702
|
+
```
|
556
703
|
|
557
704
|
## License
|
558
705
|
|
data/Rakefile
CHANGED
@@ -4,6 +4,8 @@ rescue LoadError
|
|
4
4
|
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
5
5
|
end
|
6
6
|
|
7
|
+
require "bundler/gem_tasks"
|
8
|
+
|
7
9
|
require "rdoc/task"
|
8
10
|
|
9
11
|
RDoc::Task.new(:rdoc) do |rdoc|
|
@@ -19,10 +21,6 @@ load "rails/tasks/engine.rake"
|
|
19
21
|
|
20
22
|
load "rails/tasks/statistics.rake"
|
21
23
|
|
22
|
-
unless Rails.env.test?
|
23
|
-
require "bundler/gem_tasks"
|
24
|
-
end
|
25
|
-
|
26
24
|
require "rake/testtask"
|
27
25
|
|
28
26
|
Rake::TestTask.new(:test) do |t|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Pay
|
2
|
+
module Webhooks
|
3
|
+
class PaddleController < Pay::ApplicationController
|
4
|
+
if Rails.application.config.action_controller.default_protect_from_forgery
|
5
|
+
skip_before_action :verify_authenticity_token
|
6
|
+
end
|
7
|
+
|
8
|
+
def create
|
9
|
+
verifier = Pay::Paddle::Webhooks::SignatureVerifier.new(check_params.as_json)
|
10
|
+
if verifier.verify
|
11
|
+
case params["alert_name"]
|
12
|
+
when "subscription_created"
|
13
|
+
Pay::Paddle::Webhooks::SubscriptionCreated.new(check_params.as_json)
|
14
|
+
when "subscription_updated"
|
15
|
+
Pay::Paddle::Webhooks::SubscriptionUpdated.new(check_params.as_json)
|
16
|
+
when "subscription_cancelled"
|
17
|
+
Pay::Paddle::Webhooks::SubscriptionCancelled.new(check_params.as_json)
|
18
|
+
when "subscription_payment_succeeded"
|
19
|
+
Pay::Paddle::Webhooks::SubscriptionPaymentSucceeded.new(check_params.as_json)
|
20
|
+
when "subscription_payment_refunded"
|
21
|
+
Pay::Paddle::Webhooks::SubscriptionPaymentRefunded.new(check_params.as_json)
|
22
|
+
end
|
23
|
+
render json: {success: true}, status: :ok
|
24
|
+
else
|
25
|
+
head :ok
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_params
|
32
|
+
params.except(:action, :controller).permit!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,5 +1,10 @@
|
|
1
1
|
module Pay
|
2
|
-
class ApplicationRecord <
|
2
|
+
class ApplicationRecord < Pay.model_parent_class.constantize
|
3
3
|
self.abstract_class = true
|
4
|
+
|
5
|
+
def self.json_column?(name)
|
6
|
+
return unless connected? && table_exists?
|
7
|
+
[:json, :jsonb].include?(attribute_types[name].type)
|
8
|
+
end
|
4
9
|
end
|
5
10
|
end
|
data/app/models/pay/charge.rb
CHANGED
@@ -2,6 +2,9 @@ module Pay
|
|
2
2
|
class Charge < ApplicationRecord
|
3
3
|
self.table_name = Pay.chargeable_table
|
4
4
|
|
5
|
+
# Only serialize for non-json columns
|
6
|
+
serialize :data unless json_column?("data")
|
7
|
+
|
5
8
|
# Associations
|
6
9
|
belongs_to :owner, polymorphic: true
|
7
10
|
|
@@ -39,5 +42,9 @@ module Pay
|
|
39
42
|
def paypal?
|
40
43
|
braintree? && card_type == "PayPal"
|
41
44
|
end
|
45
|
+
|
46
|
+
def paddle?
|
47
|
+
processor == "paddle"
|
48
|
+
end
|
42
49
|
end
|
43
50
|
end
|