shopify_dashboard_plus 0.0.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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