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,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>