workarea-zipco 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/.eslintrc.json +35 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. data/.github/workflows/ci.yml +64 -0
  7. data/.gitignore +23 -0
  8. data/.rubocop.yml +2 -0
  9. data/.stylelintrc.json +8 -0
  10. data/CHANGELOG.md +17 -0
  11. data/Gemfile +17 -0
  12. data/README.md +51 -0
  13. data/Rakefile +59 -0
  14. data/app/assets/images/zipco/.keep +0 -0
  15. data/app/assets/stylesheets/workarea/storefront/zipco/components/_zipco_icon.scss +7 -0
  16. data/app/controllers/storefront/checkout/place_order_controller.decorator +11 -0
  17. data/app/controllers/storefront/zipco_controller.rb +96 -0
  18. data/app/controllers/storefront/zipco_landing_controller.rb +6 -0
  19. data/app/helpers/.keep +0 -0
  20. data/app/mailers/.keep +0 -0
  21. data/app/models/checkout.decorator +33 -0
  22. data/app/models/workarea/order.decorator +52 -0
  23. data/app/models/workarea/order/status/zip_referred.rb +13 -0
  24. data/app/models/workarea/payment.decorator +30 -0
  25. data/app/models/workarea/payment/authorize/zipco.rb +66 -0
  26. data/app/models/workarea/payment/capture/zipco.rb +58 -0
  27. data/app/models/workarea/payment/purchase/zipco.rb +66 -0
  28. data/app/models/workarea/payment/refund/zipco.rb +59 -0
  29. data/app/models/workarea/payment/tender/zipco.rb +13 -0
  30. data/app/models/workarea/search/admin/admin.decorator +7 -0
  31. data/app/services/workarea/zipco/checkout.rb +48 -0
  32. data/app/services/workarea/zipco/order.rb +188 -0
  33. data/app/services/workarea/zipco/setup.rb +37 -0
  34. data/app/view_models/workarea/storefront/checkout/payment_view_model.decorator +17 -0
  35. data/app/view_models/workarea/storefront/order_view_model.decorator +11 -0
  36. data/app/view_models/workarea/storefront/zipco_view_model.rb +18 -0
  37. data/app/views/workarea/admin/orders/tenders/_zipco.html.haml +4 -0
  38. data/app/views/workarea/storefront/checkouts/_zipco_payment.html.haml +10 -0
  39. data/app/views/workarea/storefront/order_mailer/tenders/_zipco.html.haml +4 -0
  40. data/app/views/workarea/storefront/order_mailer/tenders/_zipco.text.erb +1 -0
  41. data/app/views/workarea/storefront/orders/_zipco_order_message.html.haml +2 -0
  42. data/app/views/workarea/storefront/orders/tenders/_zipco.html.haml +6 -0
  43. data/app/views/workarea/storefront/zipco/_zipco_tagline.html.haml +4 -0
  44. data/app/views/workarea/storefront/zipco_landing/show.html.haml +4 -0
  45. data/bin/rails +25 -0
  46. data/config/initializers/appends.rb +24 -0
  47. data/config/initializers/workarea.rb +15 -0
  48. data/config/locales/en.yml +38 -0
  49. data/config/routes.rb +5 -0
  50. data/lib/workarea/zipco.rb +48 -0
  51. data/lib/workarea/zipco/bogus_gateway.rb +160 -0
  52. data/lib/workarea/zipco/engine.rb +10 -0
  53. data/lib/workarea/zipco/gateway.rb +113 -0
  54. data/lib/workarea/zipco/response.rb +29 -0
  55. data/lib/workarea/zipco/version.rb +5 -0
  56. data/test/dummy/.ruby-version +1 -0
  57. data/test/dummy/Rakefile +6 -0
  58. data/test/dummy/app/assets/config/manifest.js +3 -0
  59. data/test/dummy/app/assets/images/.keep +0 -0
  60. data/test/dummy/app/assets/javascripts/application.js +14 -0
  61. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  62. data/test/dummy/app/controllers/application_controller.rb +2 -0
  63. data/test/dummy/app/controllers/concerns/.keep +0 -0
  64. data/test/dummy/app/helpers/application_helper.rb +2 -0
  65. data/test/dummy/app/jobs/application_job.rb +2 -0
  66. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  67. data/test/dummy/app/models/concerns/.keep +0 -0
  68. data/test/dummy/app/views/layouts/application.html.erb +15 -0
  69. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  70. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  71. data/test/dummy/bin/bundle +3 -0
  72. data/test/dummy/bin/rails +4 -0
  73. data/test/dummy/bin/rake +4 -0
  74. data/test/dummy/bin/setup +28 -0
  75. data/test/dummy/bin/update +28 -0
  76. data/test/dummy/bin/yarn +11 -0
  77. data/test/dummy/config.ru +5 -0
  78. data/test/dummy/config/application.rb +34 -0
  79. data/test/dummy/config/boot.rb +5 -0
  80. data/test/dummy/config/environment.rb +5 -0
  81. data/test/dummy/config/environments/development.rb +52 -0
  82. data/test/dummy/config/environments/production.rb +83 -0
  83. data/test/dummy/config/environments/test.rb +45 -0
  84. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  85. data/test/dummy/config/initializers/assets.rb +14 -0
  86. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  87. data/test/dummy/config/initializers/content_security_policy.rb +25 -0
  88. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  89. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  90. data/test/dummy/config/initializers/inflections.rb +16 -0
  91. data/test/dummy/config/initializers/mime_types.rb +4 -0
  92. data/test/dummy/config/initializers/workarea.rb +5 -0
  93. data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
  94. data/test/dummy/config/locales/en.yml +33 -0
  95. data/test/dummy/config/puma.rb +34 -0
  96. data/test/dummy/config/routes.rb +5 -0
  97. data/test/dummy/config/spring.rb +6 -0
  98. data/test/dummy/db/seeds.rb +2 -0
  99. data/test/dummy/lib/assets/.keep +0 -0
  100. data/test/dummy/log/.keep +0 -0
  101. data/test/integration/workarea/storefront/zipco_integration_test.rb +250 -0
  102. data/test/models/workarea/order/zipco_queries_test.rb +35 -0
  103. data/test/models/workarea/payment/zipco_payment_integration_test.rb +89 -0
  104. data/test/services/workarea/zipco/checkout_test.rb +43 -0
  105. data/test/services/workarea/zipco/order_test.rb +102 -0
  106. data/test/services/workarea/zipco/setup_test.rb +65 -0
  107. data/test/teaspoon_env.rb +6 -0
  108. data/test/test_helper.rb +10 -0
  109. data/test/view_models/workarea/storefront/zipco_view_model_test.rb +55 -0
  110. data/workarea-zipco.gemspec +19 -0
  111. metadata +167 -0
@@ -0,0 +1,188 @@
1
+ module Workarea
2
+ module Zipco
3
+ class Order
4
+ module ProductUrl
5
+ include Workarea::I18n::DefaultUrlOptions
6
+ include Storefront::Engine.routes.url_helpers
7
+ extend self
8
+ end
9
+
10
+ module ProductImageUrl
11
+ include Workarea::ApplicationHelper
12
+ include Workarea::I18n::DefaultUrlOptions
13
+ include ActionView::Helpers::AssetUrlHelper
14
+ include Core::Engine.routes.url_helpers
15
+ extend self
16
+
17
+ def mounted_core
18
+ self
19
+ end
20
+ end
21
+
22
+ attr_reader :order
23
+
24
+ # @param ::Workarea::Order
25
+ def initialize(order)
26
+ @order = Workarea::Storefront::OrderViewModel.new(order)
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ shopper: zipco_shopper,
32
+ order: zipco_order,
33
+ config: zipco_config
34
+ }
35
+ end
36
+
37
+ private
38
+ def zipco_shopper
39
+ {
40
+ first_name: order.billing_address.first_name,
41
+ last_name: order.billing_address.last_name,
42
+ phone: order.billing_address.phone_number,
43
+ email: order.email,
44
+ billing_address: {
45
+ line1: order.billing_address.street,
46
+ line2: order.billing_address.street_2,
47
+ city: order.billing_address.city,
48
+ state: order.billing_address.region,
49
+ postal_code: order.billing_address.postal_code,
50
+ country: order.billing_address.country.alpha2
51
+ }
52
+ }
53
+ end
54
+
55
+ def zipco_order
56
+ {
57
+ reference: order.id,
58
+ amount: order.order_balance.to_f,
59
+ currency: currency_code,
60
+ shipping: zip_shipping,
61
+ items: items
62
+ }
63
+ end
64
+
65
+ def currency_code
66
+ @currency_code = order.total_price.currency.iso_code
67
+ end
68
+
69
+ def zip_shipping
70
+ return unless shipping.present?
71
+ {
72
+ pickup: false, # integrate this with bopus?
73
+ tracking: {
74
+ carrier: shipping_service
75
+ },
76
+ address: {
77
+ line1: order.shipping_address.street,
78
+ city: order.shipping_address.city,
79
+ state: order.shipping_address.region,
80
+ postal_code: order.shipping_address.postal_code,
81
+ country: order.shipping_address.country.alpha2
82
+ }
83
+ }
84
+ end
85
+
86
+ def shipping
87
+ @shipping = Workarea::Shipping.find_by_order(order.id)
88
+ end
89
+
90
+ def shipping_service
91
+ return unless shipping.present?
92
+ shipping.shipping_service.name
93
+ end
94
+
95
+ def items
96
+ items_array = order.items.map do |oi|
97
+
98
+ url = ProductImageUrl.product_image_url(oi.image, :detail)
99
+ item_image = url.starts_with?("/") ? nil : url
100
+
101
+ {
102
+ name: oi.product.name,
103
+ amount: oi.original_unit_price.to_f,
104
+ quantity: oi.quantity,
105
+ type: "sku",
106
+ reference: oi.id,
107
+ image_uri: item_image,
108
+ item_uri: ProductUrl.product_url(id: oi.product.to_param, host: Workarea.config.host)
109
+ }
110
+ end
111
+
112
+ # add taxes as item.
113
+ items_array << {
114
+ name: "Tax",
115
+ amount: order.tax_total.to_f,
116
+ quantity: 1,
117
+ type: "tax",
118
+ reference: "#{order.id}-tax",
119
+ }
120
+
121
+ # add shipping as item
122
+ items_array << {
123
+ name: "Shipping",
124
+ amount: order.shipping_total.to_f,
125
+ quantity: 1,
126
+ type: "shipping",
127
+ reference: "#{order.id}-shipping",
128
+ }
129
+
130
+ if gift_wrap_item.present?
131
+ items_array << gift_wrap_item
132
+ end
133
+
134
+ items_array + discount_items + advanced_payment_discount_item
135
+ end
136
+
137
+ def discount_items
138
+ discounts = order.price_adjustments.select { |p| p.discount? }
139
+ return [] unless discounts.present?
140
+
141
+ discounts.map do |d|
142
+ {
143
+ name: "Discount",
144
+ amount: d.amount.to_f,
145
+ quantity: 1,
146
+ type: "discount"
147
+ }
148
+ end
149
+ end
150
+
151
+ def advanced_payment_discount_item
152
+ return [] if order.order_balance == order.total_price
153
+ balance = order.total_price - order.order_balance
154
+
155
+ [
156
+ {
157
+ name: "Other payment tenders",
158
+ amount: (balance * -1).to_f,
159
+ quantity: 1,
160
+ type: "discount"
161
+ }
162
+ ]
163
+ end
164
+
165
+ def zipco_config
166
+ {
167
+ redirect_uri: confirm_url
168
+ }
169
+ end
170
+
171
+ def confirm_url
172
+ Storefront::Engine.routes.url_helpers.complete_zipco_url(host: Workarea.config.host)
173
+ end
174
+
175
+ def gift_wrap_item
176
+ adjustment = order.price_adjustments.detect { |pa| pa.description == 'Gift Wrapping' }
177
+ return unless adjustment.present?
178
+
179
+ {
180
+ name: "Gift Wrapping",
181
+ amount: adjustment.amount.to_f,
182
+ quantity: 1,
183
+ type: "sku"
184
+ }
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,37 @@
1
+ module Workarea
2
+ module Zipco
3
+ class Setup
4
+ attr_reader :checkout, :order
5
+
6
+ # @param ::Workarea::Checkout
7
+ def initialize(checkout)
8
+ @checkout = checkout
9
+ @order = checkout.order
10
+ end
11
+
12
+ def create_order_response
13
+ @create_order_response ||= Zipco.gateway.create_order(order_details)
14
+ end
15
+
16
+ def set_checkout_data
17
+ payment = Workarea::Payment.find(order.id)
18
+
19
+ payment.clear_credit_card
20
+
21
+ payment.set_zipco(token: create_order_response.zipco_order_id)
22
+
23
+ order.update_attributes!(zipco_order_id: create_order_response.zipco_order_id)
24
+ end
25
+
26
+ def redirect_uri
27
+ create_order_response.redirect_uri
28
+ end
29
+
30
+ private
31
+
32
+ def order_details
33
+ Workarea::Zipco::Order.new(order).to_h
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ module Workarea
2
+ decorate Storefront::Checkout::PaymentViewModel, with: :zipco do
3
+ decorated do
4
+ delegate :zipco?, to: :payment
5
+ end
6
+
7
+ def using_new_card?
8
+ super && !zipco?
9
+ end
10
+
11
+ def zipco
12
+ order = Workarea::Order.find(payment.id)
13
+ Storefront::ZipcoViewModel.new(nil, order: order)
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,11 @@
1
+ module Workarea
2
+ decorate Storefront::OrderViewModel, with: :kount do
3
+ def placed_at
4
+ if model.zipco_referred?
5
+ Time.current
6
+ else
7
+ model.placed_at
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module Workarea
2
+ module Storefront
3
+ class ZipcoViewModel < ApplicationViewModel
4
+ def show?
5
+ Zipco.config.allowed_countries.include?(order.billing_address.country.alpha2)
6
+ end
7
+
8
+ private
9
+
10
+ def order
11
+ @order ||= begin
12
+ o = options[:order]
13
+ Workarea::Storefront::OrderViewModel.new(o)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ %li
2
+ = image_tag('https://static.zipmoney.com.au/assets/default/footer-tile/footer-tile-new.png')
3
+ = t('workarea.zipco.tender_description')
4
+
@@ -0,0 +1,10 @@
1
+ - content_for :head do
2
+ = javascript_include_tag 'https://static.zipmoney.com.au/checkout/checkout-v1.min.js'
3
+
4
+ - if @step.zipco.show?
5
+ .checkout-payment__primary-method.checkout-payment__primary-method--zipco{ class: ('checkout-payment__primary-method--selected' if @step.zipco?) }
6
+ .button-property
7
+ = radio_button_tag 'payment', 'zipco', step.zipco?, data: { zipco_payment_method: '' }
8
+ = label_tag 'payment[zipco]', nil, class: 'button-property__name' do
9
+ = image_tag('https://static.zipmoney.com.au/assets/default/footer-tile/footer-tile-new.png', class: "zipco-icon")
10
+ %p.button-property__text= t('workarea.storefront.zipco.on_continue')
@@ -0,0 +1,4 @@
1
+ %p{ style: "margin: 0 0 6px; font: 13px/1.5 arial; color: #{@config.text_color};" }
2
+ = t('workarea.zipco.tender_description')
3
+
4
+
@@ -0,0 +1 @@
1
+ <%= t('workarea.zipco.tender_description') %>
@@ -0,0 +1,2 @@
1
+ - if @order.zipco_referred?
2
+ %p= t('workarea.storefront.orders.referred_message')
@@ -0,0 +1,6 @@
1
+ .data-card
2
+ .data-card__cell
3
+ %p.data-card__line
4
+ = image_tag('https://static.zipmoney.com.au/assets/default/footer-tile/footer-tile-new.png', class: "zipco-icon")
5
+
6
+
@@ -0,0 +1,4 @@
1
+ - if Workarea::Zipco.config.marketing_assets_key.present? && Workarea::Zipco.config.show_tagline
2
+ %script{:src => "https://static.zipmoney.com.au/lib/js/zm-widget-js/dist/zip-widget.min.js", :type => "text/javascript"}
3
+ %div{ data: {env: "#{Workarea::Zipco.config.marketing_assets_env}", "zm-merchant": "#{Workarea::Zipco.config.marketing_assets_key}" }}
4
+ #zip-product-widg{data: {"zm-asset": "productwidget", "zm-popup-asset": "termsdialog", "zm-widget": "popup" }, style: "cursor:pointer"}
@@ -0,0 +1,4 @@
1
+ - if Workarea::Zipco.config.marketing_assets_key.present?
2
+ %script{:src => "https://static.zipmoney.com.au/lib/js/zm-widget-js/dist/zip-widget.min.js", :type => "text/javascript"}
3
+ %div{data: {"env": "#{Workarea::Zipco.config.marketing_assets_env}", "zm-merchant": "#{Workarea::Zipco.config.marketing_assets_key}" }}
4
+ %div{data: {"zm-asset": "landingpage", "zm-widget": "inline"}}
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/workarea/zipco/engine', __dir__)
7
+ APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12
+
13
+ require "rails"
14
+ # Pick the frameworks you want:
15
+ require "active_model/railtie"
16
+ require "active_job/railtie"
17
+ # require "active_record/railtie"
18
+ # require "active_storage/engine"
19
+ require "action_controller/railtie"
20
+ require "action_mailer/railtie"
21
+ require "action_view/railtie"
22
+ # require "action_cable/engine"
23
+ require "sprockets/railtie"
24
+ require "rails/test_unit/railtie"
25
+ require 'rails/engine/commands'
@@ -0,0 +1,24 @@
1
+ Workarea::Plugin.append_partials(
2
+ 'storefront.product_pricing_details',
3
+ 'workarea/storefront/zipco/zipco_tagline'
4
+ )
5
+
6
+ Workarea::Plugin.append_partials(
7
+ 'storefront.cart_checkout_actions',
8
+ 'workarea/storefront/zipco/zipco_tagline'
9
+ )
10
+
11
+ Workarea::Plugin.append_partials(
12
+ 'storefront.payment_method',
13
+ 'workarea/storefront/checkouts/zipco_payment'
14
+ )
15
+
16
+ Workarea::Plugin.append_partials(
17
+ 'storefront.checkout_confirmation_text',
18
+ 'workarea/storefront/orders/zipco_order_message'
19
+ )
20
+
21
+ Workarea::Plugin.append_stylesheets(
22
+ "storefront.components",
23
+ "workarea/storefront/zipco/components/zipco_icon"
24
+ )
@@ -0,0 +1,15 @@
1
+ Workarea.configure do |config|
2
+ config.order_status_calculators.insert(0, 'Workarea::Order::Status::ZipReferred')
3
+ config.tender_types.append(:zipco)
4
+
5
+ config.zipco = ActiveSupport::Configurable::Configuration.new
6
+ config.zipco.api_version = "2017-03-01"
7
+ config.zipco.api_timeout = 15
8
+ config.zipco.open_timeout = 15
9
+
10
+ config.zipco.marketing_assets_key = nil
11
+ config.zipco.marketing_assets_env = Rails.env.production? ? "production" : "sandbox"
12
+
13
+ config.zipco.show_tagline = true # toggles tagline on PDP and cart display
14
+ config.zipco.allowed_countries = ["AU", "NZ"]
15
+ end
@@ -0,0 +1,38 @@
1
+ ---
2
+ en:
3
+ workarea:
4
+ admin:
5
+ orders:
6
+ tenders:
7
+ zip:
8
+ title: Zip Payments
9
+ storefront:
10
+ orders:
11
+ referred_message: Your order is pending approval of your zipMoney application. You will receive an email with instructions shortly on whether your application has been approved or not. If approved you will be able to complete your purchase by following a link in the email.
12
+ tenders:
13
+ zipco: Zip Payments
14
+ carts:
15
+ zip: Zip Payments
16
+ checkouts:
17
+ zip: Zip Payments
18
+ zipco:
19
+ cancelled_message: Your Zip payment has been cancelled. Please select another payment method.
20
+ declined_message: Your Zip payment has been declined.
21
+ check_out: Check Out with Zip
22
+ checkout_submit_text: Continue to Zip
23
+ on_continue: When you click Place Order you will be directed to ZIP to complete payment. When using ZIP all promo codes must be applied before moving to the next step.
24
+ payment_error: There was a problem processing your Zip payment, you have not been charged. Please try again or select
25
+ another payment method.
26
+ zip: Zip
27
+ learn_more: Learn More
28
+ zipco:
29
+ authorize: "%{amount} has been authorized"
30
+ capture: "%{amount} has been captured"
31
+ purchase: "%{amount} has been purchased"
32
+ refund: "%{amount} has been refunded"
33
+ refund: "Zip Payment Transaction has been refunded"
34
+ authorize_failure: "Zip authorize has failed"
35
+ capture_failure: "Zip capture has failed"
36
+ purchase_failure: "Zip purchase has failed"
37
+ refund_failure: "Zip refund has failed"
38
+ tender_description: "Zip Payment"
@@ -0,0 +1,5 @@
1
+ Workarea::Storefront::Engine.routes.draw do
2
+ get 'zipco/start/' => 'zipco#start', as: :start_zipco
3
+ get 'zipco/complete/' => 'zipco#complete', as: :complete_zipco
4
+ get 'zipco_landing' => 'zipco_landing#show', as: :zipco_landing
5
+ end
@@ -0,0 +1,48 @@
1
+ require 'workarea'
2
+ require 'workarea/storefront'
3
+ require 'workarea/admin'
4
+
5
+ require 'workarea/zipco/engine'
6
+ require 'workarea/zipco/version'
7
+
8
+ require 'workarea/zipco/bogus_gateway'
9
+ require 'workarea/zipco/gateway'
10
+ require 'workarea/zipco/response'
11
+
12
+
13
+ require "faraday"
14
+
15
+ module Workarea
16
+ module Zipco
17
+ RETRY_ERROR_STATUSES = 500..599
18
+
19
+ def self.credentials
20
+ (Rails.application.secrets.zipco || {}).deep_symbolize_keys
21
+ end
22
+
23
+ def self.config
24
+ Workarea.config.zipco
25
+ end
26
+
27
+ def self.secret_key
28
+ return unless credentials.present?
29
+ credentials[:secret_key]
30
+ end
31
+
32
+ def self.test?
33
+ config[:test]
34
+ end
35
+
36
+ # Conditionally use the real gateway when secrets are present.
37
+ # Otherwise, use the bogus gateway.
38
+ #
39
+ # @return [Zipco::Gateway]
40
+ def self.gateway(options = {})
41
+ if credentials.present?
42
+ Zipco::Gateway.new(secret_key: secret_key, api_version: config.api_version)
43
+ else
44
+ Zipco::BogusGateway.new
45
+ end
46
+ end
47
+ end
48
+ end