stripe_model_callbacks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +153 -0
  4. data/Rakefile +26 -0
  5. data/app/assets/config/stripe_model_callbacks_manifest.js +2 -0
  6. data/app/assets/javascripts/stripe_model_callbacks/application.js +13 -0
  7. data/app/assets/stylesheets/stripe_model_callbacks/application.css +15 -0
  8. data/app/controllers/stripe_model_callbacks/application_controller.rb +3 -0
  9. data/app/jobs/stripe_model_callbacks/application_job.rb +2 -0
  10. data/app/mailers/stripe_model_callbacks/application_mailer.rb +4 -0
  11. data/app/models/stripe_model_callbacks/application_record.rb +60 -0
  12. data/app/services/stripe_model_callbacks/account/external_account/updated_service.rb +13 -0
  13. data/app/services/stripe_model_callbacks/attributes_assigner_service.rb +35 -0
  14. data/app/services/stripe_model_callbacks/base_event_service.rb +9 -0
  15. data/app/services/stripe_model_callbacks/base_service.rb +26 -0
  16. data/app/services/stripe_model_callbacks/charge/dispute_updated_service.rb +29 -0
  17. data/app/services/stripe_model_callbacks/charge/updated_service.rb +40 -0
  18. data/app/services/stripe_model_callbacks/configure_service.rb +217 -0
  19. data/app/services/stripe_model_callbacks/coupon/updated_service.rb +18 -0
  20. data/app/services/stripe_model_callbacks/customer/bank_account/deleted_service.rb +13 -0
  21. data/app/services/stripe_model_callbacks/customer/deleted_service.rb +13 -0
  22. data/app/services/stripe_model_callbacks/customer/discount_updated_service.rb +32 -0
  23. data/app/services/stripe_model_callbacks/customer/source_updated_service.rb +25 -0
  24. data/app/services/stripe_model_callbacks/customer/subscription/updated_service.rb +28 -0
  25. data/app/services/stripe_model_callbacks/customer/updated_service.rb +13 -0
  26. data/app/services/stripe_model_callbacks/event_mocker_service.rb +38 -0
  27. data/app/services/stripe_model_callbacks/invoice/updated_service.rb +26 -0
  28. data/app/services/stripe_model_callbacks/invoice_item/updated_service.rb +13 -0
  29. data/app/services/stripe_model_callbacks/notifier_service.rb +6 -0
  30. data/app/services/stripe_model_callbacks/order/updated_service.rb +28 -0
  31. data/app/services/stripe_model_callbacks/payout/updated_service.rb +29 -0
  32. data/app/services/stripe_model_callbacks/plan/updated_service.rb +14 -0
  33. data/app/services/stripe_model_callbacks/product/updated_service.rb +14 -0
  34. data/app/services/stripe_model_callbacks/recipient/updated_service.rb +14 -0
  35. data/app/services/stripe_model_callbacks/refund/updated_service.rb +12 -0
  36. data/app/services/stripe_model_callbacks/review/updated_service.rb +13 -0
  37. data/app/services/stripe_model_callbacks/sku/updated_service.rb +14 -0
  38. data/app/services/stripe_model_callbacks/source/updated_service.rb +31 -0
  39. data/app/services/stripe_model_callbacks/subscription/state_checker_service.rb +21 -0
  40. data/app/services/stripe_model_callbacks/transfer/updated_service.rb +18 -0
  41. data/app/views/layouts/stripe_model_callbacks/application.html.erb +14 -0
  42. data/config/rails_best_practices.yml +49 -0
  43. data/config/routes.rb +2 -0
  44. data/db/migrate/20180206115801_create_stripe_charges.rb +54 -0
  45. data/db/migrate/20180206115802_create_stripe_coupons.rb +22 -0
  46. data/db/migrate/20180206115803_create_stripe_customers.rb +21 -0
  47. data/db/migrate/20180206115804_create_stripe_discounts.rb +27 -0
  48. data/db/migrate/20180206115805_create_stripe_invoice_items.rb +26 -0
  49. data/db/migrate/20180206115806_create_stripe_invoices.rb +57 -0
  50. data/db/migrate/20180206115807_create_stripe_order_items.rb +15 -0
  51. data/db/migrate/20180206115808_create_stripe_orders.rb +46 -0
  52. data/db/migrate/20180206115809_create_stripe_payouts.rb +27 -0
  53. data/db/migrate/20180206115810_create_stripe_plans.rb +20 -0
  54. data/db/migrate/20180206115811_create_stripe_products.rb +25 -0
  55. data/db/migrate/20180206115812_create_stripe_recipients.rb +18 -0
  56. data/db/migrate/20180206115813_create_stripe_refunds.rb +21 -0
  57. data/db/migrate/20180206115814_create_stripe_skus.rb +22 -0
  58. data/db/migrate/20180206115815_create_stripe_sources.rb +70 -0
  59. data/db/migrate/20180206115816_create_stripe_subscriptions.rb +29 -0
  60. data/db/migrate/20180206115817_create_stripe_transfers.rb +25 -0
  61. data/db/migrate/20180206130408_create_disputes.rb +76 -0
  62. data/db/migrate/20180206151132_change_activities_trackable_id_to_string.rb +5 -0
  63. data/db/migrate/20180207121808_create_stripe_subscription_items.rb +14 -0
  64. data/db/migrate/20180207171559_remove_name_from_stripe_plans.rb +5 -0
  65. data/db/migrate/20180207172011_add_stripe_product_id_to_stripe_plans.rb +6 -0
  66. data/db/migrate/20180207185604_add_index_on_refund_charge.rb +5 -0
  67. data/db/migrate/20180208091647_change_stripe_subscription_items_quantity_to_integer.rb +5 -0
  68. data/db/migrate/20180208102513_add_attempt_count_to_stripe_invoices.rb +7 -0
  69. data/db/migrate/20180208121235_create_stripe_reviews.rb +13 -0
  70. data/db/migrate/20180208122654_create_stripe_bank_accounts.rb +20 -0
  71. data/db/migrate/20180208160046_add_nickname_to_stripe_plans.rb +5 -0
  72. data/db/migrate/20180211092437_rename_stripe_invoice_items_subscription_item.rb +6 -0
  73. data/db/migrate/20180216224335_rename_stripe_subscriptions_discount.rb +5 -0
  74. data/db/migrate/20180217200350_change_stripe_invoices_to_reference_discounts.rb +9 -0
  75. data/db/migrate/20180217204628_create_stripe_cards.rb +29 -0
  76. data/db/migrate/20180218092600_add_deleted_at_to_stripe_cards.rb +9 -0
  77. data/db/migrate/20181219121712_add_new_primary_id_and_rename_old.rb +37 -0
  78. data/db/migrate/20190629112050_rename_stripe_invoice_date_to_created.rb +5 -0
  79. data/lib/stripe_model_callbacks.rb +19 -0
  80. data/lib/stripe_model_callbacks/autoload_models.rb +8 -0
  81. data/lib/stripe_model_callbacks/configuration.rb +23 -0
  82. data/lib/stripe_model_callbacks/engine.rb +5 -0
  83. data/lib/stripe_model_callbacks/event_mocker.rb +5 -0
  84. data/lib/stripe_model_callbacks/factory_bot_definitions.rb +7 -0
  85. data/lib/stripe_model_callbacks/models/stripe_bank_account.rb +17 -0
  86. data/lib/stripe_model_callbacks/models/stripe_card.rb +25 -0
  87. data/lib/stripe_model_callbacks/models/stripe_charge.rb +47 -0
  88. data/lib/stripe_model_callbacks/models/stripe_coupon.rb +24 -0
  89. data/lib/stripe_model_callbacks/models/stripe_customer.rb +23 -0
  90. data/lib/stripe_model_callbacks/models/stripe_discount.rb +53 -0
  91. data/lib/stripe_model_callbacks/models/stripe_dispute.rb +86 -0
  92. data/lib/stripe_model_callbacks/models/stripe_invoice.rb +88 -0
  93. data/lib/stripe_model_callbacks/models/stripe_invoice_item.rb +33 -0
  94. data/lib/stripe_model_callbacks/models/stripe_order.rb +63 -0
  95. data/lib/stripe_model_callbacks/models/stripe_order_item.rb +20 -0
  96. data/lib/stripe_model_callbacks/models/stripe_payout.rb +26 -0
  97. data/lib/stripe_model_callbacks/models/stripe_plan.rb +32 -0
  98. data/lib/stripe_model_callbacks/models/stripe_product.rb +27 -0
  99. data/lib/stripe_model_callbacks/models/stripe_recipient.rb +16 -0
  100. data/lib/stripe_model_callbacks/models/stripe_refund.rb +28 -0
  101. data/lib/stripe_model_callbacks/models/stripe_review.rb +18 -0
  102. data/lib/stripe_model_callbacks/models/stripe_sku.rb +28 -0
  103. data/lib/stripe_model_callbacks/models/stripe_source.rb +79 -0
  104. data/lib/stripe_model_callbacks/models/stripe_subscription.rb +101 -0
  105. data/lib/stripe_model_callbacks/models/stripe_subscription_item.rb +36 -0
  106. data/lib/stripe_model_callbacks/models/stripe_transfer.rb +25 -0
  107. data/lib/stripe_model_callbacks/require_models.rb +7 -0
  108. data/lib/stripe_model_callbacks/version.rb +3 -0
  109. data/lib/tasks/stripe_model_callbacks_tasks.rake +4 -0
  110. metadata +249 -0
@@ -0,0 +1,40 @@
1
+ class StripeModelCallbacks::Charge::UpdatedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ charge.assign_from_stripe(object)
4
+
5
+ if charge.save
6
+ create_refunds if event.type == "charge.refunded"
7
+ create_activity
8
+ succeed!
9
+ else
10
+ fail! charge.errors.full_messages
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def charge
17
+ @charge ||= StripeCharge.find_or_initialize_by(stripe_id: object.id)
18
+ end
19
+
20
+ def create_activity
21
+ case event.type
22
+ when "charge.captured"
23
+ charge.create_activity :captured
24
+ when "charge.failed"
25
+ charge.create_activity :failed
26
+ when "charge.pending"
27
+ charge.create_activity :pending
28
+ when "charge.refunded"
29
+ charge.create_activity :refunded
30
+ when "charge.succeeded"
31
+ charge.create_activity :succeeded
32
+ end
33
+ end
34
+
35
+ def create_refunds
36
+ object.refunds.each do |stripe_refund|
37
+ StripeModelCallbacks::Refund::UpdatedService.reported_execute!(object: stripe_refund)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,217 @@
1
+ class StripeModelCallbacks::ConfigureService < StripeModelCallbacks::BaseEventService
2
+ attr_reader :events
3
+
4
+ def initialize(events:)
5
+ @events = events
6
+ end
7
+
8
+ def execute
9
+ all_events
10
+ charge_refund_events
11
+ customer_bank_account_events
12
+ account_external_account_events
13
+ charge_events
14
+ charge_dispute_events
15
+ coupon_events
16
+ customer_discount_events
17
+ customer_events
18
+ customer_source_events
19
+ invoice_item_events
20
+ invoice_events
21
+ order_events
22
+ recipient_events
23
+ payout_events
24
+ plan_events
25
+ product_events
26
+ review_events
27
+ sku_events
28
+ source_events
29
+ subscription_events
30
+ transfer_events
31
+
32
+ succeed!
33
+ end
34
+
35
+ private
36
+
37
+ def account_external_account_events
38
+ %w[created deleted updated].each do |external_account_event|
39
+ subscribe "account.external_account.#{external_account_event}" do |event|
40
+ StripeModelCallbacks::Account::ExternalAccount::UpdatedService.reported_execute!(event: event)
41
+ end
42
+ end
43
+ end
44
+
45
+ def all_events
46
+ events.all do |event|
47
+ StripeModelCallbacks::NotifierService.reported_execute!(event: event)
48
+ end
49
+ end
50
+
51
+ def charge_events
52
+ %w[captured failed pending refunded updated succeeded].each do |charge_event|
53
+ subscribe "charge.#{charge_event}" do |event|
54
+ StripeModelCallbacks::Charge::UpdatedService.reported_execute!(event: event)
55
+ end
56
+ end
57
+ end
58
+
59
+ def charge_refund_events
60
+ subscribe "charge.refund.updated" do |event|
61
+ StripeModelCallbacks::Refund::UpdatedService.reported_execute!(event: event)
62
+ end
63
+ end
64
+
65
+ def charge_dispute_events
66
+ %w[closed created funds_reinstated funds_withdrawn updated].each do |charge_event|
67
+ subscribe "charge.dispute.#{charge_event}" do |event|
68
+ StripeModelCallbacks::Charge::DisputeUpdatedService.reported_execute!(event: event)
69
+ end
70
+ end
71
+ end
72
+
73
+ def coupon_events
74
+ %w[created deleted updated].each do |coupon_event|
75
+ subscribe "coupon.#{coupon_event}" do |event|
76
+ StripeModelCallbacks::Coupon::UpdatedService.reported_execute!(event: event)
77
+ end
78
+ end
79
+ end
80
+
81
+ def customer_bank_account_events
82
+ subscribe "customer.bank_account.deleted" do |event|
83
+ StripeModelCallbacks::Customer::BankAccount::DeletedService.reported_execute!(event: event)
84
+ end
85
+ end
86
+
87
+ def customer_discount_events
88
+ %w[created deleted updated].each do |customer_event|
89
+ subscribe "customer.discount.#{customer_event}" do |event|
90
+ StripeModelCallbacks::Customer::DiscountUpdatedService.reported_execute!(event: event)
91
+ end
92
+ end
93
+ end
94
+
95
+ def customer_events
96
+ %w[created deleted updated].each do |customer_event|
97
+ subscribe "customer.#{customer_event}" do |event|
98
+ StripeModelCallbacks::Customer::UpdatedService.reported_execute!(event: event)
99
+ end
100
+ end
101
+ end
102
+
103
+ def customer_source_events
104
+ %w[created deleted expiring updated].each do |customer_event|
105
+ subscribe "customer.source.#{customer_event}" do |event|
106
+ StripeModelCallbacks::Customer::SourceUpdatedService.reported_execute!(event: event)
107
+ end
108
+ end
109
+ end
110
+
111
+ def invoice_events
112
+ # Upcoming event doesnt send an invoice ID. Dunno what to do about it... Disabling for now
113
+ # upcoming
114
+
115
+ %w[created payment_failed payment_succeeded sent updated].each do |invoice_event|
116
+ subscribe "invoice.#{invoice_event}" do |event|
117
+ StripeModelCallbacks::Invoice::UpdatedService.reported_execute!(event: event)
118
+ end
119
+ end
120
+ end
121
+
122
+ def invoice_item_events
123
+ %w[created deleted updated].each do |event_type|
124
+ subscribe "invoiceitem.#{event_type}" do |event|
125
+ StripeModelCallbacks::InvoiceItem::UpdatedService.reported_execute!(event: event)
126
+ end
127
+ end
128
+ end
129
+
130
+ def order_events
131
+ %w[created updated].each do |order_event|
132
+ subscribe "order.#{order_event}" do |event|
133
+ StripeModelCallbacks::Order::UpdatedService.reported_execute!(event: event)
134
+ end
135
+ end
136
+ end
137
+
138
+ def payout_events
139
+ %w[canceled created failed paid updated].each do |payout_event|
140
+ subscribe "payout.#{payout_event}" do |event|
141
+ StripeModelCallbacks::Payout::UpdatedService.reported_execute!(event: event)
142
+ end
143
+ end
144
+ end
145
+
146
+ def recipient_events
147
+ %w[created deleted updated].each do |recipient_event|
148
+ subscribe "recipient.#{recipient_event}" do |event|
149
+ StripeModelCallbacks::Recipient::UpdatedService.reported_execute!(event: event)
150
+ end
151
+ end
152
+ end
153
+
154
+ def plan_events
155
+ %w[created deleted updated].each do |plan_event|
156
+ subscribe "plan.#{plan_event}" do |event|
157
+ StripeModelCallbacks::Plan::UpdatedService.reported_execute!(event: event)
158
+ end
159
+ end
160
+ end
161
+
162
+ def sku_events
163
+ %w[created deleted updated].each do |sku_event|
164
+ subscribe "sku.#{sku_event}" do |event|
165
+ StripeModelCallbacks::Sku::UpdatedService.reported_execute!(event: event)
166
+ end
167
+ end
168
+ end
169
+
170
+ def product_events
171
+ %w[created deleted updated].each do |product_event|
172
+ subscribe "product.#{product_event}" do |event|
173
+ StripeModelCallbacks::Product::UpdatedService.reported_execute!(event: event)
174
+ end
175
+ end
176
+ end
177
+
178
+ def review_events
179
+ %w[opened closed].each do |review_event|
180
+ subscribe "review.#{review_event}" do |event|
181
+ StripeModelCallbacks::Review::UpdatedService.reported_execute!(event: event)
182
+ end
183
+ end
184
+ end
185
+
186
+ def source_events
187
+ %w[canceled chargeable failed mandate_notification].each do |source_event|
188
+ subscribe "source.#{source_event}" do |event|
189
+ StripeModelCallbacks::Source::UpdatedService.reported_execute!(event: event)
190
+ end
191
+ end
192
+ end
193
+
194
+ def subscribe(event_name)
195
+ events.subscribe(event_name) do |event|
196
+ StripeModelCallbacks::Configuration.current.with_error_handling(args: {event: event}) do
197
+ yield event
198
+ end
199
+ end
200
+ end
201
+
202
+ def subscription_events
203
+ %w[created deleted trial_will_end updated].each do |subscription_event|
204
+ subscribe "customer.subscription.#{subscription_event}" do |event|
205
+ StripeModelCallbacks::Customer::Subscription::UpdatedService.reported_execute!(event: event)
206
+ end
207
+ end
208
+ end
209
+
210
+ def transfer_events
211
+ %w[created reversed updated].each do |transfer_event|
212
+ subscribe "transfer.#{transfer_event}" do |event|
213
+ StripeModelCallbacks::Transfer::UpdatedService.reported_execute!(event: event)
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,18 @@
1
+ class StripeModelCallbacks::Coupon::UpdatedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ coupon.assign_from_stripe(object)
4
+ coupon.deleted_at ||= Time.zone.now if event.type == "coupon.deleted"
5
+
6
+ if coupon.save
7
+ succeed!
8
+ else
9
+ fail! coupon.errors.full_messages
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def coupon
16
+ @coupon ||= StripeCoupon.find_or_initialize_by(stripe_id: object.id)
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ class StripeModelCallbacks::Customer::BankAccount::DeletedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ bank_account = StripeBankAccount.find_or_initialize_by(stripe_id: object.id)
4
+ bank_account.assign_from_stripe(object)
5
+
6
+ if bank_account.save
7
+ bank_account.create_activity :customer_bank_account_deleted if event.type == "customer.bank_account.deleted"
8
+ succeed!
9
+ else
10
+ fail! bank_account.errors.full_messages
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class StripeModelCallbacks::Customer::DeletedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ customer = StripeCustomer.find_or_initialize_by(stripe_id: object.id)
4
+ customer.assign_from_stripe(object)
5
+ customer.deleted_at = Time.zone.now
6
+
7
+ if customer.save
8
+ succeed!
9
+ else
10
+ fail! stripe_customer.errors.full_messages
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ class StripeModelCallbacks::Customer::DiscountUpdatedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ discount.assign_from_stripe(object)
4
+ discount.deleted_at ||= Time.zone.now if event&.type == "customer.discount.deleted"
5
+
6
+ if discount.save
7
+ create_activity
8
+ succeed! discount
9
+ else
10
+ fail! discount.errors.full_messages
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def create_activity
17
+ discount.create_activity :deleted if event&.type == "customer.discount.deleted"
18
+ end
19
+
20
+ def coupon_id_look_up_by
21
+ event.try(:data).try(:previous_attributes).try(:coupon).try(:id) ||
22
+ object.coupon.id
23
+ end
24
+
25
+ def discount
26
+ @discount ||= StripeDiscount.find_or_initialize_by(
27
+ stripe_coupon_id: coupon_id_look_up_by,
28
+ stripe_customer_id: object.customer,
29
+ stripe_subscription_id: object.subscription
30
+ )
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ class StripeModelCallbacks::Customer::SourceUpdatedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ source = stripe_class.find_or_initialize_by(stripe_id: object.id)
4
+ source.assign_from_stripe(object)
5
+ source.deleted_at = Time.zone.now if event.type == "customer.source.deleted"
6
+
7
+ if source.save
8
+ source.create_activity :deleted if event.type == "customer.source.deleted"
9
+ source.create_activity :expiring if event.type == "customer.source.expiring"
10
+ succeed!
11
+ else
12
+ fail! source.errors.full_messages
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def stripe_class
19
+ if object.object == "card"
20
+ StripeCard
21
+ else
22
+ StripeSource
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ class StripeModelCallbacks::Customer::Subscription::UpdatedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ subscription.assign_from_stripe(object)
4
+ subscription.deleted_at ||= Time.zone.now if event.type == "customer.subscription.deleted"
5
+
6
+ if subscription.save
7
+ create_activity
8
+ succeed!
9
+ else
10
+ fail! subscription.errors.full_messages
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def create_activity
17
+ case event.type
18
+ when "customer.subscription.trial_will_end"
19
+ subscription.create_activity :trial_will_end
20
+ when "customer.subscription.deleted"
21
+ subscription.create_activity :deleted
22
+ end
23
+ end
24
+
25
+ def subscription
26
+ @subscription ||= StripeSubscription.find_or_initialize_by(stripe_id: object.id)
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ class StripeModelCallbacks::Customer::UpdatedService < StripeModelCallbacks::BaseEventService
2
+ def execute
3
+ customer = StripeCustomer.find_or_initialize_by(stripe_id: object.id)
4
+ customer.assign_from_stripe(object)
5
+ customer.deleted_at ||= Time.zone.now if event.type == "customer.deleted"
6
+
7
+ if customer.save
8
+ succeed!
9
+ else
10
+ fail! customer.errors.full_messages
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ class StripeModelCallbacks::EventMockerService
2
+ attr_reader :args, :name, :scope
3
+
4
+ def initialize(args:, name:, scope:)
5
+ @args = args
6
+ @name = name
7
+ @scope = scope
8
+
9
+ bypass_event_signature(payload)
10
+ post_event
11
+ end
12
+
13
+ private
14
+
15
+ def bypass_event_signature(payload)
16
+ event = Stripe::Event.construct_from(payload)
17
+ scope.expect(Stripe::Webhook).to scope.receive(:construct_event).and_return(event)
18
+ end
19
+
20
+ def first_part
21
+ @first_part ||= name.split(".").first
22
+ end
23
+
24
+ def fixture_path
25
+ @fixture_path ||= "#{File.dirname(__FILE__)}/../../../spec/fixtures/stripe_events/#{first_part}/#{name}.json"
26
+ end
27
+
28
+ def payload
29
+ file_content = File.read(fixture_path)
30
+ data = JSON.parse(file_content, symbolize_names: true)
31
+ data.deep_merge!(args) if args
32
+ data
33
+ end
34
+
35
+ def post_event
36
+ scope.post("/stripe-events", params: JSON.generate(payload))
37
+ end
38
+ end