pay_me 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +111 -0
- data/Rakefile +33 -0
- data/app/controllers/pay_me/api/v1/invoices_controller.rb +30 -0
- data/app/controllers/pay_me/api/v1/stripe_plans_controller.rb +35 -0
- data/app/controllers/pay_me/application_controller.rb +10 -0
- data/app/helpers/pay_me/application_helper.rb +4 -0
- data/app/subscribers/pay_me/customer_subscriber.rb +42 -0
- data/app/subscribers/pay_me/plan_subscriber.rb +38 -0
- data/app/subscribers/pay_me/subscription_subscriber.rb +50 -0
- data/app/subscribers/pay_me/webhook_event_subscriber.rb +11 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20160725204454_create_pay_me_subscriptions.rb +16 -0
- data/db/migrate/20161011170659_create_pay_me_plans.rb +8 -0
- data/db/migrate/20171030174731_add_quantity_to_pay_me_subscription.rb +6 -0
- data/db/migrate/20171130192525_create_pay_me_customers.rb +11 -0
- data/db/migrate/20180116193943_remove_customerable_from_subscriptions.rb +11 -0
- data/db/migrate/20180215235017_add_past_due_to_subscriptions.rb +5 -0
- data/lib/generators/pay_me/controllers/controllers_generator.rb +11 -0
- data/lib/generators/pay_me/controllers/templates/subscriptions_controller.rb +12 -0
- data/lib/generators/pay_me/install_generator.rb +39 -0
- data/lib/generators/pay_me/migrate_to_customer_model/USAGE +8 -0
- data/lib/generators/pay_me/migrate_to_customer_model/migrate_to_customer_model_generator.rb +9 -0
- data/lib/generators/pay_me/migrate_to_customer_model/templates/migration.rb.erb +64 -0
- data/lib/generators/pay_me/models/models_generator.rb +13 -0
- data/lib/generators/pay_me/models/templates/customer.rb +14 -0
- data/lib/generators/pay_me/models/templates/plan.rb +14 -0
- data/lib/generators/pay_me/models/templates/subscription.rb +14 -0
- data/lib/generators/pay_me/policies/policies_generator.rb +11 -0
- data/lib/generators/pay_me/policies/templates/subscription_policy.rb +34 -0
- data/lib/generators/pay_me/templates/pay_me_initializer.rb +17 -0
- data/lib/pay_me.rb +28 -0
- data/lib/pay_me/active_record.rb +6 -0
- data/lib/pay_me/concerns/controllers/customerable.rb +116 -0
- data/lib/pay_me/concerns/models/customerable.rb +54 -0
- data/lib/pay_me/concerns/models/stripe_customerable.rb +24 -0
- data/lib/pay_me/concerns/models/stripe_plannable.rb +31 -0
- data/lib/pay_me/concerns/models/stripe_subscribable.rb +43 -0
- data/lib/pay_me/configuration.rb +27 -0
- data/lib/pay_me/engine.rb +8 -0
- data/lib/pay_me/models.rb +4 -0
- data/lib/pay_me/railtie.rb +2 -0
- data/lib/pay_me/route_helpers.rb +21 -0
- data/lib/pay_me/services/customer.rb +141 -0
- data/lib/pay_me/version.rb +3 -0
- data/lib/pay_me/view_models/charge.rb +48 -0
- data/lib/tasks/pay_me_tasks.rake +4 -0
- data/lib/tasks/stripe.rake +22 -0
- data/test/controllers/pay_me/api/v1/invoices_controller_test.rb +82 -0
- data/test/controllers/pay_me/api/v1/plans_controller_test.rb +62 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/api/v1/users_controller.rb +10 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/pay_me/api/v1/subscriptions_controller.rb +12 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/pay_me/customer.rb +5 -0
- data/test/dummy/app/models/pay_me/plan.rb +5 -0
- data/test/dummy/app/models/pay_me/subscription.rb +5 -0
- data/test/dummy/app/models/user.rb +8 -0
- data/test/dummy/app/policies/pay_me/subscription_policy.rb +34 -0
- data/test/dummy/app/serializers/pay_me/subscription_serializer.rb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +25 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/pay_me.rb +17 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +10 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/migrate/20160630181300_create_pay_me_users.rb +12 -0
- data/test/dummy/db/migrate/20180118001940_api_me_migrate_to_customer_model.rb +9 -0
- data/test/dummy/db/schema.rb +53 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/customer_test.rb +395 -0
- data/test/lib/generators/pay_me/pay_me/migrate_to_customer_model_generator_test.rb +16 -0
- data/test/models/pay_me/customer_test.rb +9 -0
- data/test/models/pay_me/subscription_test.rb +9 -0
- data/test/models/pay_me/user_test.rb +6 -0
- data/test/pay_me_test.rb +7 -0
- data/test/support/concerns/api_test_helper.rb +15 -0
- data/test/support/concerns/stripe_helpers.rb +25 -0
- data/test/test_helper.rb +52 -0
- data/test/unit/concerns/customerable.rb +21 -0
- data/test/unit/models/customerable_test.rb +18 -0
- data/test/unit/services/customer_test.rb +75 -0
- data/test/unit/stubs/customerable.rb +28 -0
- data/test/unit/stubs/customerable_test.rb +16 -0
- data/test/webhooks/customer_webhook_test.rb +74 -0
- data/test/webhooks/plan_webhook_test.rb +74 -0
- metadata +288 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'stripe'
|
2
|
+
require 'pay_me/services/customer'
|
3
|
+
|
4
|
+
module PayMe
|
5
|
+
module Concerns
|
6
|
+
module Models
|
7
|
+
module Customerable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
belongs_to :customer, class_name: 'PayMe::Customer'
|
12
|
+
has_many :subscriptions, class_name: 'PayMe::Subscription', through: :customer
|
13
|
+
has_many :plans, class_name: 'PayMe::Plan', through: :subscriptions
|
14
|
+
|
15
|
+
def customer_service
|
16
|
+
PayMe::Services::Customer.new(customerable: self, customer_class: PayMe.configuration.stripe_customer_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
def stripe_customer
|
20
|
+
customer_service.customer
|
21
|
+
end
|
22
|
+
|
23
|
+
def new_customer_params
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_default_payment_source(source_id)
|
28
|
+
customer_service.add_default_payment_source(source_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_payment_source(source_id)
|
32
|
+
customer_service.add_payment_source(source_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
def subscribe(plan_id:, quantity: 1, coupon_id: nil, source: nil)
|
36
|
+
customer_service.subscribe!(plan_id: plan_id, quantity: quantity, coupon_id: coupon_id, source: source)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_subscription(quantity:, coupon_id: nil)
|
40
|
+
customer_service.update_subscription!(quantity: quantity, coupon_id: coupon_id)
|
41
|
+
end
|
42
|
+
|
43
|
+
def cancel_subscription(cancel_immediately: false)
|
44
|
+
customer_service.cancel_subscription!(cancel_immediately)
|
45
|
+
end
|
46
|
+
|
47
|
+
def stripe_customer_id
|
48
|
+
customer_service.customer_id
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module PayMe
|
2
|
+
module Concerns
|
3
|
+
module Models
|
4
|
+
module StripeCustomerable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
has_many :subscriptions, class_name: 'PayMe::Subscription'
|
9
|
+
has_many :plans, through: :subscriptions, class_name: 'PayMe::Plan'
|
10
|
+
end
|
11
|
+
class_methods do
|
12
|
+
def created_webhook!(stripe_customer)
|
13
|
+
end
|
14
|
+
|
15
|
+
def updated_webhook!(stripe_customer)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deleted_webhook!(stripe_customer)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module PayMe
|
2
|
+
module Concerns
|
3
|
+
module Models
|
4
|
+
module StripePlannable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
has_many :subscriptions, class_name: 'PayMe::Subscription', foreign_key: :pay_me_plan_id
|
9
|
+
|
10
|
+
validates :plan_id, presence: true
|
11
|
+
|
12
|
+
def as_json(options = {})
|
13
|
+
{ subscription: super({ }.merge(options)) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class_methods do
|
17
|
+
def created_webhook!(stripe_plan)
|
18
|
+
end
|
19
|
+
|
20
|
+
def updated_webhook!(stripe_plan)
|
21
|
+
end
|
22
|
+
|
23
|
+
def deleted_webhook!(stripe_plan)
|
24
|
+
# Suggested implementation:
|
25
|
+
# Handle customers who's plan was removed?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module PayMe
|
2
|
+
module Concerns
|
3
|
+
module Models
|
4
|
+
module StripeSubscribable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
belongs_to :customer, class_name: 'PayMe::Customer'
|
9
|
+
belongs_to :plan, foreign_key: :pay_me_plan_id, class_name: 'PayMe::Plan'
|
10
|
+
|
11
|
+
validates :customer, presence: true
|
12
|
+
validates :is_active, inclusion: { in: [ true, false ] }
|
13
|
+
validates :subscription_id, presence: true
|
14
|
+
validates :plan, presence: true
|
15
|
+
|
16
|
+
scope :most_current, -> { order(updated_at: :desc).limit(1) }
|
17
|
+
|
18
|
+
def stripe_plan_id=(stripe_plan_id)
|
19
|
+
self.plan = Plan.find_by(plan_id: stripe_plan_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def stripe_plan_id
|
23
|
+
self.plan.blank? ? nil : self.plan.plan_id
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_json(options = {})
|
27
|
+
{ subscription: super({ }.merge(options)) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
class_methods do
|
31
|
+
def created_webhook!(stripe_subscription)
|
32
|
+
end
|
33
|
+
|
34
|
+
def updated_webhook!(stripe_subscription)
|
35
|
+
end
|
36
|
+
|
37
|
+
def deleted_webhook!(stripe_subscription)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module PayMe
|
4
|
+
class Configuration
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
attr_accessor :mount_path
|
8
|
+
attr_accessor :live_mode
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
self.mount_path = 'pay_me'
|
12
|
+
self.live_mode = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def stripe_customer_class
|
16
|
+
Stripe::Customer
|
17
|
+
end
|
18
|
+
|
19
|
+
def stripe_invoice_class
|
20
|
+
Stripe::Invoice
|
21
|
+
end
|
22
|
+
|
23
|
+
def stripe_plan_class
|
24
|
+
Stripe::Plan
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActionDispatch
|
2
|
+
module Routing
|
3
|
+
class Mapper
|
4
|
+
def pay_me_customerable_for(*resources)
|
5
|
+
resources.each do |r|
|
6
|
+
resources r, only: [] do
|
7
|
+
member do
|
8
|
+
post 'subscribe'
|
9
|
+
put 'update_subscription'
|
10
|
+
delete 'cancel_subscription'
|
11
|
+
post 'charge'
|
12
|
+
get 'invoices'
|
13
|
+
put 'update_default_source'
|
14
|
+
get 'default_source'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module PayMe
|
2
|
+
module Services
|
3
|
+
class Customer
|
4
|
+
attr_reader :customerable, :customer_class
|
5
|
+
|
6
|
+
def initialize(customerable:, customer_class: PayMe.configuration.stripe_customer_class, subcription_class: Stripe::Subscription)
|
7
|
+
@customerable = customerable
|
8
|
+
@customer_class = customer_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def destroy!
|
12
|
+
customer.delete
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_default_payment_source(source_id)
|
16
|
+
customer.default_source = customer.sources.create(source: source_id).id
|
17
|
+
customer.save
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_payment_source(source_id)
|
21
|
+
customer.sources.create(source: source_id)
|
22
|
+
end
|
23
|
+
|
24
|
+
def customer_id
|
25
|
+
customer.id
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
### Notes
|
30
|
+
# Assumes only one active subcription at a time.
|
31
|
+
# Updates current subscription.
|
32
|
+
# Source must be a token
|
33
|
+
###
|
34
|
+
def subscribe!(plan_id:, quantity: 1, coupon_id: nil, source: nil)
|
35
|
+
new_subscription = nil
|
36
|
+
ActiveRecord::Base.transaction do
|
37
|
+
stripe_hash = {
|
38
|
+
customer: customer_id,
|
39
|
+
plan: plan_id,
|
40
|
+
quantity: quantity,
|
41
|
+
coupon: coupon_id,
|
42
|
+
source: source
|
43
|
+
}
|
44
|
+
stripe_subscription = Stripe::Subscription.create(stripe_hash)
|
45
|
+
subscription_status = stripe_subscription.status
|
46
|
+
is_not_active = subscription_status == 'canceled' || subscription_status == 'unpaid'
|
47
|
+
|
48
|
+
new_subscription = PayMe::Subscription.create!(
|
49
|
+
subscription_id: stripe_subscription.id,
|
50
|
+
customer: customerable.customer,
|
51
|
+
is_active: !is_not_active,
|
52
|
+
stripe_plan_id: plan_id,
|
53
|
+
quantity: quantity,
|
54
|
+
coupon_id: coupon_id
|
55
|
+
)
|
56
|
+
end
|
57
|
+
return new_subscription
|
58
|
+
end
|
59
|
+
|
60
|
+
### Notes
|
61
|
+
# Assumes only one active subcription at a time.
|
62
|
+
# Updates current subscription quantity and coupon
|
63
|
+
###
|
64
|
+
def update_subscription!(quantity:, coupon_id: nil)
|
65
|
+
subscription = nil
|
66
|
+
ActiveRecord::Base.transaction do
|
67
|
+
subscription = current_subscription
|
68
|
+
unless subscription.blank?
|
69
|
+
stripe_subscription = Stripe::Subscription.retrieve(subscription.subscription_id)
|
70
|
+
stripe_subscription.quantity = quantity
|
71
|
+
stripe_subscription.coupon = coupon_id
|
72
|
+
subscription.update!(
|
73
|
+
quantity: quantity,
|
74
|
+
coupon_id: coupon_id
|
75
|
+
)
|
76
|
+
stripe_subscription.save
|
77
|
+
end
|
78
|
+
end
|
79
|
+
return subscription
|
80
|
+
end
|
81
|
+
|
82
|
+
### Notes
|
83
|
+
# Assumes only one active subcription at a time.
|
84
|
+
# Cancels current subscription.
|
85
|
+
###
|
86
|
+
def cancel_subscription!(cancel_immediately)
|
87
|
+
subscription = nil
|
88
|
+
ActiveRecord::Base.transaction do
|
89
|
+
subscription = current_subscription
|
90
|
+
unless subscription.blank?
|
91
|
+
stripe_subscription = Stripe::Subscription.retrieve(subscription.subscription_id)
|
92
|
+
subscription.update!(is_active: !cancel_immediately)
|
93
|
+
stripe_subscription.delete(at_period_end: !cancel_immediately)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
return subscription
|
97
|
+
end
|
98
|
+
|
99
|
+
def customer
|
100
|
+
if @customerable.customer && @customerable.customer.customer_id
|
101
|
+
@customer ||= customer_class.retrieve(customerable.customer.customer_id)
|
102
|
+
else
|
103
|
+
@customer = create!
|
104
|
+
end
|
105
|
+
|
106
|
+
@customer
|
107
|
+
end
|
108
|
+
|
109
|
+
def current_subscription
|
110
|
+
@customerable.subscriptions.most_current.limit(1).first
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
def create!(_params = {})
|
116
|
+
ActiveRecord::Base.transaction do
|
117
|
+
@customer = customer_class.create(
|
118
|
+
customerable.new_customer_params.merge(
|
119
|
+
metadata: { customerable_id: customerable.id }
|
120
|
+
)
|
121
|
+
)
|
122
|
+
|
123
|
+
begin
|
124
|
+
if @customerable.customer
|
125
|
+
@customerable.customer.update!(customer_id: @customer.id)
|
126
|
+
else
|
127
|
+
@customerable.update!(
|
128
|
+
customer: PayMe::Customer.create!(customer_id: @customer.id)
|
129
|
+
)
|
130
|
+
end
|
131
|
+
rescue
|
132
|
+
@customer.delete
|
133
|
+
raise
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
@customer
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module PayMe
|
4
|
+
module ViewModels
|
5
|
+
class Charge
|
6
|
+
include Virtus.model
|
7
|
+
include ActiveModel::Validations
|
8
|
+
|
9
|
+
attribute :amount, Integer
|
10
|
+
attribute :currency, String
|
11
|
+
attribute :customerable
|
12
|
+
attribute :description, String
|
13
|
+
attribute :source, String
|
14
|
+
|
15
|
+
validates :amount,
|
16
|
+
presence: true,
|
17
|
+
numericality: { greater_than_or_equal_to: 0, only_integer: true }
|
18
|
+
validates :currency, presence: true, format: { with: /\A[a-z]{3}\z/i }
|
19
|
+
validates :customerable, presence: true
|
20
|
+
validate :customer_is_valid, unless: proc { |c| c.customerable.blank? }
|
21
|
+
validates :description, presence: true
|
22
|
+
validates :source, presence: true, unless: proc { |c| c.source.blank? }
|
23
|
+
|
24
|
+
def charge
|
25
|
+
valid!
|
26
|
+
stripe_payload_hash = {
|
27
|
+
amount: amount,
|
28
|
+
currency: currency,
|
29
|
+
customer: customerable.stripe_customer_id,
|
30
|
+
receipt_email: customerable.receipt_email,
|
31
|
+
description: description
|
32
|
+
}
|
33
|
+
stripe_payload_hash[:source] = source unless source.blank?
|
34
|
+
Stripe::Charge.create(stripe_payload_hash)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def customer_is_valid
|
40
|
+
errors.add(:base, 'customer invalid') unless customerable.stripe_customer_id.present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid!
|
44
|
+
raise ActiveRecord::RecordInvalid, self unless valid?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
namespace :pay_me do
|
3
|
+
namespace :stripe do
|
4
|
+
desc 'Additively caches stripe plans locally.'
|
5
|
+
task add_stripe_plans: :environment do |_t, args|
|
6
|
+
Stripe::Plan.list.each do |plan|
|
7
|
+
PayMe::Plan.create!(plan_id: plan.id) unless PayMe::Plan.exists?(plan_id: plan.id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Destructively caches stripe plans locally to match stripe.'
|
12
|
+
task sync_stripe_plans: :environment do |_t, args|
|
13
|
+
@stripe_plans_ids = Stripe::Plan.list.map(&:id)
|
14
|
+
PayMe::Plan.find_each do |pay_me_plan|
|
15
|
+
pay_me_plan.destroy unless @stripe_plans_ids.delete(pay_me_plan.plan_id) != nil
|
16
|
+
end
|
17
|
+
Stripe::Plan.list.each do |plan|
|
18
|
+
PayMe::Plan.create!(plan_id: plan.id) unless PayMe::Plan.exists?(plan_id: plan.id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|