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,62 @@
|
|
1
|
+
# Flow.io (2017)
|
2
|
+
# communicates with flow api, responds to webhook events
|
3
|
+
|
4
|
+
class Flow::Webhook
|
5
|
+
attr_accessor :product
|
6
|
+
attr_accessor :variant
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def process data, opts={}
|
10
|
+
web_hook = new data, opts
|
11
|
+
web_hook.process
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
###
|
16
|
+
|
17
|
+
def initialize(data, opts={})
|
18
|
+
@data = data
|
19
|
+
@opts = opts
|
20
|
+
end
|
21
|
+
|
22
|
+
def process
|
23
|
+
@discriminator = @data['discriminator']
|
24
|
+
|
25
|
+
m = 'hook_%s' % @discriminator
|
26
|
+
|
27
|
+
return 'Error: No hook for %s' % @discriminator unless respond_to?(m)
|
28
|
+
raise ArgumentError, 'Organization name mismatch for %s' % @data['organization'] if @data['organization'] != Flow.organization
|
29
|
+
|
30
|
+
send(m)
|
31
|
+
end
|
32
|
+
|
33
|
+
# hooks
|
34
|
+
|
35
|
+
def hook_localized_item_upserted
|
36
|
+
raise ArgumentError, 'number not found' unless @data['number']
|
37
|
+
raise ArgumentError, 'local not found' unless @data['local']
|
38
|
+
|
39
|
+
number = @data['number']
|
40
|
+
exp_key = @data['local']['experience']['key']
|
41
|
+
|
42
|
+
# for testing we need ability to inject dependency for variant class
|
43
|
+
variant_class = @opts[:variant_class] || Spree::Variant
|
44
|
+
|
45
|
+
@variant = variant_class.find number
|
46
|
+
@product = @variant.product
|
47
|
+
is_included = @data['local']['status'] == 'included'
|
48
|
+
|
49
|
+
@product.flow_data['%s.excluded' % exp_key] = is_included ? 0 : 1
|
50
|
+
|
51
|
+
@product.save!
|
52
|
+
|
53
|
+
message = is_included ? 'included in' : 'excluded from'
|
54
|
+
|
55
|
+
'Product id:%s - "%s" (from variant %s) %s experience "%s"' % [@product.id, @product.name, @variant.id, message, exp_key]
|
56
|
+
end
|
57
|
+
|
58
|
+
# we should consume only localized_item_upserted
|
59
|
+
def hook_subcatalog_item_upserted
|
60
|
+
'not used'
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# Flow.io (2017)
|
2
|
+
# helper class to manage product sync scheduling
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module FolwApiRefresh
|
8
|
+
extend self
|
9
|
+
|
10
|
+
SYNC_INTERVAL_IN_MINUTES = 60 unless defined?(SYNC_INTERVAL_IN_MINUTES)
|
11
|
+
CHECK_FILE = Pathname.new './tmp/last-flow-refresh.txt' unless defined?(CHECK_FILE)
|
12
|
+
LOGGER = Logger.new('./log/sync.log', 3, 1024000) unless defined?(LOGGER)
|
13
|
+
|
14
|
+
###
|
15
|
+
|
16
|
+
def get_data
|
17
|
+
CHECK_FILE.exist? ? JSON.parse(CHECK_FILE.read) : {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def write
|
21
|
+
data = get_data
|
22
|
+
yield data
|
23
|
+
CHECK_FILE.write data.to_json
|
24
|
+
data
|
25
|
+
end
|
26
|
+
|
27
|
+
def log message
|
28
|
+
$stdout.puts message
|
29
|
+
LOGGER.info '%s (pid/ppid: %d/%d)' % [message, Process.pid, Process.ppid]
|
30
|
+
end
|
31
|
+
|
32
|
+
def schedule_refresh!
|
33
|
+
write do |data|
|
34
|
+
data['force_refresh'] = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def log_refresh! start_time=nil
|
39
|
+
write do |data|
|
40
|
+
if start_time
|
41
|
+
data['force_refresh'] = false
|
42
|
+
data['duration_in_seconds'] = Time.now.to_i - start_time.to_i if start_time
|
43
|
+
data['start'] = start_time.to_i
|
44
|
+
data['end'] = Time.now.to_i
|
45
|
+
data.delete('started')
|
46
|
+
else
|
47
|
+
data['started'] = true
|
48
|
+
data['start'] = Time.now.to_i
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def last_refresh
|
54
|
+
json = get_data
|
55
|
+
|
56
|
+
return 'No last sync data' unless json['end']
|
57
|
+
|
58
|
+
info = []
|
59
|
+
|
60
|
+
info.push 'Sync started %d seconds ago (it is in progress).' % (Time.now.to_i - json['start'].to_i) if json['started']
|
61
|
+
|
62
|
+
info.push 'Last sync finished %{finished} minutes ago and lasted for %{duration} sec. We sync every %{every} minutes.' %
|
63
|
+
{
|
64
|
+
finished: (Time.now.to_i - json['end'].to_i)/60,
|
65
|
+
duration: json['duration_in_seconds'] || '?',
|
66
|
+
every: SYNC_INTERVAL_IN_MINUTES
|
67
|
+
}
|
68
|
+
|
69
|
+
info.join(' ')
|
70
|
+
end
|
71
|
+
|
72
|
+
def sync_products_if_needed!
|
73
|
+
json = get_data
|
74
|
+
|
75
|
+
sync_needed = false
|
76
|
+
|
77
|
+
unless json['started']
|
78
|
+
sync_needed ||= true if json['force_refresh']
|
79
|
+
sync_needed ||= true if json['start'].to_i < (Time.now.to_i - SYNC_INTERVAL_IN_MINUTES * 60)
|
80
|
+
end
|
81
|
+
|
82
|
+
if sync_needed
|
83
|
+
log 'Sync needed, running ...'
|
84
|
+
system 'bundle exec rake flow:sync_localized_items'
|
85
|
+
else
|
86
|
+
log last_refresh
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Flow.io (2017)
|
2
|
+
# adapter for Solidus/Spree that talks to activemerchant_flow
|
3
|
+
|
4
|
+
# load '/Users/dux/dev/org/flow.io/activemerchant_flow/lib/active_merchant/billing/gateways/flow.rb'
|
5
|
+
|
6
|
+
module Spree
|
7
|
+
class Gateway::Flow < Gateway
|
8
|
+
def provider_class
|
9
|
+
self.class
|
10
|
+
end
|
11
|
+
|
12
|
+
def actions
|
13
|
+
%w(capture authorize purchase refund void)
|
14
|
+
end
|
15
|
+
|
16
|
+
# if user wants to force auto capture
|
17
|
+
def auto_capture?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def payment_profiles_supported?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_type
|
26
|
+
'gateway'
|
27
|
+
end
|
28
|
+
|
29
|
+
def preferences
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
|
33
|
+
# def create_profile(payment)
|
34
|
+
# # binding.pry
|
35
|
+
# # ActiveMerchant::Billing::FlowGateway.new(token: Flow.api_key, organization: Flow.organization)
|
36
|
+
|
37
|
+
# case payment.order.state
|
38
|
+
# when 'payment'
|
39
|
+
# when 'confirm'
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
|
43
|
+
def supports?(source)
|
44
|
+
# flow supports credit cards
|
45
|
+
source.class == Spree::CreditCard
|
46
|
+
end
|
47
|
+
|
48
|
+
def authorize(amount, payment_method, options={})
|
49
|
+
order = load_order options
|
50
|
+
order.cc_authorization
|
51
|
+
end
|
52
|
+
|
53
|
+
def capture(amount, payment_method, options={})
|
54
|
+
order = load_order options
|
55
|
+
order.cc_capture
|
56
|
+
end
|
57
|
+
|
58
|
+
def purchase(amount, payment_method, options={})
|
59
|
+
order = load_order options
|
60
|
+
flow_auth = order.cc_authorization
|
61
|
+
|
62
|
+
if flow_auth.success?
|
63
|
+
order.cc_capture
|
64
|
+
else
|
65
|
+
flow_auth
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def refund(money, authorization_key, options={})
|
70
|
+
order = load_order options
|
71
|
+
order.cc_refund
|
72
|
+
end
|
73
|
+
|
74
|
+
def void(money, authorization_key, options={})
|
75
|
+
# binding.pry
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def load_order(options)
|
81
|
+
order_number = options[:order_id].split('-').first
|
82
|
+
spree_order = Spree::Order.find_by number: order_number
|
83
|
+
::Flow::SimpleGateway.new spree_order
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# require 'flowcommerce-activemerchant'
|
2
|
+
|
3
|
+
# module Spree
|
4
|
+
# class Gateway::StripeGateway < Gateway
|
5
|
+
# # https://dashboard.stripe.com/account/apikeys
|
6
|
+
|
7
|
+
# preference :secret_key, :string
|
8
|
+
# preference :publishable_key, :string
|
9
|
+
|
10
|
+
# CARD_TYPE_MAPPING = {
|
11
|
+
# 'American Express' => 'american_express',
|
12
|
+
# 'Diners Club' => 'diners_club',
|
13
|
+
# 'Visa' => 'visa'
|
14
|
+
# }
|
15
|
+
|
16
|
+
# def method_type
|
17
|
+
# 'gateway'
|
18
|
+
# end
|
19
|
+
|
20
|
+
# def provider_class
|
21
|
+
# ActiveMerchant::Billing::FlowGatewaya
|
22
|
+
# end
|
23
|
+
|
24
|
+
# def payment_profiles_supported?
|
25
|
+
# true
|
26
|
+
# end
|
27
|
+
|
28
|
+
# def purchase(money, creditcard, gateway_options)
|
29
|
+
# ap [:purchase, money, creditcard, gateway_options]
|
30
|
+
# binding.pry
|
31
|
+
# provider.purchase(*options_for_purchase_or_auth(money, creditcard, gateway_options))
|
32
|
+
# end
|
33
|
+
|
34
|
+
# def authorize(money, creditcard, gateway_options)
|
35
|
+
# ap [:authorize, money, creditcard, gateway_options]
|
36
|
+
# binding.pry
|
37
|
+
# provider.authorize(*options_for_purchase_or_auth(money, creditcard, gateway_options))
|
38
|
+
# end
|
39
|
+
|
40
|
+
# def capture(money, response_code, gateway_options)
|
41
|
+
# ap [:capture, money, response_code, gateway_options]
|
42
|
+
# binding.pry
|
43
|
+
# provider.capture(money, response_code, gateway_options)
|
44
|
+
# end
|
45
|
+
|
46
|
+
# def credit(money, creditcard, response_code, gateway_options)
|
47
|
+
# ap [:credit, money, creditcard, response_code, gateway_options]
|
48
|
+
# binding.pry
|
49
|
+
# provider.refund(money, response_code, {})
|
50
|
+
# end
|
51
|
+
|
52
|
+
# def void(response_code, creditcard, gateway_options)
|
53
|
+
# ap [:void, response_code, creditcard, gateway_options]
|
54
|
+
# binding.pry
|
55
|
+
# provider.void(response_code, {})
|
56
|
+
# end
|
57
|
+
|
58
|
+
# def cancel(response_code)
|
59
|
+
# ap [:cancel, response_code]
|
60
|
+
# provider.void(response_code, {})
|
61
|
+
# end
|
62
|
+
|
63
|
+
# def create_profile(payment)
|
64
|
+
# ap [:create_profile, payment]
|
65
|
+
# return unless payment.source.gateway_customer_profile_id.nil?
|
66
|
+
# options = {
|
67
|
+
# email: payment.order.email,
|
68
|
+
# login: preferred_secret_key,
|
69
|
+
# }.merge! address_for(payment)
|
70
|
+
|
71
|
+
# source = update_source!(payment.source)
|
72
|
+
# if source.number.blank? && source.gateway_payment_profile_id.present?
|
73
|
+
# creditcard = source.gateway_payment_profile_id
|
74
|
+
# else
|
75
|
+
# creditcard = source
|
76
|
+
# end
|
77
|
+
|
78
|
+
# response = provider.store(creditcard, options)
|
79
|
+
# if response.success?
|
80
|
+
# payment.source.update_attributes!({
|
81
|
+
# cc_type: payment.source.cc_type, # side-effect of update_source!
|
82
|
+
# gateway_customer_profile_id: response.params['id'],
|
83
|
+
# gateway_payment_profile_id: response.params['default_source'] || response.params['default_card']
|
84
|
+
# })
|
85
|
+
|
86
|
+
# else
|
87
|
+
# payment.send(:gateway_error, response.message)
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
|
91
|
+
# private
|
92
|
+
|
93
|
+
# # In this gateway, what we call 'secret_key' is the 'login'
|
94
|
+
# def options
|
95
|
+
# options = super
|
96
|
+
# options.merge(:login => preferred_secret_key)
|
97
|
+
# end
|
98
|
+
|
99
|
+
# def options_for_purchase_or_auth(money, creditcard, gateway_options)
|
100
|
+
# options = {}
|
101
|
+
# options[:description] = "Spree Order ID: #{gateway_options[:order_id]}"
|
102
|
+
# options[:currency] = gateway_options[:currency]
|
103
|
+
|
104
|
+
# if customer = creditcard.gateway_customer_profile_id
|
105
|
+
# options[:customer] = customer
|
106
|
+
# end
|
107
|
+
# if token_or_card_id = creditcard.gateway_payment_profile_id
|
108
|
+
# # The Stripe ActiveMerchant gateway supports passing the token directly as the creditcard parameter
|
109
|
+
# # The Stripe ActiveMerchant gateway supports passing the customer_id and credit_card id
|
110
|
+
# # https://github.com/Shopify/active_merchant/issues/770
|
111
|
+
# creditcard = token_or_card_id
|
112
|
+
# end
|
113
|
+
# return money, creditcard, options
|
114
|
+
# end
|
115
|
+
|
116
|
+
# def address_for(payment)
|
117
|
+
# {}.tap do |options|
|
118
|
+
# if address = payment.order.bill_address
|
119
|
+
# options.merge!(address: {
|
120
|
+
# address1: address.address1,
|
121
|
+
# address2: address.address2,
|
122
|
+
# city: address.city,
|
123
|
+
# zip: address.zipcode
|
124
|
+
# })
|
125
|
+
|
126
|
+
# if country = address.country
|
127
|
+
# options[:address].merge!(country: country.name)
|
128
|
+
# end
|
129
|
+
|
130
|
+
# if state = address.state
|
131
|
+
# options[:address].merge!(state: state.name)
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
# end
|
136
|
+
|
137
|
+
# def update_source!(source)
|
138
|
+
# source.cc_type = CARD_TYPE_MAPPING[source.cc_type] if CARD_TYPE_MAPPING.include?(source.cc_type)
|
139
|
+
# source
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
# end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<%= render partial: 'spree/admin/shared/order_tabs', locals: { current: "Payments" } %>
|
2
|
+
|
3
|
+
<% content_for :page_actions do %>
|
4
|
+
<% if @order.outstanding_balance? %>
|
5
|
+
<li id="new_payment_section">
|
6
|
+
<%= button_link_to Spree.t(:new_payment), new_admin_order_payment_url(@order) %>
|
7
|
+
</li>
|
8
|
+
<% end %>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
<% admin_breadcrumb(plural_resource_name(Spree::Payment)) %>
|
12
|
+
|
13
|
+
<% if @order.outstanding_balance? %>
|
14
|
+
<h5 class="outstanding-balance"><%= @order.outstanding_balance < 0 ? Spree.t(:credit_owed) : Spree.t(:balance_due) %>: <strong><%= @order.display_outstanding_balance %></strong></h5>
|
15
|
+
<% end %>
|
16
|
+
|
17
|
+
<% if @payments.any? %>
|
18
|
+
|
19
|
+
<fieldset data-hook="payment_list" class="no-border-bottom">
|
20
|
+
<legend align="center"><%= plural_resource_name(Spree::Payment) %></legend>
|
21
|
+
<%= render :partial => 'list', :locals => { :payments => @payments } %>
|
22
|
+
</fieldset>
|
23
|
+
|
24
|
+
<% if @refunds.any? %>
|
25
|
+
<fieldset data-hook="payment_list" class="no-border-bottom">
|
26
|
+
<legend align="center"><%= plural_resource_name(Spree::Refund) %></legend>
|
27
|
+
<%= render :partial => 'spree/admin/shared/refunds', :locals => { :refunds => @refunds, show_actions: true } %>
|
28
|
+
</fieldset>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
<% else %>
|
32
|
+
<div class="col-xs-9 no-objects-found"><%= Spree.t(:order_has_no_payments) %></div>
|
33
|
+
<% end %>
|
34
|
+
|
35
|
+
<!-- SHOW REFUNDS START -->
|
36
|
+
<%= render '/flow/show_refunds' %>
|
37
|
+
<!-- SHOW REFUNDS END -->
|
@@ -0,0 +1,59 @@
|
|
1
|
+
<% admin_breadcrumb(link_to plural_resource_name(Spree::Promotion), spree.admin_promotions_path) %>
|
2
|
+
<% admin_breadcrumb(@promotion.name) %>
|
3
|
+
|
4
|
+
|
5
|
+
<% content_for :page_actions do %>
|
6
|
+
<li>
|
7
|
+
<% if can?(:display, Spree::PromotionCode) %>
|
8
|
+
<%= button_link_to Spree.t(:download_promotion_code_list), admin_promotion_promotion_codes_path(promotion_id: @promotion.id, format: :csv) %>
|
9
|
+
<% end %>
|
10
|
+
</li>
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<%= form_for @promotion, :url => object_url, :method => :put do |f| %>
|
14
|
+
<fieldset class="no-border-top">
|
15
|
+
<%= render :partial => 'form', :locals => { :f => f } %>
|
16
|
+
<% if can?(:update, @promotion) %>
|
17
|
+
<%= render :partial => 'spree/admin/shared/edit_resource_links' %>
|
18
|
+
<% end %>
|
19
|
+
</fieldset>
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
<div id="promotion-filters" class="row">
|
23
|
+
<div id="rules_container" class="col-xs-6">
|
24
|
+
<%= render :partial => 'rules' %>
|
25
|
+
</div>
|
26
|
+
|
27
|
+
<div id="actions_container" class="col-xs-6">
|
28
|
+
<%= render :partial => 'actions' %>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<!-- Flow filters experience -->
|
33
|
+
|
34
|
+
<%
|
35
|
+
@promotion_keys = @promotion.flow_data.dig('filter', 'experience') || []
|
36
|
+
%>
|
37
|
+
|
38
|
+
<script>
|
39
|
+
window.promotion_set_option = function(key_name, value) {
|
40
|
+
var opts = {
|
41
|
+
id: <%= @promotion.id %>,
|
42
|
+
type: 'experience',
|
43
|
+
name: key_name,
|
44
|
+
value: value ? 1 : 0
|
45
|
+
};
|
46
|
+
|
47
|
+
$.post('/flow/promotion_set_option', opts, function(r) { console.log(r); });
|
48
|
+
}
|
49
|
+
</script>
|
50
|
+
|
51
|
+
<fieldset>
|
52
|
+
<legend align="center">Enable for Flow experiences</legend>
|
53
|
+
|
54
|
+
<p>If you do not select single experience, promotion will be enabled for all experiences.</p>
|
55
|
+
|
56
|
+
<% for experience in Flow::Experience.all %>
|
57
|
+
<p><label><input type="checkbox" onclick="promotion_set_option('<%= experience.key %>', this.checked);" <%= @promotion_keys.include?(experience.key) ? 'checked="1"' : '' %> /> <%= experience.key %></label></p>
|
58
|
+
<% end %>
|
59
|
+
</fieldset>
|