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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +16 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile.lock +99 -35
  5. data/README.md +16 -7
  6. data/Rakefile +13 -0
  7. data/bin/shopify_dashboard_plus.rb +1 -1
  8. data/config.ru +4 -0
  9. data/lib/shopify_dashboard_plus.rb +40 -52
  10. data/lib/shopify_dashboard_plus/currency.rb +31 -0
  11. data/lib/shopify_dashboard_plus/discount_report.rb +36 -0
  12. data/lib/shopify_dashboard_plus/helpers.rb +65 -51
  13. data/lib/shopify_dashboard_plus/report.rb +4 -196
  14. data/lib/shopify_dashboard_plus/revenue_report.rb +55 -0
  15. data/lib/shopify_dashboard_plus/sales_report.rb +86 -0
  16. data/lib/shopify_dashboard_plus/traffic_report.rb +65 -0
  17. data/lib/shopify_dashboard_plus/version.rb +3 -1
  18. data/shopify_dashboard_plus.gemspec +17 -7
  19. data/test/fixtures/vcr_cassettes/.gitkeep +0 -0
  20. data/test/fixtures/vcr_cassettes/authenticate.yml +88 -0
  21. data/test/fixtures/vcr_cassettes/multiple_pages_orders.yml +1544 -0
  22. data/test/fixtures/vcr_cassettes/orders_from_2010_01_01.yml +815 -0
  23. data/test/fixtures/vcr_cassettes/orders_from_2010_01_01_to_2015_01_01.yml +566 -0
  24. data/test/fixtures/vcr_cassettes/orders_no_paramaters.yml +81 -0
  25. data/test/fixtures/vcr_cassettes/orders_none.yml +77 -0
  26. data/test/fixtures/vcr_cassettes/orders_to_2015-06-26.yml +81 -0
  27. data/test/resources/anonymizer.rb +125 -0
  28. data/test/resources/modify_data.rb +110 -0
  29. data/test/strip_sensitive_data.rb +79 -0
  30. data/test/test_app.rb +60 -0
  31. data/test/test_frontend.rb +76 -0
  32. data/test/test_mockdata.rb +237 -0
  33. data/views/connect.erb +6 -6
  34. data/views/layout.erb +2 -3
  35. data/views/report.erb +1 -1
  36. metadata +190 -59
@@ -0,0 +1,81 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://<API_KEY>:<API_PWD>@<SHOP_NAME>.myshopify.com/admin/orders.json?created_at_max=<%= today %>%2023:59:59&created_at_min==<%= today %>%200:00&fields%5B%5D=billing_address&fields%5B%5D=created_at&fields%5B%5D=currency&fields%5B%5D=customer&fields%5B%5D=discount_codes&fields%5B%5D=line_items&fields%5B%5D=referring_site&fields%5B%5D=total_price&limit=250&page=1
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept:
11
+ - application/json
12
+ User-Agent:
13
+ - ShopifyAPI/4.0.4 ActiveResource/4.0.0 Ruby/2.1.2
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ response:
17
+ status:
18
+ code: 200
19
+ message: OK
20
+ headers:
21
+ Server:
22
+ - nginx
23
+ Date:
24
+ - Sat, 27 Jun 2015 03:02:01 GMT
25
+ Content-Type:
26
+ - application/json; charset=utf-8
27
+ Transfer-Encoding:
28
+ - chunked
29
+ Connection:
30
+ - keep-alive
31
+ X-Sorting-Hat-Podid:
32
+ - '3'
33
+ X-Sorting-Hat-Shopid:
34
+ - '4500785'
35
+ X-Sorting-Hat-Podid-Cached:
36
+ - '1'
37
+ X-Sorting-Hat-Shopid-Cached:
38
+ - '1'
39
+ Vary:
40
+ - Accept-Encoding
41
+ Status:
42
+ - 200 OK
43
+ X-Xss-Protection:
44
+ - 1; mode=block; report=/xss-report/0a457a06-1849-408f-bc0f-57f4edc59f89?source%5Baction%5D=index&source%5Bcontroller%5D=admin%2Forders&source%5Bsection%5D=admin
45
+ X-Content-Type-Options:
46
+ - nosniff
47
+ - nosniff
48
+ X-Frame-Options:
49
+ - DENY
50
+ X-Shopid:
51
+ - '4500785'
52
+ X-Shardid:
53
+ - '3'
54
+ X-Shopify-Shop-Api-Call-Limit:
55
+ - 1/40
56
+ Http-X-Shopify-Shop-Api-Call-Limit:
57
+ - 1/40
58
+ X-Stats-Userid:
59
+ - '0'
60
+ X-Stats-Apiclientid:
61
+ - '860030'
62
+ X-Stats-Apipermissionid:
63
+ - '11578246'
64
+ Set-Cookie:
65
+ - request_method=GET; path=/
66
+ X-Request-Id:
67
+ - 0a457a06-1849-408f-bc0f-57f4edc59f89
68
+ P3p:
69
+ - CP="NOI DSP COR NID ADMa OPTa OUR NOR"
70
+ X-Dc:
71
+ - ash
72
+ body:
73
+ encoding: UTF-8
74
+ string: '{"orders":[{"created_at":"<%= today %>T20:06:43-04:00","currency":"CAD","referring_site":"http://thielhodkiewicz.ca/caandra","total_price":"281.37","discount_codes":[],"line_items":[{"product_id":"08314615","price":76.97,"title":"Ergonomic
75
+ Wooden Gloves","variant_id":"4564472289","vendor":"Jewelery","name":"Sleek
76
+ Concrete Car"}],"billing_address":{"address1":"9875 Bogan Grove","address2":"Suite
77
+ 328","city":"Modestoport","country":"Canada","company":"Leannon-Veum","first_name":"Conner","last_name":"Grimes","latitude":"18.212338163213815","longitude":"70.88552439133932","phone":"300-733-7915
78
+ x4220","zip":"A5Q1V5","name":"ConnerGrimes"},"customer":{"id":"41185198","email":"frances@lubowitz.com","first_name":"Ephraim","last_name":"Baumbach"}}]}'
79
+ http_version:
80
+ recorded_at: Sat, 27 Jun 2015 03:03:11 GMT
81
+ recorded_with: VCR 2.9.3
@@ -0,0 +1,77 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://<API_KEY>:<API_PWD>@<SHOP_NAME>.myshopify.com/admin/orders.json?created_at_max=2015-06-27%2023:59:59&created_at_min=2015-06-27%200:00&fields%5B%5D=billing_address&fields%5B%5D=created_at&fields%5B%5D=currency&fields%5B%5D=customer&fields%5B%5D=discount_codes&fields%5B%5D=line_items&fields%5B%5D=referring_site&fields%5B%5D=total_price&limit=250&page=1
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept:
11
+ - application/json
12
+ User-Agent:
13
+ - ShopifyAPI/4.0.4 ActiveResource/4.0.0 Ruby/2.1.2
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ response:
17
+ status:
18
+ code: 200
19
+ message: OK
20
+ headers:
21
+ Server:
22
+ - nginx
23
+ Date:
24
+ - Sat, 27 Jun 2015 05:19:23 GMT
25
+ Content-Type:
26
+ - application/json; charset=utf-8
27
+ Transfer-Encoding:
28
+ - chunked
29
+ Connection:
30
+ - keep-alive
31
+ X-Sorting-Hat-Podid:
32
+ - '3'
33
+ X-Sorting-Hat-Shopid:
34
+ - '4500785'
35
+ X-Sorting-Hat-Podid-Cached:
36
+ - '0'
37
+ X-Sorting-Hat-Shopid-Cached:
38
+ - '0'
39
+ Vary:
40
+ - Accept-Encoding
41
+ Status:
42
+ - 200 OK
43
+ X-Xss-Protection:
44
+ - 1; mode=block; report=/xss-report/89d0344f-7536-411e-96ea-965119e394ff?source%5Baction%5D=index&source%5Bcontroller%5D=admin%2Forders&source%5Bsection%5D=admin
45
+ X-Content-Type-Options:
46
+ - nosniff
47
+ - nosniff
48
+ X-Frame-Options:
49
+ - DENY
50
+ X-Shopid:
51
+ - '4500785'
52
+ X-Shardid:
53
+ - '3'
54
+ X-Shopify-Shop-Api-Call-Limit:
55
+ - 1/40
56
+ Http-X-Shopify-Shop-Api-Call-Limit:
57
+ - 1/40
58
+ X-Stats-Userid:
59
+ - '0'
60
+ X-Stats-Apiclientid:
61
+ - '860030'
62
+ X-Stats-Apipermissionid:
63
+ - '11578246'
64
+ Set-Cookie:
65
+ - request_method=GET; path=/
66
+ X-Request-Id:
67
+ - 89d0344f-7536-411e-96ea-965119e394ff
68
+ P3p:
69
+ - CP="NOI DSP COR NID ADMa OPTa OUR NOR"
70
+ X-Dc:
71
+ - ash
72
+ body:
73
+ encoding: UTF-8
74
+ string: '{"orders":[]}'
75
+ http_version:
76
+ recorded_at: Sat, 27 Jun 2015 05:20:33 GMT
77
+ recorded_with: VCR 2.9.3
@@ -0,0 +1,81 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://<API_KEY>:<API_PWD>@<SHOP_NAME>.myshopify.com/admin/orders.json?created_at_max=2015-06-26%2023:59:59&created_at_min=2015-06-26%200:00&fields%5B%5D=billing_address&fields%5B%5D=created_at&fields%5B%5D=currency&fields%5B%5D=customer&fields%5B%5D=discount_codes&fields%5B%5D=line_items&fields%5B%5D=referring_site&fields%5B%5D=total_price&limit=250&page=1
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept:
11
+ - application/json
12
+ User-Agent:
13
+ - ShopifyAPI/4.0.4 ActiveResource/4.0.0 Ruby/2.1.2
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ response:
17
+ status:
18
+ code: 200
19
+ message: OK
20
+ headers:
21
+ Server:
22
+ - nginx
23
+ Date:
24
+ - Sat, 27 Jun 2015 03:02:01 GMT
25
+ Content-Type:
26
+ - application/json; charset=utf-8
27
+ Transfer-Encoding:
28
+ - chunked
29
+ Connection:
30
+ - keep-alive
31
+ X-Sorting-Hat-Podid:
32
+ - '3'
33
+ X-Sorting-Hat-Shopid:
34
+ - '4500785'
35
+ X-Sorting-Hat-Podid-Cached:
36
+ - '1'
37
+ X-Sorting-Hat-Shopid-Cached:
38
+ - '1'
39
+ Vary:
40
+ - Accept-Encoding
41
+ Status:
42
+ - 200 OK
43
+ X-Xss-Protection:
44
+ - 1; mode=block; report=/xss-report/db2dd2d8-eb26-4650-8bc4-588a41b866ef?source%5Baction%5D=index&source%5Bcontroller%5D=admin%2Forders&source%5Bsection%5D=admin
45
+ X-Content-Type-Options:
46
+ - nosniff
47
+ - nosniff
48
+ X-Frame-Options:
49
+ - DENY
50
+ X-Shopid:
51
+ - '4500785'
52
+ X-Shardid:
53
+ - '3'
54
+ X-Shopify-Shop-Api-Call-Limit:
55
+ - 1/40
56
+ Http-X-Shopify-Shop-Api-Call-Limit:
57
+ - 1/40
58
+ X-Stats-Userid:
59
+ - '0'
60
+ X-Stats-Apiclientid:
61
+ - '860030'
62
+ X-Stats-Apipermissionid:
63
+ - '11578246'
64
+ Set-Cookie:
65
+ - request_method=GET; path=/
66
+ X-Request-Id:
67
+ - db2dd2d8-eb26-4650-8bc4-588a41b866ef
68
+ P3p:
69
+ - CP="NOI DSP COR NID ADMa OPTa OUR NOR"
70
+ X-Dc:
71
+ - ash
72
+ body:
73
+ encoding: UTF-8
74
+ string: '{"orders":[{"created_at":"2015-06-26T20:06:43-04:00","currency":"CAD","referring_site":"http://bartell.com/alvis","total_price":"281.37","discount_codes":[],"line_items":[{"product_id":"63864731","price":"95.11","title":"Gorgeous
75
+ Rubber Pants","variant_id":"7654546673","vendor":"Automotive","name":"Incredible
76
+ Cotton Gloves"}],"billing_address":{"address1":"72886 Emmanuelle Manors","address2":"Suite
77
+ 813","city":"Connhaven","country":"Canada","company":"Collins LLC","first_name":"Chelsea","last_name":"Thiel","latitude":"69.41862661206804","longitude":"82.43290958920517","phone":"1-244-410-9707
78
+ x716","zip":"M1I9E7","name":"ChelseaThiel"},"customer":{"id":"29327241","email":"emmalee_goodwin@cronagottlieb.com","first_name":"Sarina","last_name":"Abshire"}}]}'
79
+ http_version:
80
+ recorded_at: Sat, 27 Jun 2015 03:03:11 GMT
81
+ recorded_with: VCR 2.9.3
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anonymizer
4
+
5
+ # Strip discount information and replace with fake data
6
+ def anonymize_discounts(orders)
7
+ discount_code_replacement = {}
8
+
9
+ orders.each_with_index do |order, index|
10
+ order['discount_codes'].each_with_index do |dc, inner_index|
11
+ next if dc.nil? || dc.empty?
12
+
13
+ old_code = order['discount_codes'][inner_index]['code']
14
+
15
+ unless discount_code_replacement[old_code]
16
+ discount_code_replacement[old_code] = Faker::Internet.slug
17
+ end
18
+
19
+ orders[index]['discount_codes'][inner_index]['code'] = discount_code_replacement[old_code]
20
+ end
21
+ end
22
+
23
+ orders
24
+ end
25
+
26
+ # Strip referral information and replace with fake data
27
+ def anonymize_referrals(orders)
28
+ referring_site_replacement = {}
29
+
30
+ orders.each_with_index do |order, index|
31
+ next if order['referring_site'].nil? || order['referring_site'].empty?
32
+
33
+ old_site = order['referring_site']
34
+
35
+ unless referring_site_replacement[old_site]
36
+ referring_site_replacement[old_site] = Faker::Internet.url
37
+ end
38
+
39
+ orders[index]['referring_site'] = referring_site_replacement[old_site]
40
+ end
41
+
42
+ orders
43
+ end
44
+
45
+ # Strip billing address informatino and replace with fake data
46
+ def anonymize_billing_address(orders)
47
+ billing_address_replacement = Hash.new { |hash, key| hash[key] = {} }
48
+
49
+ orders.each_with_index do |order, index|
50
+ next if order['billing_address'].nil? || order['billing_address'].empty?
51
+
52
+ old_address = order['billing_address']['address1']
53
+
54
+ if billing_address_replacement[old_address].empty?
55
+ billing_address_replacement[old_address]['address1'] = Faker::Address.street_address
56
+ billing_address_replacement[old_address]['address2'] = Faker::Address.secondary_address
57
+ billing_address_replacement[old_address]['city'] = Faker::Address.city
58
+ billing_address_replacement[old_address]['country'] = 'Canada'
59
+ billing_address_replacement[old_address]['company'] = Faker::Company.name
60
+ billing_address_replacement[old_address]['first_name'] = Faker::Name.first_name
61
+ billing_address_replacement[old_address]['last_name'] = Faker::Name.last_name
62
+ billing_address_replacement[old_address]['latitude'] = Faker::Address.latitude
63
+ billing_address_replacement[old_address]['longitude'] = Faker::Address.longitude
64
+ billing_address_replacement[old_address]['phone'] = Faker::PhoneNumber.phone_number
65
+ billing_address_replacement[old_address]['zip'] = Faker::Address.zip_code
66
+ billing_address_replacement[old_address]['name'] = billing_address_replacement[old_address]['first_name'] + billing_address_replacement[old_address]['last_name']
67
+ end
68
+
69
+ orders[index]['billing_address'] = billing_address_replacement[old_address]
70
+ end
71
+
72
+ orders
73
+ end
74
+
75
+ # Strip line item information and replace with fake data
76
+ def anonymize_line_items(orders)
77
+ line_items_replacement = Hash.new { |hash, key| hash[key] = {} }
78
+
79
+ orders.each_with_index do |order, index|
80
+ order['line_items'].each_with_index do |item, inner_index|
81
+ next if item.empty?
82
+ old_item = order['line_items'][inner_index]['product_id']
83
+
84
+ if line_items_replacement[old_item].empty?
85
+ line_items_replacement[old_item]['product_id'] = Faker::Number.number(8)
86
+ line_items_replacement[old_item]['price'] = Faker::Commerce.price.to_s
87
+ line_items_replacement[old_item]['title'] = Faker::Commerce.product_name
88
+ line_items_replacement[old_item]['variant_id'] = Faker::Number.number(10)
89
+ line_items_replacement[old_item]['vendor'] = Faker::Commerce.department
90
+ line_items_replacement[old_item]['name'] = Faker::Commerce.product_name
91
+ end
92
+
93
+ orders[index]['line_items'][inner_index] = line_items_replacement[old_item]
94
+ end
95
+ end
96
+
97
+ orders
98
+ end
99
+
100
+ # Strip customer data and replace with fake data
101
+ def anonymize_customers(orders)
102
+ customer_replacement = Hash.new { |hash, key| hash[key] = {} }
103
+
104
+ orders.each_with_index do |order, index|
105
+ next if order['customer'].empty?
106
+
107
+ old_customer = order['customer']['id']
108
+
109
+ if customer_replacement[old_customer].empty?
110
+ customer_replacement[old_customer]['id'] = Faker::Number.number(8)
111
+ customer_replacement[old_customer]['email'] = Faker::Internet.email
112
+ customer_replacement[old_customer]['first_name'] = Faker::Name.first_name
113
+ customer_replacement[old_customer]['last_name'] = Faker::Name.last_name
114
+
115
+ # TODO: Take billing_address parameter to fill in:
116
+ # customer_replacement[customer[:id]][:default_address]
117
+ end
118
+
119
+ orders[index]['customer'] = customer_replacement[old_customer]
120
+ end
121
+
122
+ orders
123
+ end
124
+
125
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModifyData
4
+ Faker::Config.locale = :"en-CA"
5
+
6
+ # Replace the shop data from real stores with randomly generated data
7
+ def self.anonymize_shop(raw_store_data)
8
+
9
+ store_data = JSON.parse(raw_store_data).fetch('shop') rescue (return raw_store_data)
10
+
11
+ # Store Details
12
+ store_data['id'] = Faker::Number.number(8) if store_data['id']
13
+ store_data['name'] = Faker::Company.name if store_data['name']
14
+ store_data['domain'] = Faker::Internet.url if store_data['domain']
15
+
16
+ # Owner
17
+ store_data['shop_owner'] = Faker::Name.name if store_data['shop_owner']
18
+ store_data['phone'] = Faker::PhoneNumber.phone_number if store_data['phone']
19
+ store_data['email'] = Faker::Internet.email if store_data['email']
20
+ store_data['customer_email'] = Faker::Internet.email if store_data['customer_email']
21
+
22
+ # Address
23
+ store_data['city'] = Faker::Address.city if store_data['city']
24
+ store_data['address1'] = Faker::Address.street_address if store_data['address1']
25
+ store_data['longitude'] = Faker::Address.longitude if store_data['longitude']
26
+ store_data['latitude'] = Faker::Address.latitude if store_data['latitude']
27
+ store_data['zip'] = Faker::Address.zip if store_data['zip']
28
+
29
+ # Set data back under the json 'shop' key and return as JSON
30
+ updated_store_data = JSON.parse('{}')
31
+ updated_store_data['shop'] = store_data
32
+ updated_store_data.to_json
33
+ end
34
+
35
+
36
+ # Intercept Shopify Orders and replace data with generated mock data
37
+ # Ensure predictable replacements of data
38
+ # e.g. If a real order from John Galt is replaced with a fake name, Jane Smith,
39
+ # every instance of John Galt should be replaced with the same data.
40
+ def self.anonymize_orders(raw_order_data)
41
+
42
+ order_data = JSON.parse(raw_order_data).fetch('orders') rescue (return raw_order_data)
43
+
44
+ order_data = anonymize_discounts(order_data)
45
+ order_data = anonymize_referrals(order_data)
46
+ order_data = anonymize_billing_address(order_data)
47
+ order_data = anonymize_line_items(order_data)
48
+ order_data = anonymize_customers(order_data)
49
+
50
+ # Set data back under the json 'orders' key and return as JSON
51
+ updated_order_data = JSON.parse('{}')
52
+ updated_order_data['orders'] = order_data
53
+ updated_order_data.to_json
54
+ end
55
+
56
+
57
+ # Traverse through orders and return a new array with each order <multiplier_constant> times
58
+ def self.duplicate_orders(raw_order_data, multiplier_constant:)
59
+ begin
60
+ order_data = JSON.parse(raw_order_data).fetch('orders')
61
+ rescue
62
+ return raw_order_data
63
+ end
64
+
65
+ duplicated_order_data = []
66
+
67
+ order_data.each do |order|
68
+ multiplier_constant.to_i.times { duplicated_order_data << order }
69
+ end
70
+
71
+ # Set data back under the json 'orders' key and return as JSON
72
+ returned_order_data = JSON.parse('{}')
73
+ returned_order_data['orders'] = duplicated_order_data
74
+ returned_order_data.to_json
75
+ end
76
+
77
+
78
+ # Ensure at least <floor> orders exist, or otherwise continually append the last order until enough orders exist
79
+ def self.number_of_orders_floor(raw_order_data, floor:)
80
+ order_data = JSON.parse(raw_order_data).fetch('orders') rescue (return raw_order_data)
81
+
82
+ order_delta = order_data.length - floor
83
+ return raw_order_data if order_delta >= 0
84
+
85
+ order_delta.to_i.times { new_order_data << order_data.last }
86
+
87
+ # Set data back under the json 'orders' key and return as JSON
88
+ returned_order_data = JSON.parse('{}')
89
+ returned_order_data['orders'] = new_order_data
90
+ returned_order_data.to_json
91
+ end
92
+
93
+
94
+ # Ensure no more than <ceiling> orders exist, or otherwise clip the array at <ceiling>
95
+ def self.number_of_orders_ceiling(raw_order_data, ceiling:)
96
+ order_data = JSON.parse(raw_order_data).fetch('orders') rescue (return raw_order_data)
97
+ trimmed_order_data = []
98
+
99
+ order_delta = ceiling - order_data.length
100
+ return raw_order_data if order_delta >= 0
101
+
102
+ ceiling.to_i.times { |i| trimmed_order_data << order_data[i] }
103
+
104
+ # Set data back under the json 'orders' key and return as JSON
105
+ returned_order_data = JSON.parse('{}')
106
+ returned_order_data['orders'] = trimmed_order_data
107
+ returned_order_data.to_json
108
+ end
109
+
110
+ end