shopify_dashboard_plus 0.0.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +16 -0
- data/.travis.yml +6 -0
- data/Gemfile.lock +99 -35
- data/README.md +16 -7
- data/Rakefile +13 -0
- data/bin/shopify_dashboard_plus.rb +1 -1
- data/config.ru +4 -0
- data/lib/shopify_dashboard_plus.rb +40 -52
- data/lib/shopify_dashboard_plus/currency.rb +31 -0
- data/lib/shopify_dashboard_plus/discount_report.rb +36 -0
- data/lib/shopify_dashboard_plus/helpers.rb +65 -51
- data/lib/shopify_dashboard_plus/report.rb +4 -196
- data/lib/shopify_dashboard_plus/revenue_report.rb +55 -0
- data/lib/shopify_dashboard_plus/sales_report.rb +86 -0
- data/lib/shopify_dashboard_plus/traffic_report.rb +65 -0
- data/lib/shopify_dashboard_plus/version.rb +3 -1
- data/shopify_dashboard_plus.gemspec +17 -7
- data/test/fixtures/vcr_cassettes/.gitkeep +0 -0
- data/test/fixtures/vcr_cassettes/authenticate.yml +88 -0
- data/test/fixtures/vcr_cassettes/multiple_pages_orders.yml +1544 -0
- data/test/fixtures/vcr_cassettes/orders_from_2010_01_01.yml +815 -0
- data/test/fixtures/vcr_cassettes/orders_from_2010_01_01_to_2015_01_01.yml +566 -0
- data/test/fixtures/vcr_cassettes/orders_no_paramaters.yml +81 -0
- data/test/fixtures/vcr_cassettes/orders_none.yml +77 -0
- data/test/fixtures/vcr_cassettes/orders_to_2015-06-26.yml +81 -0
- data/test/resources/anonymizer.rb +125 -0
- data/test/resources/modify_data.rb +110 -0
- data/test/strip_sensitive_data.rb +79 -0
- data/test/test_app.rb +60 -0
- data/test/test_frontend.rb +76 -0
- data/test/test_mockdata.rb +237 -0
- data/views/connect.erb +6 -6
- data/views/layout.erb +2 -3
- data/views/report.erb +1 -1
- metadata +190 -59
@@ -1,37 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'currency'
|
4
|
+
|
1
5
|
module ApplicationHelpers
|
2
6
|
|
3
7
|
include Rack::Utils
|
8
|
+
using Currency
|
9
|
+
|
4
10
|
alias_method :h, :escape_html
|
5
11
|
|
6
|
-
DESIRED_FIELDS = [
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
]
|
12
|
+
DESIRED_FIELDS = %w[
|
13
|
+
total_price
|
14
|
+
created_at
|
15
|
+
billing_address
|
16
|
+
currency
|
17
|
+
line_items
|
18
|
+
customer
|
19
|
+
referring_site
|
20
|
+
discount_codes
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
## Authentication Helpers
|
24
|
+
|
25
|
+
def authenticated?
|
26
|
+
session[:logged_in]
|
27
|
+
end
|
28
|
+
|
16
29
|
|
17
30
|
## Connection & Setup Helpers
|
18
|
-
|
31
|
+
|
19
32
|
def set_connection(key, pwd, name)
|
20
|
-
|
21
|
-
ShopifyAPI::Base.site = shop_url
|
33
|
+
ShopifyAPI::Base.site = "https://#{key}:#{pwd}@#{name}.myshopify.com/admin"
|
22
34
|
shop = ShopifyAPI::Shop.current
|
35
|
+
|
23
36
|
$shop_name = name
|
37
|
+
$currency = shop.money_with_currency_format
|
38
|
+
|
39
|
+
session[:logged_in] = true
|
24
40
|
open_connection
|
25
|
-
rescue
|
41
|
+
rescue SocketError, ActiveResource::ResourceNotFound => e
|
42
|
+
puts "Exception: #{e}"
|
26
43
|
close_connection
|
27
44
|
end
|
28
45
|
|
29
46
|
def close_connection
|
30
47
|
$connected = false
|
48
|
+
session[:logged_in] = false
|
31
49
|
end
|
32
50
|
|
33
51
|
def open_connection
|
34
52
|
$connected = true
|
53
|
+
session[:logged_in] = true
|
35
54
|
end
|
36
55
|
|
37
56
|
def connected?
|
@@ -71,11 +90,11 @@ module ApplicationHelpers
|
|
71
90
|
## Metrics Helpers
|
72
91
|
|
73
92
|
def max_hash_key_exclude_value(unsorted_hash, exclude_value)
|
74
|
-
unsorted_hash.sort_by{ |
|
93
|
+
unsorted_hash.sort_by { |_, v| v }.map { |k, v| [k, v] unless k.downcase == exclude_value }.compact.last
|
75
94
|
end
|
76
95
|
|
77
96
|
def display_as_currency(value)
|
78
|
-
|
97
|
+
$currency.gsub("{{amount}}", value.to_s)
|
79
98
|
rescue
|
80
99
|
'N/A'
|
81
100
|
end
|
@@ -92,41 +111,39 @@ module ApplicationHelpers
|
|
92
111
|
end
|
93
112
|
|
94
113
|
def get_average_revenue(total_revenue, duration)
|
95
|
-
(total_revenue/duration).round(2)
|
114
|
+
(total_revenue / duration).round(2)
|
96
115
|
rescue
|
97
116
|
'N/A'
|
98
117
|
end
|
99
|
-
|
118
|
+
|
100
119
|
def get_daily_revenues(start_date, end_date, orders)
|
101
120
|
# Create hash entry for every day within interval over which to inspect sales
|
102
121
|
revenue_per_day = {}
|
103
122
|
days = get_date_range(start_date, end_date)
|
104
|
-
(0..days).each{ |day| revenue_per_day[(DateTime.parse(end_date) - day).strftime("%Y-%m-%d")] = 0 }
|
123
|
+
(0..days).each { |day| revenue_per_day[(DateTime.parse(end_date) - day).strftime("%Y-%m-%d")] = 0 }
|
105
124
|
|
106
125
|
# Retreive array of ActiveRecord::Collections, each containing orders between the start and end date
|
107
|
-
order_details = orders.map{ |order| [order.created_at, order.total_price.to_f] }
|
108
|
-
|
126
|
+
order_details = orders.map { |order| [order.created_at, order.total_price.to_f] }
|
127
|
+
|
109
128
|
# Filter order details into daily totals and return
|
110
129
|
order_details.each do |(date, total)|
|
111
130
|
day_index = DateTime.parse(date).strftime('%Y-%m-%d')
|
112
131
|
revenue_per_day[day_index] = revenue_per_day[day_index].plus(total)
|
113
132
|
end
|
133
|
+
|
114
134
|
revenue_per_day
|
115
135
|
end
|
116
136
|
|
117
137
|
def hash_to_graph_format(sales, merge_results: false)
|
118
|
-
|
119
138
|
# ChartKick requires a strange format to build graphs. For instance, an array of
|
120
139
|
# {:name => <item_name>, :data => [[<customer_id>, <item_price>], [<customer_id>, <item_price>]]}
|
121
140
|
# places <customer_id> on the independent (x) axis, and stacks each item (item_name) on the y-axis by price (item_price)
|
122
141
|
|
123
|
-
name_hash = sales.map{ |sale| {:name => sale[:name], :data => []} }.uniq
|
124
|
-
|
142
|
+
name_hash = sales.map { |sale| { :name => sale[:name], :data => [] } }.uniq
|
143
|
+
|
125
144
|
sales.map do |old_hash|
|
126
145
|
name_hash.map do |new_hash|
|
127
|
-
if old_hash[:name] == new_hash[:name]
|
128
|
-
new_hash[:data].push(old_hash[:data])
|
129
|
-
end
|
146
|
+
new_hash[:data].push(old_hash[:data]) if old_hash[:name] == new_hash[:name]
|
130
147
|
end
|
131
148
|
end
|
132
149
|
|
@@ -146,38 +163,34 @@ module ApplicationHelpers
|
|
146
163
|
name_hash
|
147
164
|
end
|
148
165
|
|
149
|
-
# Return order query parameters hash
|
150
166
|
def order_parameters_paginate(start_date, end_date, page)
|
151
|
-
{
|
152
|
-
:created_at_min => start_date + " 0:00",
|
153
|
-
:created_at_max => end_date + " 23:59:59",
|
154
|
-
:limit
|
155
|
-
:page
|
156
|
-
:fields => DESIRED_FIELDS
|
167
|
+
{
|
168
|
+
:created_at_min => start_date + " 0:00",
|
169
|
+
:created_at_max => end_date + " 23:59:59",
|
170
|
+
:limit => 250,
|
171
|
+
:page => page,
|
172
|
+
:fields => DESIRED_FIELDS
|
157
173
|
}
|
158
174
|
end
|
159
175
|
|
160
|
-
|
161
|
-
# Return array of ActiveRecord::Collections, each containing up to :limit (250) orders
|
162
|
-
# Continue to query next page until less than :limit orders are returned, indicating no next pages with orders matching query
|
163
176
|
def get_list_of_orders(start_date, end_date)
|
177
|
+
# Return array of ActiveRecord::Collections, each containing up to :limit (250) orders
|
178
|
+
# Continue to query next page until less than :limit orders are returned, indicating no next pages with orders matching query
|
164
179
|
|
165
180
|
# Get first 250 results matching query
|
166
181
|
params = order_parameters_paginate(start_date, end_date, 1)
|
167
182
|
revenue_metrics = [ShopifyAPI::Order.find(:all, :params => params)]
|
168
|
-
|
183
|
+
|
169
184
|
# If the amount of results equal to the limit (250) were returned, pass the query on to the next page (orders 251 to 500)
|
170
185
|
while revenue_metrics.last.length == 250
|
171
186
|
params = order_parameters_paginate(start_date, end_date, revenue_metrics.length + 1)
|
172
187
|
revenue_metrics << ShopifyAPI::Order.find(:all, :params => params)
|
173
188
|
end
|
174
189
|
|
175
|
-
revenue_metrics.flat_map{ |orders| orders.map{ |order| order }}
|
190
|
+
revenue_metrics.flat_map { |orders| orders.map { |order| order } }
|
176
191
|
end
|
177
|
-
|
178
192
|
|
179
193
|
def get_detailed_revenue_metrics(start_date, end_date = DateTime.now)
|
180
|
-
|
181
194
|
order_list = get_list_of_orders(start_date, end_date)
|
182
195
|
|
183
196
|
# Revenue
|
@@ -185,22 +198,23 @@ module ApplicationHelpers
|
|
185
198
|
duration = get_date_range(start_date, end_date) + 1
|
186
199
|
avg_revenue = get_average_revenue(total_revenue, duration)
|
187
200
|
daily_revenue = get_daily_revenues(start_date, end_date, order_list)
|
188
|
-
max_daily_revenue = daily_revenue.max_by{ |
|
189
|
-
|
201
|
+
max_daily_revenue = daily_revenue.max_by { |_, v| v }[1]
|
202
|
+
|
190
203
|
# Retrieve Metrics
|
191
|
-
sales_report
|
192
|
-
revenue_report
|
193
|
-
traffic_report
|
204
|
+
sales_report = ShopifyDashboardPlus::SalesReport.new(order_list).to_h
|
205
|
+
revenue_report = ShopifyDashboardPlus::RevenueReport.new(order_list).to_h
|
206
|
+
traffic_report = ShopifyDashboardPlus::TrafficReport.new(order_list).to_h
|
194
207
|
discounts_report = ShopifyDashboardPlus::DiscountReport.new(order_list).to_h
|
195
|
-
metrics = {
|
196
|
-
:total_revenue
|
197
|
-
:average_revenue
|
198
|
-
:daily_revenue
|
208
|
+
metrics = {
|
209
|
+
:total_revenue => total_revenue,
|
210
|
+
:average_revenue => avg_revenue,
|
211
|
+
:daily_revenue => daily_revenue,
|
199
212
|
:max_daily_revenue => max_daily_revenue,
|
200
|
-
:duration
|
213
|
+
:duration => duration
|
201
214
|
}
|
202
215
|
|
203
216
|
[sales_report, revenue_report, traffic_report, discounts_report, metrics].inject(&:merge)
|
204
217
|
end
|
205
218
|
|
206
219
|
end
|
220
|
+
|
@@ -1,206 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'helpers'
|
2
4
|
|
3
5
|
module ShopifyDashboardPlus
|
4
|
-
|
5
6
|
class Report
|
6
7
|
include ApplicationHelpers
|
7
8
|
|
8
9
|
def initialize(orders)
|
9
|
-
@orders
|
10
|
-
@line_items = orders.flat_map{ |order| order.line_items.map { |line_item| line_item }}
|
10
|
+
@orders = orders
|
11
|
+
@line_items = orders.flat_map { |order| order.line_items.map { |line_item| line_item } }
|
11
12
|
end
|
12
13
|
end
|
13
|
-
|
14
|
-
|
15
|
-
class SalesReport < Report
|
16
|
-
def sales_per_country
|
17
|
-
sales_per_country = Hash.new(0)
|
18
|
-
|
19
|
-
@orders.each do |order|
|
20
|
-
sales_per_country[order.billing_address.country] += 1 if order.attributes['billing_address']
|
21
|
-
end
|
22
|
-
sales_per_country
|
23
|
-
end
|
24
|
-
|
25
|
-
def sales_per_product
|
26
|
-
sales_per_product = Hash.new(0)
|
27
|
-
|
28
|
-
@line_items.each do |item|
|
29
|
-
sales_per_product[item.title] += 1
|
30
|
-
end
|
31
|
-
|
32
|
-
sales_per_product
|
33
|
-
end
|
34
|
-
|
35
|
-
def sales_per_customer
|
36
|
-
customer_sales = []
|
37
|
-
@orders.each do |order|
|
38
|
-
order.line_items.each do |item|
|
39
|
-
customer_name = "#{order.customer.first_name} #{order.customer.last_name} (#{order.customer.email})"
|
40
|
-
customer_sales.push({ :name => item.title,
|
41
|
-
:data => [customer_name, item.price.to_f]})
|
42
|
-
end
|
43
|
-
end
|
44
|
-
hash_to_graph_format(customer_sales)
|
45
|
-
end
|
46
|
-
|
47
|
-
def sales_per_price_point
|
48
|
-
sales_per_price_point = Hash.new(0)
|
49
|
-
|
50
|
-
@line_items.each do |item|
|
51
|
-
sales_per_price_point[item.price] += 1
|
52
|
-
end
|
53
|
-
sales_per_price_point.sort_by{ |x,y| x.to_f }.to_h rescue {}
|
54
|
-
end
|
55
|
-
|
56
|
-
def number_of_sales
|
57
|
-
@orders.length
|
58
|
-
end
|
59
|
-
|
60
|
-
def currencies_per_sale
|
61
|
-
currencies = Hash.new(0)
|
62
|
-
|
63
|
-
@orders.each do |order|
|
64
|
-
currencies[order.currency] += 1 if order.attributes['currency']
|
65
|
-
end
|
66
|
-
currencies
|
67
|
-
end
|
68
|
-
|
69
|
-
def to_h
|
70
|
-
{
|
71
|
-
:currencies_per_sale => currencies_per_sale,
|
72
|
-
:most_used_currency => currencies_per_sale.sort_by{ |k, v| v }.last,
|
73
|
-
:sales_per_country => sales_per_country,
|
74
|
-
:most_sales_per_country => sales_per_country.sort_by{ |k, v| v }.last,
|
75
|
-
:sales_per_price => sales_per_price_point,
|
76
|
-
:top_selling_price_point => sales_per_price_point.sort_by{ |k, v| v }.last,
|
77
|
-
:sales_per_product => sales_per_product,
|
78
|
-
:top_selling_product => sales_per_product.sort_by{ |k, v| v }.last,
|
79
|
-
:sales_per_customer => sales_per_customer,
|
80
|
-
:number_of_sales => number_of_sales
|
81
|
-
}
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
|
86
|
-
class RevenueReport < Report
|
87
|
-
def revenue_per_country
|
88
|
-
revenue_per_country = []
|
89
|
-
@orders.each do |order|
|
90
|
-
order.line_items.each do |item|
|
91
|
-
revenue_per_country.push({:name => item.title,
|
92
|
-
:data => [order.billing_address.country, item.price.to_f]})
|
93
|
-
end
|
94
|
-
end
|
95
|
-
hash_to_graph_format(revenue_per_country, merge_results: true)
|
96
|
-
end
|
97
|
-
|
98
|
-
def revenue_per_price_point
|
99
|
-
revenue_per_price_point = Hash.new(0)
|
100
|
-
@line_items.each do |item|
|
101
|
-
revenue_per_price_point[item.price] = revenue_per_price_point[item.price].plus(item.price)
|
102
|
-
end
|
103
|
-
revenue_per_price_point.sort_by{ |x,y| x.to_f }.to_h rescue {}
|
104
|
-
end
|
105
|
-
|
106
|
-
def revenue_per_product
|
107
|
-
revenue_per_product = Hash.new(0.0)
|
108
|
-
@line_items.each do |item|
|
109
|
-
revenue_per_product[item.title] = revenue_per_product[item.title].plus(item.price)
|
110
|
-
end
|
111
|
-
revenue_per_product
|
112
|
-
end
|
113
|
-
|
114
|
-
def to_h
|
115
|
-
{
|
116
|
-
:revenue_per_country => revenue_per_country,
|
117
|
-
:revenue_per_product => revenue_per_product,
|
118
|
-
:top_grossing_product => revenue_per_product.sort_by{ |k, v| v }.last,
|
119
|
-
:revenue_per_price_point => revenue_per_price_point,
|
120
|
-
:top_grossing_price_point => revenue_per_price_point.sort_by{ |k, v| v}.last
|
121
|
-
}
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
|
126
|
-
class DiscountReport < Report
|
127
|
-
|
128
|
-
def discount_usage
|
129
|
-
discount_value, discount_used = Hash.new(0.0), Hash.new(0)
|
130
|
-
|
131
|
-
@orders.each do |order|
|
132
|
-
if order.attributes['discount_codes']
|
133
|
-
order.discount_codes.each do |discount_code|
|
134
|
-
discount_value[discount_code.code] = discount_value[discount_code.code].plus(discount_code.amount)
|
135
|
-
discount_used[discount_code.code] += 1
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
{
|
140
|
-
:discount_savings => discount_value,
|
141
|
-
:top_discount_savings => discount_value.sort_by{ |k, v| v }.last,
|
142
|
-
:discount_quantity => discount_used,
|
143
|
-
:most_used_discount_code => discount_used.sort_by{ |k, v| v }.last
|
144
|
-
}
|
145
|
-
end
|
146
|
-
|
147
|
-
def to_h
|
148
|
-
discount_usage
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
|
153
|
-
class TrafficReport < Report
|
154
|
-
|
155
|
-
def number_of_referrals
|
156
|
-
referring_sites, referring_pages = Hash.new(0), Hash.new(0)
|
157
|
-
|
158
|
-
@orders.each do |order|
|
159
|
-
if order.attributes['referring_site'].empty?
|
160
|
-
referring_pages['None'] += 1
|
161
|
-
referring_sites['None'] += 1
|
162
|
-
else
|
163
|
-
host = get_host(order.referring_site)
|
164
|
-
page = strip_protocol(order.referring_site)
|
165
|
-
referring_pages[page] += 1
|
166
|
-
referring_sites[host] += 1
|
167
|
-
end
|
168
|
-
end
|
169
|
-
{
|
170
|
-
:referral_sites => referring_sites.sort().to_h,
|
171
|
-
:top_referral_site => max_hash_key_exclude_value(referring_sites, 'none'),
|
172
|
-
:referral_pages => referring_pages.sort().to_h,
|
173
|
-
:top_referral_page => max_hash_key_exclude_value(referring_pages, 'none')
|
174
|
-
}
|
175
|
-
end
|
176
|
-
|
177
|
-
def traffic_revenue
|
178
|
-
revenue_per_referral_page, revenue_per_referral_site = Hash.new(0.0), Hash.new(0.0)
|
179
|
-
|
180
|
-
@orders.each do |order|
|
181
|
-
order.line_items.each do |item|
|
182
|
-
if order.attributes['referring_site'].empty?
|
183
|
-
revenue_per_referral_page['None'] = revenue_per_referral_page['None'].plus(item.price)
|
184
|
-
revenue_per_referral_site['None'] = revenue_per_referral_site['None'].plus(item.price)
|
185
|
-
else
|
186
|
-
host = get_host(order.referring_site)
|
187
|
-
page = strip_protocol(order.referring_site)
|
188
|
-
revenue_per_referral_site[host] = revenue_per_referral_site[host].plus(item.price)
|
189
|
-
revenue_per_referral_page[page] = revenue_per_referral_page[page].plus(item.price)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
{
|
194
|
-
:revenue_per_referral_site => revenue_per_referral_site.sort().to_h,
|
195
|
-
:top_referral_site_revenue => max_hash_key_exclude_value(revenue_per_referral_site, 'none'),
|
196
|
-
:revenue_per_referral_page => revenue_per_referral_page.sort().to_h,
|
197
|
-
:top_referral_page_revenue => max_hash_key_exclude_value(revenue_per_referral_page, 'none')
|
198
|
-
}
|
199
|
-
end
|
200
|
-
|
201
|
-
def to_h
|
202
|
-
traffic_revenue.merge(number_of_referrals)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
14
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'currency'
|
4
|
+
require_relative 'helpers'
|
5
|
+
require_relative 'report'
|
6
|
+
|
7
|
+
module ShopifyDashboardPlus
|
8
|
+
class RevenueReport < Report
|
9
|
+
using Currency
|
10
|
+
|
11
|
+
def revenue_per_country
|
12
|
+
revenue_per_country = []
|
13
|
+
@orders.each do |order|
|
14
|
+
order.line_items.each do |item|
|
15
|
+
revenue_per_country.push(
|
16
|
+
:name => item.title,
|
17
|
+
:data => [order.billing_address.country, item.price.to_f]
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
hash_to_graph_format(revenue_per_country, merge_results: true)
|
23
|
+
end
|
24
|
+
|
25
|
+
def revenue_per_price_point
|
26
|
+
revenue_per_price_point = Hash.new(0)
|
27
|
+
@line_items.each do |item|
|
28
|
+
revenue_per_price_point[item.price] = revenue_per_price_point[item.price].plus(item.price)
|
29
|
+
end
|
30
|
+
|
31
|
+
revenue_per_price_point.sort_by { |x, _| x.to_f }.to_h
|
32
|
+
rescue
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
|
36
|
+
def revenue_per_product
|
37
|
+
revenue_per_product = Hash.new(0.0)
|
38
|
+
@line_items.each do |item|
|
39
|
+
revenue_per_product[item.title] = revenue_per_product[item.title].plus(item.price)
|
40
|
+
end
|
41
|
+
|
42
|
+
revenue_per_product
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_h
|
46
|
+
{
|
47
|
+
:revenue_per_country => revenue_per_country,
|
48
|
+
:revenue_per_product => revenue_per_product,
|
49
|
+
:top_grossing_product => revenue_per_product.sort_by { |_, v| v }.last,
|
50
|
+
:revenue_per_price_point => revenue_per_price_point,
|
51
|
+
:top_grossing_price_point => revenue_per_price_point.sort_by { |_, v| v }.last
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|