flowcommerce-solidus 0.1.11

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