flowcommerce_spree 0.0.2 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -13
  3. data/SPREE_FLOW.md +6 -28
  4. data/app/controllers/concerns/current_zone_loader_decorator.rb +33 -25
  5. data/app/controllers/flowcommerce_spree/inventory_controller.rb +23 -0
  6. data/app/controllers/flowcommerce_spree/orders_controller.rb +20 -0
  7. data/app/controllers/flowcommerce_spree/webhooks_controller.rb +23 -13
  8. data/app/controllers/users/sessions_controller_decorator.rb +28 -0
  9. data/app/helpers/spree/core/controller_helpers/flow_io_order_helper_decorator.rb +4 -9
  10. data/app/models/spree/address_decorator.rb +19 -0
  11. data/app/models/spree/calculator/flow_io.rb +61 -0
  12. data/app/models/spree/calculator/shipping/flow_io.rb +40 -0
  13. data/app/models/spree/flow_io_credit_card_decorator.rb +21 -0
  14. data/app/models/spree/flow_io_order_decorator.rb +163 -0
  15. data/app/models/spree/flow_io_variant_decorator.rb +4 -2
  16. data/app/models/spree/gateway/flow_io.rb +153 -0
  17. data/app/models/spree/{credit_card_decorator.rb → payment_capture_event_decorator.rb} +1 -1
  18. data/app/models/spree/promotion_handler/coupon_decorator.rb +1 -1
  19. data/app/models/spree/zones/flow_io_product_zone_decorator.rb +8 -0
  20. data/app/models/tracking/setup_decorator.rb +40 -0
  21. data/app/overrides/spree/admin/order_sidebar_summary_flow_link.rb +13 -0
  22. data/app/overrides/spree/admin/products/order_price_flow_message.rb +9 -0
  23. data/app/serializers/api/v2/order_serializer_decorator.rb +20 -0
  24. data/app/services/flowcommerce_spree/import_experience_items.rb +1 -1
  25. data/app/services/flowcommerce_spree/order_sync.rb +81 -173
  26. data/app/services/flowcommerce_spree/order_updater.rb +78 -0
  27. data/app/services/flowcommerce_spree/webhooks/capture_upserted_v2.rb +76 -0
  28. data/app/services/flowcommerce_spree/webhooks/card_authorization_upserted_v2.rb +66 -0
  29. data/app/services/flowcommerce_spree/webhooks/experience_upserted_v2.rb +25 -0
  30. data/app/services/flowcommerce_spree/webhooks/fraud_status_changed.rb +35 -0
  31. data/app/services/flowcommerce_spree/webhooks/local_item_upserted.rb +40 -0
  32. data/app/views/spree/admin/payments/source_views/_flow_io_gateway.html.erb +21 -0
  33. data/config/rails_best_practices.yml +51 -0
  34. data/config/routes.rb +3 -1
  35. data/db/migrate/20201021755957_add_meta_to_spree_tables.rb +6 -4
  36. data/lib/flow/simple_gateway.rb +0 -36
  37. data/lib/flowcommerce_spree.rb +17 -3
  38. data/lib/flowcommerce_spree/engine.rb +33 -3
  39. data/lib/flowcommerce_spree/experience_service.rb +1 -27
  40. data/lib/flowcommerce_spree/logging_http_client.rb +33 -15
  41. data/lib/flowcommerce_spree/session.rb +17 -32
  42. data/lib/flowcommerce_spree/test_support.rb +7 -0
  43. data/lib/flowcommerce_spree/version.rb +1 -1
  44. data/lib/tasks/flowcommerce_spree.rake +4 -1
  45. metadata +88 -21
  46. data/app/mailers/spree/spree_order_mailer_decorator.rb +0 -24
  47. data/app/models/spree/gateway/spree_flow_gateway.rb +0 -116
  48. data/app/models/spree/line_item_decorator.rb +0 -15
  49. data/app/models/spree/order_decorator.rb +0 -179
  50. data/app/views/spree/order_mailer/confirm_email.html.erb +0 -86
  51. data/app/views/spree/order_mailer/confirm_email.text.erb +0 -38
  52. data/config/initializers/flowcommerce_spree.rb +0 -7
  53. data/lib/flow/error.rb +0 -73
  54. data/lib/flow/pay_pal.rb +0 -25
  55. data/lib/flowcommerce_spree/webhook_service.rb +0 -98
  56. data/lib/simple_csv_writer.rb +0 -44
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ module Webhooks
5
+ class CardAuthorizationUpsertedV2
6
+ attr_reader :errors
7
+ alias full_messages errors
8
+
9
+ def self.process(data)
10
+ new(data).process
11
+ end
12
+
13
+ def initialize(data)
14
+ @data = data['authorization']&.to_hash
15
+ @data&.[]('method')&.delete('images')
16
+ @errors = []
17
+ end
18
+
19
+ def process
20
+ errors << { message: 'Authorization param missing' } && (return self) unless @data
21
+
22
+ errors << { message: 'Card param missing' } && (return self) unless (flow_io_card = @data.delete('card'))
23
+
24
+ if (order_number = @data.dig('order', 'number'))
25
+ if (order = Spree::Order.find_by(number: order_number))
26
+ card = upsert_card(flow_io_card, order)
27
+
28
+ order.payments.where(response_code: @data['id'])
29
+ .update_all(source_id: card.id, source_type: 'Spree::CreditCard')
30
+
31
+ return card
32
+ else
33
+ errors << { message: "Order #{order_number} not found" }
34
+ end
35
+ else
36
+ errors << { message: 'Order number param missing' }
37
+ end
38
+
39
+ self
40
+ end
41
+
42
+ private
43
+
44
+ def upsert_card(flow_io_card, order)
45
+ flow_io_card_expiration = flow_io_card.delete('expiration')
46
+
47
+ card = Spree::CreditCard.find_or_initialize_by(month: flow_io_card_expiration['month'].to_s,
48
+ year: flow_io_card_expiration['year'].to_s,
49
+ cc_type: flow_io_card.delete('type'),
50
+ last_digits: flow_io_card.delete('last4'),
51
+ name: flow_io_card.delete('name'),
52
+ user_id: order.user&.id)
53
+ card.flow_data ||= {}
54
+ if card.new_record?
55
+ card.flow_data.merge!(flow_io_card.except('discriminator'))
56
+ card.imported = true
57
+ end
58
+
59
+ card.push_authorization(@data.except('discriminator'))
60
+ card.new_record? ? card.save : card.update_column(:meta, card.meta.to_json)
61
+
62
+ card
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ module Webhooks
5
+ class ExperienceUpsertedV2
6
+ attr_accessor :errors
7
+ alias full_messages errors
8
+
9
+ def self.process(data, opts = {})
10
+ new(data, opts).process
11
+ end
12
+
13
+ def initialize(data, opts = {})
14
+ @data = data
15
+ @opts = opts
16
+ @errors = []
17
+ end
18
+
19
+ def process
20
+ experience = @data['experience']
21
+ Spree::Zones::Product.find_or_initialize_by(name: experience['key'].titleize).store_flow_io_data(experience)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ module Webhooks
5
+ class FraudStatusChanged
6
+ attr_accessor :errors
7
+ alias full_messages errors
8
+
9
+ def self.process(data, opts = {})
10
+ new(data, opts).process
11
+ end
12
+
13
+ def initialize(data, opts = {})
14
+ @data = data
15
+ @opts = opts
16
+ @errors = []
17
+ end
18
+
19
+ def process
20
+ order_number = @data.dig('order', 'number')
21
+ errors << { message: 'Order number param missing' } && (return self) unless order_number
22
+
23
+ order = Spree::Order.find_by(number: order_number)
24
+ errors << { message: "Order #{order_number} not found" } && (return self) unless order
25
+
26
+ if @data['status'] == 'declined'
27
+ order.update_columns(fraudulent: true)
28
+ order.cancel!
29
+ end
30
+
31
+ order
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ module Webhooks
5
+ class LocalItemUpserted
6
+ attr_accessor :errors
7
+ alias full_messages errors
8
+
9
+ def self.process(data, opts = {})
10
+ new(data, opts).process
11
+ end
12
+
13
+ def initialize(data, opts = {})
14
+ @data = data
15
+ @opts = opts
16
+ @errors = []
17
+ end
18
+
19
+ def process
20
+ errors << { message: 'Local item param missing' } && (return self) unless (local_item = @data['local_item'])
21
+
22
+ errors << { message: 'SKU param missing' } && (return self) unless (flow_sku = local_item.dig('item', 'number'))
23
+
24
+ if (variant = Spree::Variant.find_by(sku: flow_sku))
25
+ variant.add_flow_io_experience_data(
26
+ local_item.dig('experience', 'key'),
27
+ 'prices' => [local_item.dig('pricing', 'price')], 'status' => local_item['status']
28
+ )
29
+
30
+ variant.update_column(:meta, variant.meta.to_json)
31
+ return variant
32
+ else
33
+ errors << { message: "Variant with sku [#{flow_sku}] not found!" }
34
+ end
35
+
36
+ self
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ <fieldset data-hook="credit_card">
2
+ <legend align="center"><%= Spree.t(:credit_card) %></legend>
3
+
4
+ <div class="row">
5
+ <div class="alpha six columns">
6
+ <dl>
7
+ <dt><%= Spree.t(:name_on_card) %>:</dt>
8
+ <dd><%= payment.source&.name %></dd>
9
+
10
+ <dt><%= Spree.t(:card_type) %>:</dt>
11
+ <dd><%= payment.source&.cc_type %></dd>
12
+
13
+ <dt><%= Spree.t(:card_number) %>:</dt>
14
+ <dd><%= payment.source&.display_number %></dd>
15
+
16
+ <dt><%= Spree.t(:expiration) %>:</dt>
17
+ <dd><%= payment.source&.month %>/<%= payment.source&.year %></dd>
18
+ </dl>
19
+ </div>
20
+ </div>
21
+ </fieldset>
@@ -0,0 +1,51 @@
1
+ AddModelVirtualAttributeCheck: { }
2
+ AlwaysAddDbIndexCheck: { }
3
+ #CheckSaveReturnValueCheck: { }
4
+ #CheckDestroyReturnValueCheck: { }
5
+ DefaultScopeIsEvilCheck: { }
6
+ DryBundlerInCapistranoCheck: { }
7
+ #HashSyntaxCheck: { }
8
+ IsolateSeedDataCheck: { }
9
+ KeepFindersOnTheirOwnModelCheck: { }
10
+ LawOfDemeterCheck: { }
11
+ #LongLineCheck: { max_line_length: 80 }
12
+ MoveCodeIntoControllerCheck: { }
13
+ MoveCodeIntoHelperCheck: { array_count: 3 }
14
+ MoveCodeIntoModelCheck: { use_count: 2 }
15
+ MoveFinderToNamedScopeCheck: { }
16
+ MoveModelLogicIntoModelCheck: { use_count: 4 }
17
+ NeedlessDeepNestingCheck: { nested_count: 2 }
18
+ NotRescueExceptionCheck: { }
19
+ NotUseDefaultRouteCheck: { }
20
+ NotUseTimeAgoInWordsCheck: { }
21
+ OveruseRouteCustomizationsCheck: { customize_count: 3 }
22
+ ProtectMassAssignmentCheck: { }
23
+ RemoveEmptyHelpersCheck: { }
24
+ #RemoveTabCheck: { }
25
+ RemoveTrailingWhitespaceCheck: { }
26
+ RemoveUnusedMethodsInControllersCheck: { except_methods: [] }
27
+ RemoveUnusedMethodsInHelpersCheck: { except_methods: [] }
28
+ RemoveUnusedMethodsInModelsCheck: { except_methods:
29
+ [
30
+ 'Spree::Calculator::FlowIo#compute_shipment', # Used by Spree::Calculator
31
+ 'Spree::Calculator::FlowIo#compute_line_item', # Used by Spree::Calculator
32
+ 'Spree::Calculator::FlowIo#description', # Used by Spree
33
+ 'Spree::Shipping::FlowIo#compute_package', # Used by Spree
34
+ 'Spree::Shipping::FlowIo#default_charge', # Used by Spree
35
+ 'Spree::Shipping::FlowIo#threshold', # Used by Spree
36
+ 'Spree::Shipping::FlowIo#description', # Used by Spree
37
+ ] }
38
+ ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 }
39
+ ReplaceInstanceVariableWithLocalVariableCheck: { }
40
+ RestrictAutoGeneratedRoutesCheck: { }
41
+ SimplifyRenderInControllersCheck: { }
42
+ SimplifyRenderInViewsCheck: { }
43
+ #UseBeforeFilterCheck: { customize_count: 2 }
44
+ UseModelAssociationCheck: { }
45
+ UseMultipartAlternativeAsContentTypeOfEmailCheck: { }
46
+ #UseParenthesesInMethodDefCheck: { }
47
+ UseObserverCheck: { }
48
+ UseQueryAttributeCheck: { }
49
+ UseSayWithTimeInMigrationsCheck: { }
50
+ UseScopeAccessCheck: { }
51
+ UseTurboSprocketsRails3Check: { }
data/config/routes.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  FlowcommerceSpree::Engine.routes.draw do
4
- post '/event-target', to: 'webhooks#handle_flow_web_hook_event'
4
+ post '/event-target', to: 'webhooks#handle_flow_io_event'
5
+ get '/order-completed', to: 'orders#order_completed'
6
+ post '/online-stock-availability', to: 'inventory#online_stock_availability'
5
7
  end
@@ -5,13 +5,15 @@ class AddMetaToSpreeTables < ActiveRecord::Migration
5
5
  add_column :spree_orders, :meta, :jsonb, default: '{}' unless column_exists?(:spree_orders, :meta)
6
6
  add_column :spree_promotions, :meta, :jsonb, default: '{}' unless column_exists?(:spree_promotions, :meta)
7
7
  add_column :spree_credit_cards, :meta, :jsonb, default: '{}' unless column_exists?(:spree_credit_cards, :meta)
8
+ add_column :spree_payment_capture_events, :meta, :jsonb, default: '{}' unless column_exists?(:spree_payment_capture_events, :meta)
8
9
  end
9
10
 
10
11
  def down
11
- remove_column :spree_products, :meta if column_exists?(:spree_products, :meta)
12
- remove_column :spree_variants, :meta if column_exists?(:spree_variants, :meta)
13
- remove_column :spree_orders, :meta if column_exists?(:spree_orders, :meta)
14
- remove_column :spree_promotions, :meta if column_exists?(:spree_promotions, :meta)
12
+ remove_column :spree_payment_capture_events, :meta if column_exists?(:spree_payment_capture_events, :meta)
15
13
  remove_column :spree_credit_cards, :meta if column_exists?(:spree_credit_cards, :meta)
14
+ remove_column :spree_promotions, :meta if column_exists?(:spree_promotions, :meta)
15
+ remove_column :spree_orders, :meta if column_exists?(:spree_orders, :meta)
16
+ remove_column :spree_variants, :meta if column_exists?(:spree_variants, :meta)
17
+ remove_column :spree_products, :meta if column_exists?(:spree_products, :meta)
16
18
  end
17
19
  end
@@ -34,42 +34,6 @@ module Flow
34
34
  error_response(e)
35
35
  end
36
36
 
37
- # capture authorised funds
38
- def cc_capture
39
- # GET /:organization/authorizations, order_number: abc
40
- data = @order.flow_data['authorization']
41
-
42
- raise ArgumentError, 'No Authorization data, please authorize first' unless data
43
-
44
- capture_form = ::Io::Flow::V0::Models::CaptureForm.new(data)
45
- response = FlowcommerceSpree.client.captures.post(FlowcommerceSpree::ORGANIZATION, capture_form)
46
-
47
- return ActiveMerchant::Billing::Response.new false, 'error', response: response unless response.id
48
-
49
- @order.update_column :flow_data, @order.flow_data.merge('capture': response.to_hash)
50
- @order.flow_finalize!
51
-
52
- ActiveMerchant::Billing::Response.new true, 'success', response: response
53
- rescue StandardError => e
54
- error_response(e)
55
- end
56
-
57
- def cc_refund
58
- raise ArgumentError, 'capture info is not available' unless @order.flow_data['capture']
59
-
60
- # we allways have capture ID, so we use it
61
- refund_data = { capture_id: @order.flow_data['capture']['id'] }
62
- refund_form = ::Io::Flow::V0::Models::RefundForm.new(refund_data)
63
- response = FlowcommerceSpree.client.refunds.post(FlowcommerceSpree::ORGANIZATION, refund_form)
64
-
65
- return ActiveMerchant::Billing::Response.new false, 'error', response: response unless response.id
66
-
67
- @order.update_column :flow_data, @order.flow_data.merge('refund': response.to_hash)
68
- ActiveMerchant::Billing::Response.new true, 'success', response: response
69
- rescue StandardError => e
70
- error_response(e)
71
- end
72
-
73
37
  private
74
38
 
75
39
  # if order is not in flow, we use local Spree settings
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'activerecord-postgres-json'
4
+ require 'active_model_serializers'
3
5
  require 'flowcommerce'
4
6
  require 'flowcommerce_spree/api'
5
7
  require 'flowcommerce_spree/refresher'
6
8
  require 'flowcommerce_spree/engine'
7
9
  require 'flowcommerce_spree/logging_http_client'
8
10
  require 'flowcommerce_spree/logging_http_handler'
9
- require 'flowcommerce_spree/webhook_service'
10
11
  require 'flowcommerce_spree/session'
11
12
  require 'flow/simple_gateway'
13
+ require 'oj'
12
14
 
13
15
  module FlowcommerceSpree
16
+ API_KEY = ENV.fetch('FLOW_TOKEN', 'test_key')
17
+ ENV['FLOW_TOKEN'] = API_KEY
18
+
14
19
  def self.client(logger: FlowcommerceSpree.logger, **opts)
15
20
  FlowCommerce.instance(http_handler: LoggingHttpHandler.new(logger: logger), **opts)
16
21
  end
@@ -20,11 +25,20 @@ module FlowcommerceSpree
20
25
  end
21
26
 
22
27
  def self.logger
23
- logger = ActiveSupport::Logger.new(STDOUT, 3, 10_485_760)
28
+ logger = ActiveSupport::Logger.new(STDOUT)
29
+
30
+ logger_formatter = proc do |severity, datetime, _progname, msg|
31
+ "\n#{datetime}, #{severity}: #{msg}\n"
32
+ end
33
+
34
+ logger.formatter = logger_formatter
24
35
 
25
36
  # Broadcast the log into the file besides STDOUT, if `log` folder exists
26
37
  if Dir.exist?('log')
27
- logger.extend(ActiveSupport::Logger.broadcast(ActiveSupport::Logger.new('log/flowcommerce_spree.log')))
38
+ file_logger = ActiveSupport::Logger.new('log/flowcommerce_spree.log', 3, 10_485_760)
39
+ file_logger.formatter = logger_formatter
40
+
41
+ logger.extend(ActiveSupport::Logger.broadcast(file_logger))
28
42
  end
29
43
  logger
30
44
  end
@@ -6,22 +6,52 @@ module FlowcommerceSpree
6
6
  isolate_namespace FlowcommerceSpree
7
7
 
8
8
  config.before_initialize do
9
+ FlowcommerceSpree::ORGANIZATION = ENV.fetch('FLOW_ORGANIZATION', 'flow.io')
10
+ FlowcommerceSpree::BASE_COUNTRY = ENV.fetch('FLOW_BASE_COUNTRY', 'USA')
11
+ FlowcommerceSpree::API_KEY = ENV.fetch('FLOW_TOKEN', 'test_key')
12
+ FlowcommerceSpree::FLOW_IO_WEBHOOK_USER = ENV.fetch('FLOW_IO_WEBHOOK_USER', 'test_user')
13
+ FlowcommerceSpree::FLOW_IO_WEBHOOK_PASSWORD = ENV.fetch('FLOW_IO_WEBHOOK_PASSWORD', 'test_password')
14
+
9
15
  FlowcommerceSpree::Config = FlowcommerceSpree::Settings.new
10
16
  end
11
17
 
12
- config.after_initialize do
18
+ config.flowcommerce_spree = ActiveSupport::OrderedOptions.new
19
+
20
+ initializer 'flowcommerce_spree.configuration' do |app|
21
+ # If some Rake tasks will fail in development environment, the cause could be the autoloading.
22
+ # Uncommenting the following 3 lines will enable eager-loading for the flowcommerce_spree Rake tasks.
23
+ # if Rails.env.development?
24
+ # app.config.eager_load = Rake.application.top_level_tasks.any? { |t| t.start_with?('flowcommerce_spree') }
25
+ # end
26
+
27
+ app.config.flowcommerce_spree[:mounted_path] = ENV.fetch('FLOW_MOUNT_PATH', '/flow')
28
+
29
+ app.routes.prepend do
30
+ mount FlowcommerceSpree::Engine => app.config.flowcommerce_spree[:mounted_path]
31
+ end
32
+ end
33
+
34
+ config.after_initialize do |app|
13
35
  # init Flow payments as an option
14
- # app.config.spree.payment_methods << Spree::Gateway::Flow
36
+ app.config.spree.payment_methods << Spree::Gateway::FlowIo
15
37
 
16
- Flow::SimpleGateway.clear_zero_amount_payments = true
38
+ # Flow::SimpleGateway.clear_zero_amount_payments = true
17
39
  end
18
40
 
19
41
  def self.activate
20
42
  Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')).sort.each do |c|
21
43
  Rails.configuration.cache_classes ? require(c) : load(c)
22
44
  end
45
+ Dir.glob(File.join(File.dirname(__FILE__), '../app/overrides/*.rb')).sort.each do |c|
46
+ Rails.configuration.cache_classes ? require(c) : load(c)
47
+ end
23
48
  end
24
49
 
25
50
  config.to_prepare(&method(:activate).to_proc)
51
+
52
+ initializer 'spree.flowcommerce_spree.calculators', after: 'spree.register.calculators' do |_app|
53
+ Rails.application.config.spree.calculators.tax_rates << Spree::Calculator::FlowIo
54
+ Rails.application.config.spree.calculators.shipping_methods << Spree::Calculator::Shipping::FlowIo
55
+ end
26
56
  end
27
57
  end
@@ -22,18 +22,6 @@ module FlowcommerceSpree
22
22
  nil
23
23
  end
24
24
 
25
- def get_by_subcatalog_id(subcatalog_id)
26
- fetch_from_flow.each do |experince|
27
- return experince if experince.subcatalog.id == subcatalog_id
28
- end
29
-
30
- nil
31
- end
32
-
33
- def compact
34
- all.map { |exp| [exp.country, exp.key, exp.name] }
35
- end
36
-
37
25
  def default
38
26
  FlowcommerceSpree::ExperienceService
39
27
  .all.select { |exp| exp.key.downcase == ENV.fetch('FLOW_BASE_COUNTRY').downcase }.first
@@ -42,24 +30,10 @@ module FlowcommerceSpree
42
30
  private
43
31
 
44
32
  def fetch_from_flow
45
- # return cached_experinces if cache_valid?
46
-
47
- experiences = FlowcommerceSpree.client.experiences.get ORGANIZATION
33
+ FlowcommerceSpree.client.experiences.get ORGANIZATION
48
34
 
49
35
  # work with active axperiences only
50
36
  # experiences = experiences.select { |it| it.status.value == 'active' }
51
-
52
- # @cache = [experiences, Time.now]
53
- experiences
54
- end
55
-
56
- def cache_valid?
57
- # cache experinces in worker memory for 1 minute
58
- @cache && @cache[1] > Time.now.ago(1.minute)
59
- end
60
-
61
- def cached_experinces
62
- @cache[0]
63
37
  end
64
38
  end
65
39
  end