flowcommerce-solidus 0.1.11

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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/.version +1 -0
  3. data/bin/flowcommerce-solidus +121 -0
  4. data/lib/flowcommerce-solidus.rb +7 -0
  5. data/static/app/flow/README.md +77 -0
  6. data/static/app/flow/SOLIDUS_FLOW.md +127 -0
  7. data/static/app/flow/decorators/admin_decorators.rb +34 -0
  8. data/static/app/flow/decorators/localized_coupon_code_decorator.rb +49 -0
  9. data/static/app/flow/decorators/spree_credit_card_decorator.rb +35 -0
  10. data/static/app/flow/decorators/spree_order_decorator.rb +128 -0
  11. data/static/app/flow/decorators/spree_product_decorator.rb +16 -0
  12. data/static/app/flow/decorators/spree_user_decorator.rb +16 -0
  13. data/static/app/flow/decorators/spree_variant_decorator.rb +124 -0
  14. data/static/app/flow/flow.rb +67 -0
  15. data/static/app/flow/flow/error.rb +46 -0
  16. data/static/app/flow/flow/experience.rb +51 -0
  17. data/static/app/flow/flow/order.rb +267 -0
  18. data/static/app/flow/flow/pay_pal.rb +27 -0
  19. data/static/app/flow/flow/session.rb +77 -0
  20. data/static/app/flow/flow/simple_crypt.rb +30 -0
  21. data/static/app/flow/flow/simple_gateway.rb +123 -0
  22. data/static/app/flow/flow/webhook.rb +62 -0
  23. data/static/app/flow/lib/flow_api_refresh.rb +89 -0
  24. data/static/app/flow/lib/spree_flow_gateway.rb +86 -0
  25. data/static/app/flow/lib/spree_stripe_gateway.rb +142 -0
  26. data/static/app/views/spree/admin/payments/index.html.erb +37 -0
  27. data/static/app/views/spree/admin/promotions/edit.html.erb +59 -0
  28. data/static/app/views/spree/admin/shared/_order_summary.html.erb +58 -0
  29. data/static/app/views/spree/admin/shared/_order_summary_flow.html.erb +13 -0
  30. data/static/app/views/spree/order_mailer/confirm_email.html.erb +85 -0
  31. data/static/app/views/spree/order_mailer/confirm_email.text.erb +41 -0
  32. data/static/lib/tasks/flow.rake +248 -0
  33. metadata +160 -0
@@ -0,0 +1,128 @@
1
+ # added flow specific methods to Spree::Order
2
+ # http://docs.solidus.io/Spree/Order.html
3
+
4
+ require 'digest/sha1'
5
+
6
+ Spree::Order.class_eval do
7
+
8
+ # we now use Solidus number as Flow number, but I will this here for now
9
+ def flow_number
10
+ return self[:flow_number] unless self[:flow_number].blank?
11
+ return unless id
12
+ number
13
+ end
14
+
15
+ def flow_order
16
+ return nil unless flow_data['order']
17
+ Hashie::Mash.new flow_data['order']
18
+ end
19
+
20
+ # accepts line item, usually called from views
21
+ def flow_line_item_price line_item, total=false
22
+ result = unless flow_order
23
+ Flow.format_default_price(line_item.price * (total ? line_item.quantity : 1))
24
+ else
25
+ id = line_item.variant.id.to_s
26
+
27
+ lines = flow_order.lines || []
28
+ item = lines.select{ |el| el['item_number'] == id }.first
29
+
30
+ return Flow.price_not_found unless item
31
+
32
+ total ? item['total']['label'] : item['price']['label']
33
+ end
34
+
35
+ # add line item promo
36
+ # promo_total, adjustment_total
37
+ result += ' (%s)' % Flow.format_default_price(line_item.promo_total) if line_item.promo_total > 0
38
+
39
+ result
40
+ end
41
+
42
+ # prepares array of prices that can be easily renderd in templates
43
+ def flow_cart_breakdown
44
+ prices = []
45
+
46
+ price_model = Struct.new(:name, :label)
47
+
48
+ if flow_order
49
+ # duty, vat, ...
50
+ unless flow_order.prices
51
+ message = Flow::Error.format_message flow_order
52
+ raise Flow::Error.new(message)
53
+ end
54
+
55
+ flow_order.prices.each do |price|
56
+ prices.push price_model.new(price['key'].to_s.capitalize , price['label'])
57
+ end
58
+ else
59
+ price_elements = [:item_total, :adjustment_total, :included_tax_total, :additional_tax_total, :tax_total, :shipment_total, :promo_total]
60
+ price_elements.each do |el|
61
+ price = send(el)
62
+ if price > 0
63
+ label = Flow.format_default_price price
64
+ prices.push price_model.new(el.to_s.humanize.capitalize, label)
65
+ end
66
+ end
67
+
68
+ # discount is applied and we allways show it in default currency
69
+ if adjustment_total != 0
70
+ formated_discounted_price = Flow.format_default_price adjustment_total
71
+ prices.push price_model.new('Discount', formated_discounted_price)
72
+ end
73
+ end
74
+
75
+ # total
76
+ prices.push price_model.new(Spree.t(:total), flow_total)
77
+
78
+ prices
79
+ end
80
+
81
+ # shows localized total, if possible. if not, fall back to Solidus default
82
+ def flow_total
83
+ # r flow_order.total.label
84
+ price = flow_order.total.label if flow_order && flow_order.total
85
+ price || Flow.format_default_price(total)
86
+ end
87
+
88
+ def flow_experience
89
+ model = Struct.new(:key)
90
+ model.new flow_order.experience.key
91
+ rescue
92
+ model.new ENV.fetch('FLOW_BASE_COUNTRY')
93
+ end
94
+
95
+ # clear invalid zero amount payments. Solidsus bug?
96
+ def clear_zero_amount_payments!
97
+ # class attribute that can be set to true
98
+ return unless Flow::Order.clear_zero_amount_payments
99
+
100
+ payments.where(amount:0, state: ['invalid', 'processing', 'pending']).map(&:destroy)
101
+ end
102
+
103
+ def flow_order_authorized?
104
+ flow_data && flow_data['authorization'] ? true : false
105
+ end
106
+
107
+ def flow_order_captured?
108
+ flow_data['capture'] ? true : false
109
+ end
110
+
111
+ # completes order and sets all states to finalized and complete
112
+ # used when we have confirmed capture from Flow API or PayPal
113
+ def flow_finalize!
114
+ finalize! unless state == 'complete'
115
+ update_column :payment_state, 'paid' if payment_state != 'paid'
116
+ update_column :state, 'complete' if state != 'complete'
117
+ end
118
+
119
+ def flow_paymeny_method
120
+ if flow_data['payment_type'] == 'paypal'
121
+ 'paypal'
122
+ else
123
+ 'cc' # creait card is default
124
+ end
125
+ end
126
+
127
+ end
128
+
@@ -0,0 +1,16 @@
1
+ # added flow specific methods to Spree::Variant
2
+
3
+ Spree::Product.class_eval do
4
+
5
+ # returns price tied to local experience from master variant
6
+ def flow_local_price(flow_exp)
7
+ variants.first.flow_local_price(flow_exp)
8
+ end
9
+
10
+ def flow_included?(flow_exp)
11
+ return true unless flow_exp
12
+ flow_data['%s.excluded' % flow_exp.key].to_i == 1 ? false : true
13
+ end
14
+
15
+ end
16
+
@@ -0,0 +1,16 @@
1
+ # added flow specific methods to Spree::User
2
+ # which is for spree / solidus in same time
3
+ # - user object (for admins as well)
4
+ # - customer object
5
+
6
+ Spree::User.class_eval do
7
+
8
+ def flow_number
9
+ return unless id
10
+
11
+ token = ENV.fetch('SECRET_TOKEN')
12
+ 'su-%s' % Digest::SHA1.hexdigest('%d-%s' % [id, token])
13
+ end
14
+
15
+ end
16
+
@@ -0,0 +1,124 @@
1
+ # added flow specific methods to Spree::Variant
2
+ # solidus / spree save all the prices inside Variant object
3
+ # we choose to have cache jsonb field named flow_data that will hold all important
4
+ # flow sync data for specific
5
+
6
+ Spree::Variant.class_eval do
7
+
8
+ # after every save we sync product
9
+ # we generate sh1 checksums to update only when change happend
10
+ after_save :flow_sync_product
11
+
12
+ # clears flow cache from all records
13
+ def self.flow_truncate
14
+ all_records = all
15
+ all_records.each { |o| o.update_column :flow_data, {} }
16
+ puts 'Truncated %d records' % all_records.length
17
+ end
18
+
19
+ ###
20
+
21
+ # syncs product variant with flow
22
+ def flow_sync_product
23
+ # initial Solidus seed will fail, so skip unless we have Flow data folder
24
+ return unless respond_to?(:flow_data)
25
+
26
+ flow_item = flow_api_item
27
+ flow_item_sh1 = Digest::SHA1.hexdigest flow_api_item.to_json
28
+
29
+ # skip if sync not needed
30
+ return nil if flow_data['last_sync_sh1'] == flow_item_sh1
31
+
32
+ response = FlowCommerce.instance.items.put_by_number(Flow.organization, id.to_s, flow_item)
33
+
34
+ # after successful put, write cache
35
+ update_column :flow_data, flow_data.merge('last_sync_sh1'=>flow_item_sh1)
36
+
37
+ response
38
+ end
39
+
40
+ ###
41
+
42
+ def flow_spree_price
43
+ '%s %s' % [self.price, self.cost_currency]
44
+ end
45
+
46
+ def flow_prices flow_exp
47
+ flow_data.dig('exp', flow_exp.key, 'prices') || []
48
+ end
49
+
50
+ # returns price tied to local experience
51
+ def flow_local_price flow_exp
52
+ price = flow_prices(flow_exp).first
53
+
54
+ if flow_exp && price
55
+ price['label']
56
+ else
57
+ flow_spree_price
58
+ end
59
+ end
60
+
61
+ # creates object for flow api
62
+ # TODO: Remove and use the one in rakefile
63
+ def flow_api_item
64
+ image_base = 'http://cdn.color-mont.com'
65
+
66
+ # add product categories
67
+ categories = []
68
+ taxon = product.taxons.first
69
+ while taxon
70
+ categories.unshift taxon.name
71
+ taxon = taxon.parent
72
+ end
73
+
74
+ images = product.images.first ? [
75
+ { url: image_base + product.display_image.attachment(:large), tags: ['main'] },
76
+ { url: image_base + product.images.first.attachment.url(:product), tags: ['thumbnail'] }
77
+ ] : []
78
+
79
+ Io::Flow::V0::Models::ItemForm.new(
80
+ number: id.to_s,
81
+ locale: 'en_US',
82
+ language: 'en',
83
+ name: product.name,
84
+ description: product.description,
85
+ currency: cost_currency,
86
+ price: price.to_f,
87
+ images: images,
88
+ categories: categories,
89
+ attributes: {
90
+ weight: weight.to_s,
91
+ height: height.to_s,
92
+ width: width.to_s,
93
+ depth: depth.to_s,
94
+ is_master: is_master ? 'true' : 'false',
95
+ product_id: product_id.to_s,
96
+ tax_category: product.tax_category_id.to_s,
97
+ product_description: product.description,
98
+ product_shipping_category: product.shipping_category_id ? shipping_category.name : nil,
99
+ product_meta_title: product.meta_title.to_s,
100
+ product_meta_description: product.meta_description.to_s,
101
+ product_meta_keywords: product.meta_keywords.to_s,
102
+ product_slug: product.slug,
103
+ }.select{ |k,v| v.present? }
104
+ )
105
+ end
106
+
107
+ # gets flow catalog item, and imports it
108
+ # called from flow:sync_localized_items rake task
109
+ def flow_import_item(item)
110
+ experience_key = item.local.experience.key
111
+ flow_data['exp'] ||= {}
112
+ flow_data['exp'][experience_key] = {}
113
+ flow_data['exp'][experience_key]['status'] = item.local.status.value
114
+ flow_data['exp'][experience_key]['prices'] = item.local.prices.map do |price|
115
+ price = price.to_hash
116
+ [:includes, :adjustment].each { |el| price.delete(el) unless price[el] }
117
+ price
118
+ end
119
+
120
+ update_column :flow_data, flow_data.dup
121
+ end
122
+
123
+ end
124
+
@@ -0,0 +1,67 @@
1
+ # module for communication and customization based on flow api
2
+ # for now all in same class
3
+
4
+ require 'logger'
5
+
6
+ module Flow
7
+ extend self
8
+
9
+ def organization() ENV.fetch('FLOW_ORGANIZATION') end
10
+ def base_country() ENV.fetch('FLOW_BASE_COUNTRY') end
11
+ def api_key() ENV.fetch('FLOW_API_KEY') end
12
+
13
+ # builds curl command and gets remote data
14
+ def api action, path, params={}, body=nil
15
+ body ||= params.delete(:BODY)
16
+
17
+ remote_params = URI.encode_www_form params
18
+ remote_path = debug_path = path.sub('%o', Flow.organization).sub(':organization', Flow.organization)
19
+ remote_path += '?%s' % remote_params unless remote_params.blank?
20
+
21
+ curl = ['curl -s']
22
+ curl.push '-X %s' % action.to_s.upcase
23
+ curl.push '-u %s:' % api_key
24
+
25
+ if body
26
+ body = body.to_json unless body.is_a?(Array)
27
+ curl.push '-H "Content-Type: application/json"'
28
+ curl.push "-d '%s'" % body.gsub(%['], %['"'"']) if body
29
+ end
30
+
31
+ curl.push '"https://api.flow.io%s"' % remote_path
32
+ command = curl.join(' ')
33
+
34
+ puts command if defined?(Rails::Console)
35
+
36
+ dir = Rails.root.join('log/api')
37
+ Dir.mkdir(dir) unless Dir.exist?(dir)
38
+ debug_file = '%s/%s.bash' % [dir, debug_path.gsub(/[^\w]+/, '_')]
39
+ File.write debug_file, command + "\n"
40
+
41
+ data = JSON.load `#{command}`
42
+
43
+ if data.kind_of?(Hash) && data['code'] == 'generic_error'
44
+ ap data
45
+ data
46
+ else
47
+ data
48
+ end
49
+ end
50
+
51
+ def log_api_error
52
+
53
+ end
54
+
55
+ def logger
56
+ @logger ||= Logger.new('./log/flow.log') # or nil for no logging
57
+ end
58
+
59
+ def price_not_found
60
+ 'n/a'
61
+ end
62
+
63
+ def format_default_price amount
64
+ '$%.2f' % amount
65
+ end
66
+
67
+ end
@@ -0,0 +1,46 @@
1
+ # Flow (2017)
2
+ # for flow errors
3
+
4
+ require 'digest/sha1'
5
+
6
+ class Flow::Error < StandardError
7
+
8
+ # logs error to file for easy discovery and fix
9
+ def self.log(exception, request)
10
+ history = exception.backtrace.reject{ |el| el.index('/gems/') }.map{ |el| el.sub(Rails.root.to_s, '') }.join($/)
11
+
12
+ msg = '%s in %s' % [exception.class, request.url]
13
+ data = [msg, exception.message, history].join("\n\n")
14
+ key = Digest::SHA1.hexdigest exception.backtrace.first.split(' ').first
15
+
16
+ folder = Rails.root.join('log/exceptions').to_s
17
+ Dir.mkdir(folder) unless Dir.exists?(folder)
18
+
19
+ folder += "/#{exception.class.to_s.tableize.gsub('/','-')}"
20
+ Dir.mkdir(folder) unless Dir.exists?(folder)
21
+
22
+ "#{folder}/#{key}.txt".tap do |path|
23
+ File.write(path, data)
24
+ end
25
+ end
26
+
27
+ def self.format_message order, flow_experience=nil
28
+ message = if order['messages']
29
+ msg = order['messages'].join(', ')
30
+
31
+ if order['numbers']
32
+ msg += ' (%s)' % Spree::Variant.where(id: order['numbers']).map(&:name).join(', ')
33
+ end
34
+
35
+ msg
36
+ else
37
+ 'Order not properly localized (sync issue)'
38
+ end
39
+
40
+ sub_info = 'Flow.io'
41
+ sub_info += ' - %s' % flow_experience.key[0, 15] if flow_experience
42
+
43
+ '%s (%s)' % [message, sub_info]
44
+ end
45
+
46
+ end
@@ -0,0 +1,51 @@
1
+ # Flow.io (2017)
2
+ # communicates with flow api, easy access
3
+ # to basic shop frontend and backend needs
4
+
5
+ module Flow::Experience
6
+ extend self
7
+
8
+ def all no_world=nil
9
+ experiences = get_from_flow
10
+ no_world ? experiences.select{ |exp| exp.key != 'world' } : experiences
11
+ end
12
+
13
+ def keys
14
+ all.map{ |el| el.key }
15
+ end
16
+
17
+ def get key
18
+ all.each do |exp|
19
+ return exp if exp.key == key
20
+ end
21
+ nil
22
+ end
23
+
24
+ def compact
25
+ all.map { |exp| [exp.country, exp.key, exp.name] }
26
+ end
27
+
28
+ def default
29
+ Flow::Experience.all.select{ |exp| exp.key == 'united-states-of-america' }.first
30
+ end
31
+
32
+ private
33
+
34
+ def get_from_flow
35
+ return cached_experinces if cache_valid?
36
+
37
+ experiences = FlowCommerce.instance.experiences.get(Flow.organization)
38
+ @cache = [experiences, Time.now]
39
+ experiences
40
+ end
41
+
42
+ def cache_valid?
43
+ # cache experinces in worker memory for 1 minute
44
+ @cache && @cache[1] > Time.now.ago(1.minute)
45
+ end
46
+
47
+ def cached_experinces
48
+ @cache[0]
49
+ end
50
+
51
+ end