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