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.
- checksums.yaml +7 -0
- data/.version +1 -0
- data/bin/flowcommerce-solidus +121 -0
- data/lib/flowcommerce-solidus.rb +7 -0
- data/static/app/flow/README.md +77 -0
- data/static/app/flow/SOLIDUS_FLOW.md +127 -0
- data/static/app/flow/decorators/admin_decorators.rb +34 -0
- data/static/app/flow/decorators/localized_coupon_code_decorator.rb +49 -0
- data/static/app/flow/decorators/spree_credit_card_decorator.rb +35 -0
- data/static/app/flow/decorators/spree_order_decorator.rb +128 -0
- data/static/app/flow/decorators/spree_product_decorator.rb +16 -0
- data/static/app/flow/decorators/spree_user_decorator.rb +16 -0
- data/static/app/flow/decorators/spree_variant_decorator.rb +124 -0
- data/static/app/flow/flow.rb +67 -0
- data/static/app/flow/flow/error.rb +46 -0
- data/static/app/flow/flow/experience.rb +51 -0
- data/static/app/flow/flow/order.rb +267 -0
- data/static/app/flow/flow/pay_pal.rb +27 -0
- data/static/app/flow/flow/session.rb +77 -0
- data/static/app/flow/flow/simple_crypt.rb +30 -0
- data/static/app/flow/flow/simple_gateway.rb +123 -0
- data/static/app/flow/flow/webhook.rb +62 -0
- data/static/app/flow/lib/flow_api_refresh.rb +89 -0
- data/static/app/flow/lib/spree_flow_gateway.rb +86 -0
- data/static/app/flow/lib/spree_stripe_gateway.rb +142 -0
- data/static/app/views/spree/admin/payments/index.html.erb +37 -0
- data/static/app/views/spree/admin/promotions/edit.html.erb +59 -0
- data/static/app/views/spree/admin/shared/_order_summary.html.erb +58 -0
- data/static/app/views/spree/admin/shared/_order_summary_flow.html.erb +13 -0
- data/static/app/views/spree/order_mailer/confirm_email.html.erb +85 -0
- data/static/app/views/spree/order_mailer/confirm_email.text.erb +41 -0
- data/static/lib/tasks/flow.rake +248 -0
- 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
|