flowcommerce_spree 0.0.1

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +91 -0
  4. data/Rakefile +33 -0
  5. data/SPREE_FLOW.md +134 -0
  6. data/app/assets/javascripts/flowcommerce_spree/application.js +13 -0
  7. data/app/assets/stylesheets/flowcommerce_spree/application.css +15 -0
  8. data/app/controllers/concerns/current_zone_loader_decorator.rb +49 -0
  9. data/app/controllers/flowcommerce_spree/webhooks_controller.rb +25 -0
  10. data/app/helpers/flowcommerce_spree/application_helper.rb +6 -0
  11. data/app/helpers/spree/admin/orders_helper_decorator.rb +17 -0
  12. data/app/helpers/spree/core/controller_helpers/flow_io_order_helper_decorator.rb +53 -0
  13. data/app/mailers/spree/spree_order_mailer_decorator.rb +24 -0
  14. data/app/models/flowcommerce_spree/settings.rb +8 -0
  15. data/app/models/spree/credit_card_decorator.rb +9 -0
  16. data/app/models/spree/flow_io_product_decorator.rb +91 -0
  17. data/app/models/spree/flow_io_variant_decorator.rb +205 -0
  18. data/app/models/spree/gateway/spree_flow_gateway.rb +116 -0
  19. data/app/models/spree/line_item_decorator.rb +15 -0
  20. data/app/models/spree/order_decorator.rb +179 -0
  21. data/app/models/spree/promotion_decorator.rb +10 -0
  22. data/app/models/spree/promotion_handler/coupon_decorator.rb +30 -0
  23. data/app/models/spree/spree_user_decorator.rb +15 -0
  24. data/app/models/spree/taxon_decorator.rb +37 -0
  25. data/app/models/spree/zone_decorator.rb +7 -0
  26. data/app/models/spree/zones/flow_io_product_zone_decorator.rb +55 -0
  27. data/app/services/flowcommerce_spree/import_experience_items.rb +76 -0
  28. data/app/services/flowcommerce_spree/import_experiences.rb +37 -0
  29. data/app/services/flowcommerce_spree/order_sync.rb +231 -0
  30. data/app/views/layouts/flowcommerce_spree/application.html.erb +14 -0
  31. data/app/views/spree/admin/payments/index.html.erb +28 -0
  32. data/app/views/spree/admin/promotions/edit.html.erb +57 -0
  33. data/app/views/spree/admin/shared/_order_summary.html.erb +44 -0
  34. data/app/views/spree/admin/shared/_order_summary_flow.html.erb +13 -0
  35. data/app/views/spree/order_mailer/confirm_email.html.erb +86 -0
  36. data/app/views/spree/order_mailer/confirm_email.text.erb +38 -0
  37. data/config/initializers/flowcommerce_spree.rb +7 -0
  38. data/config/routes.rb +5 -0
  39. data/db/migrate/20201021160159_add_type_and_meta_to_spree_zone.rb +23 -0
  40. data/db/migrate/20201021755957_add_meta_to_spree_tables.rb +17 -0
  41. data/db/migrate/20201022173210_add_zone_type_to_spree_zone_members.rb +24 -0
  42. data/db/migrate/20201022174252_add_kind_to_zone.rb +22 -0
  43. data/lib/flow/error.rb +73 -0
  44. data/lib/flow/pay_pal.rb +25 -0
  45. data/lib/flow/simple_gateway.rb +115 -0
  46. data/lib/flowcommerce_spree.rb +31 -0
  47. data/lib/flowcommerce_spree/api.rb +48 -0
  48. data/lib/flowcommerce_spree/engine.rb +27 -0
  49. data/lib/flowcommerce_spree/experience_service.rb +65 -0
  50. data/lib/flowcommerce_spree/logging_http_client.rb +43 -0
  51. data/lib/flowcommerce_spree/logging_http_handler.rb +15 -0
  52. data/lib/flowcommerce_spree/refresher.rb +81 -0
  53. data/lib/flowcommerce_spree/session.rb +71 -0
  54. data/lib/flowcommerce_spree/version.rb +5 -0
  55. data/lib/flowcommerce_spree/webhook_service.rb +98 -0
  56. data/lib/simple_csv_writer.rb +44 -0
  57. data/lib/tasks/flowcommerce_spree.rake +289 -0
  58. metadata +220 -0
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowcommerceSpree
4
+ # represents flow.io order syncing service
5
+ # for easy integration we are currently passing:
6
+ # - flow experience
7
+ # - spree order
8
+ # - current customer, present as @current_spree_user controller instance variable
9
+ #
10
+ # example:
11
+ # flow_order = FlowcommerceSpree::OrderSync.new # init flow-order object
12
+ # order: Spree::Order.last,
13
+ # experience: @flow_session.experience
14
+ # customer: Spree::User.last
15
+ # flow_order.build_flow_request # builds json body to be posted to flow.io api
16
+ # flow_order.synchronize! # sends order to flow
17
+ class OrderSync
18
+ FLOW_CENTER = 'default'
19
+
20
+ attr_reader :digest, :order, :response
21
+
22
+ class << self
23
+ def clear_cache(order)
24
+ return unless order.flow_data['order']
25
+
26
+ order.flow_data.delete('order')
27
+ order.update_column :meta, order.meta.to_json
28
+ end
29
+ end
30
+
31
+ def initialize(order:)
32
+ raise(ArgumentError, 'Experience not defined or not active') unless order.zone&.flow_io_active_experience?
33
+
34
+ @client = FlowcommerceSpree.client(session_id: order.flow_data['session_id'])
35
+ @experience = order.flow_io_experience_key
36
+ @order = order
37
+ @customer = order.user
38
+ @items = []
39
+ end
40
+
41
+ # helper method to send complete order from Spree to flow.io
42
+ def synchronize!
43
+ sync_body!
44
+ check_state!
45
+ write_response_in_cache
46
+ @response
47
+ end
48
+
49
+ def error
50
+ @response['messages'].join(', ')
51
+ end
52
+
53
+ def error_code
54
+ @response['code']
55
+ end
56
+
57
+ def error?
58
+ @response&.[]('code') && @response&.[]('messages') ? true : false
59
+ end
60
+
61
+ def delivery
62
+ deliveries.select { |el| el[:active] }.first
63
+ end
64
+
65
+ # delivery methods are defined in flow console
66
+ def deliveries
67
+ # if we have erorr with an order, but still using this method
68
+ return [] unless @order.flow_order
69
+
70
+ @order.flow_data ||= {}
71
+
72
+ delivery_list = @order.flow_order['deliveries'][0]['options']
73
+ delivery_list = delivery_list.map do |opts|
74
+ name = opts['tier']['name']
75
+
76
+ # add original Flow ID
77
+ # name += ' (%s)' % opts['tier']['strategy'] if opts['tier']['strategy']
78
+
79
+ selection_id = opts['id']
80
+
81
+ { id: selection_id,
82
+ price: { label: opts['price']['label'] },
83
+ active: @order.flow_order['selections'].include?(selection_id),
84
+ name: name }
85
+ end.to_a
86
+
87
+ # make first one active unless we have active element
88
+ delivery_list.first[:active] = true unless delivery_list.select { |el| el[:active] }.first
89
+
90
+ delivery_list
91
+ end
92
+
93
+ def total_price
94
+ @order.flow_total
95
+ end
96
+
97
+ def delivered_duty
98
+ # paid is default
99
+ @order.flow_data['delivered_duty'] || ::Io::Flow::V0::Models::DeliveredDuty.paid.value
100
+ end
101
+
102
+ # builds object that can be sent to api.flow.io to sync order data
103
+ def build_flow_request
104
+ @order.line_items.each { |line_item| add_item(line_item) }
105
+
106
+ @opts = {}
107
+ @opts[:experience] = @experience
108
+ @opts[:expand] = ['experience']
109
+
110
+ @body = { items: @items, number: @order.number }
111
+
112
+ add_customer if @customer
113
+
114
+ if (flow_data = @order.flow_data['order'])
115
+ @body[:selections] = flow_data['selections'].presence
116
+ @body[:delivered_duty] = flow_data['delivered_duty'].presence
117
+ @body[:attributes] = flow_data['attributes'].presence
118
+
119
+ if @order.adjustment_total != 0
120
+ # discount on full order is applied
121
+ @body[:discount] = { amount: @order.adjustment_total, currency: @order.currency }
122
+ end
123
+ end
124
+
125
+ # calculate digest body and cache it
126
+ @digest = Digest::SHA1.hexdigest(@opts.to_json + @body.to_json)
127
+ end
128
+
129
+ private
130
+
131
+ # if customer is defined, add customer info
132
+ # it is possible to have order in Spree without customer info (new guest session)
133
+ def add_customer
134
+ return unless @customer
135
+
136
+ address = @customer.ship_address
137
+ # address = nil
138
+ if address
139
+ @body[:customer] = { name: { first: address.firstname,
140
+ last: address.lastname },
141
+ email: @customer.email,
142
+ number: @customer.flow_number,
143
+ phone: address.phone }
144
+
145
+ streets = []
146
+ streets.push address.address1 unless address.address1.blank?
147
+ streets.push address.address2 unless address.address2.blank?
148
+
149
+ @body[:destination] = { streets: streets,
150
+ city: address.city,
151
+ province: address.state_name,
152
+ postal: address.zipcode,
153
+ country: (address.country.iso3 || 'USA'),
154
+ contact: @body[:customer] }
155
+
156
+ @body[:destination].delete_if { |_k, v| v.nil? }
157
+ end
158
+
159
+ @body
160
+ end
161
+
162
+ def sync_body!
163
+ build_flow_request if @body.blank?
164
+
165
+ @use_get = false
166
+
167
+ # use get if order is completed and closed
168
+ @use_get = true if @order.state == 'complete'
169
+
170
+ # use get if local digest hash check said there is no change
171
+ @use_get ||= true if @order.flow_data['digest'] == @digest
172
+
173
+ # do not use get if there is no local order cache
174
+ @use_get = false unless @order.flow_data['order']
175
+
176
+ if @use_get
177
+ @response ||= FlowcommerceSpree::Api.run :get, "/:organization/orders/#{@body[:number]}", expand: 'experience'
178
+ else
179
+ @response = @client.orders.put_by_number(FlowcommerceSpree::ORGANIZATION, @order.number,
180
+ Io::Flow::V0::Models::OrderPutForm.new(@body), @opts).to_hash
181
+ end
182
+ end
183
+
184
+ def check_state!
185
+ # authorize if not authorized
186
+ # if !@order.flow_order_authorized?
187
+
188
+ # authorize payment on complete, unless authorized
189
+ if @order.state == 'complete' && !@order.flow_order_authorized?
190
+ simple_gateway = Flow::SimpleGateway.new(@order)
191
+ simple_gateway.cc_authorization
192
+ end
193
+
194
+ @order.flow_finalize! if @order.flow_order_authorized? && @order.state != 'complete'
195
+ end
196
+
197
+ def add_item(line_item)
198
+ variant = line_item.variant
199
+ price_root = variant.flow_data&.dig('exp', @experience, 'prices')&.[](0) || {}
200
+
201
+ # create flow order line item
202
+ item = { center: FLOW_CENTER,
203
+ number: variant.sku,
204
+ quantity: line_item.quantity,
205
+ price: { amount: price_root['amount'] || variant.cost_price,
206
+ currency: price_root['currency'] || variant.cost_currency } }
207
+
208
+ @items.push item
209
+ end
210
+
211
+ # set cache for total order ammount
212
+ # written in flow_data field inside spree_orders table
213
+ def write_response_in_cache
214
+ if !@response || error?
215
+ @order.flow_data.delete('digest')
216
+ @order.flow_data.delete('order')
217
+ else
218
+ response_total = @response.dig('total', 'label')
219
+ cache_total = @order.flow_data.dig('order', 'total', 'label')
220
+
221
+ # return if total is not changed, no products removed or added
222
+ return if @use_get && response_total == cache_total
223
+
224
+ # update local order
225
+ @order.flow_data.merge!('digest' => @digest, 'order' => @response.to_hash)
226
+ end
227
+
228
+ @order.update_column(:meta, @order.meta.to_json)
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>FlowcommerceSpree</title>
5
+ <%= stylesheet_link_tag "flowcommerce_spree/application", media: "all" %>
6
+ <%= javascript_include_tag "flowcommerce_spree/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,28 @@
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), icon: 'plus' %>
7
+ </li>
8
+ <% end %>
9
+ <li><%= button_link_to Spree.t(:back_to_orders_list), admin_orders_path, icon: 'arrow-left' %></li>
10
+ <% end %>
11
+
12
+ <% content_for :page_title do %>
13
+ <i class="fa fa-arrow-right"></i> <%= Spree.t(:payments) %>
14
+ <% end %>
15
+
16
+ <% if @order.outstanding_balance? %>
17
+ <h5 class="outstanding-balance"><%= @order.outstanding_balance < 0 ? Spree.t(:credit_owed) : Spree.t(:balance_due) %>: <strong><%= @order.display_outstanding_balance %></strong></h5>
18
+ <% end %>
19
+
20
+ <% if @payments.any? %>
21
+ <%= render partial: 'list', :locals => { :payments => @payments } %>
22
+ <% else %>
23
+ <div class="alpha twelve columns no-objects-found"><%= Spree.t(:order_has_no_payments) %></div>
24
+ <% end %>
25
+
26
+ <!-- SHOW REFUNDS START -->
27
+ <%= render '/flow/show_refunds' %>
28
+ <!-- SHOW REFUNDS END -->
@@ -0,0 +1,57 @@
1
+ <% content_for :page_title do %>
2
+ <%= Spree.t(:editing_promotion) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <li>
7
+ <%= button_link_to Spree.t(:back_to_promotions_list), admin_promotions_path, icon: 'arrow-left' %>
8
+ </li>
9
+ <% end %>
10
+
11
+ <%= form_for @promotion, url: object_url, method: :put do |f| %>
12
+ <fieldset class="no-border-top">
13
+ <%= render partial: 'form', locals: { f: f } %>
14
+ <%= render partial: 'spree/admin/shared/edit_resource_links' %>
15
+ </fieldset>
16
+ <% end %>
17
+
18
+ <div id="promotion-filters" class="row">
19
+ <div id="rules_container" class="alpha eight columns">
20
+ <%= render partial: 'rules' %>
21
+ </div>
22
+
23
+ <div id="actions_container" class="omega eight columns">
24
+ <%= render partial: 'actions' %>
25
+ </div>
26
+ </div>
27
+
28
+ <%= render partial: 'spree/admin/variants/autocomplete', :formats => [:js] %>
29
+
30
+ <!-- Flow filters experience -->
31
+
32
+ <%
33
+ @promotion_keys = @promotion.flow_data.dig('filter', 'experience') || []
34
+ %>
35
+
36
+ <script>
37
+ window.promotion_set_option = function(key_name, value) {
38
+ var opts = {
39
+ id: <%= @promotion.id %>,
40
+ type: 'experience',
41
+ name: key_name,
42
+ value: value ? 1 : 0
43
+ };
44
+
45
+ $.post('/flow/promotion_set_option', opts, function(r) { console.log(r); });
46
+ }
47
+ </script>
48
+
49
+ <fieldset>
50
+ <legend align="center">Enable for Flow experiences</legend>
51
+
52
+ <p>Promotion will be enabled for all experiences unless a selection is made.</p>
53
+
54
+ <% for experience in FlowcommerceSpree::ExperienceService.all %>
55
+ <p><label><input type="checkbox" onclick="promotion_set_option('<%= experience.key %>', this.checked);" <%= @promotion_keys.include?(experience.key) ? 'checked="1"' : '' %> /> <%= experience.key %></label></p>
56
+ <% end %>
57
+ </fieldset>
@@ -0,0 +1,44 @@
1
+ <header id="order_tab_summary" data-hook>
2
+ <dl class="additional-info">
3
+ <dt id="order_status" data-hook><%= Spree.t(:status) %>:</dt>
4
+ <dd><span class="state <%= @order.state %>"><%= Spree.t(@order.state, scope: :order_state) %></span></dd>
5
+ <dt data-hook='admin_order_tab_subtotal_title'><%= Spree.t(:subtotal) %>:</dt>
6
+ <dd id='item_total'><%= @order.display_item_total.to_html %></dd>
7
+ <% if checkout_steps.include?("delivery") && @order.ship_total > 0 %>
8
+ <dt data-hook='admin_order_tab_ship_total_title'><%= Spree.t(:ship_total) %>:</dt>
9
+ <dd id='ship_total'><%= @order.display_ship_total.to_html %></dd>
10
+ <% end %>
11
+
12
+ <% if @order.included_tax_total != 0 %>
13
+ <dt data-hook='admin_order_tab_included_tax_title'><%= Spree.t(:tax_included) %>:</dt>
14
+ <dd id='included_tax_total'><%= @order.display_included_tax_total.to_html %></dd>
15
+ <% end %>
16
+
17
+ <% if @order.additional_tax_total != 0 %>
18
+ <dt data-hook='admin_order_tab_additional_tax_title'><%= Spree.t(:tax) %>:</dt>
19
+ <dd id='additional_tax_total'><%= @order.display_additional_tax_total.to_html %></dd>
20
+ <% end %>
21
+
22
+ <dt data-hook='admin_order_tab_total_title'><%= Spree.t(:total) %>:</dt>
23
+ <dd id='order_total'><%= @order.display_total.to_html %></dd>
24
+
25
+ <% if @order.completed? %>
26
+ <dt><%= Spree.t(:shipment) %>: </dt>
27
+ <dd id='shipment_status'><span class="state <%= @order.shipment_state %>"><%= Spree.t(@order.shipment_state, scope: :shipment_states, default: [:missing, "none"]) %></span></dd>
28
+ <dt><%= Spree.t(:payment) %>: </dt>
29
+ <dd id='payment_status'><span class="state <%= @order.payment_state %>"><%= Spree.t(@order.payment_state, scope: :payment_states, default: [:missing, "none"]) %></span></dd>
30
+ <dt data-hook='admin_order_tab_date_completed_title'><%= Spree.t(:date_completed) %>:</dt>
31
+ <dd id='date_complete'><%= pretty_time(@order.completed_at) %></dd>
32
+ <% end %>
33
+
34
+ <% if @order.approved? %>
35
+ <dt><%= Spree.t(:approver) %></dt>
36
+ <dd><%= @order.approver.email %></dd>
37
+ <dt><%= Spree.t(:approved_at) %></dt>
38
+ <dd><%= pretty_time(@order.approved_at) %></dd>
39
+ <% end %>
40
+ </dl>
41
+ </header>
42
+
43
+ <!-- Add Flow order summary, if available -->
44
+ <%= render partial: 'spree/admin/shared/order_summary_flow' %>
@@ -0,0 +1,13 @@
1
+ <hr />
2
+
3
+ <h5 class="sidebar-title">Flow details</h5>
4
+
5
+ <%= total_cart_breakdown %>
6
+
7
+ <% if @order.flow_data['order'] %>
8
+ <p>Experience: <b><%= @order.flow_data.dig('order', 'experience', 'key') %></b></p>
9
+ <a target="_fc_<%= @order.id %>" href="https://console.flow.io/<%= FlowcommerceSpree::ORGANIZATION %>/orders/<%= @order.number %>">Open in Flow console</a>
10
+ <% else %>
11
+ <p>Order is not localized with Flow.</p>
12
+ <% end %>
13
+
@@ -0,0 +1,86 @@
1
+ <%
2
+ # render text email in console. parts[1] for html body
3
+ # puts Spree::OrderMailer.confirm_email(Spree::Order.last).body.parts[0].body
4
+
5
+ @prices = @order.flow_cart_breakdown
6
+ @total_price = @prices.pop
7
+ %>
8
+
9
+ <style>
10
+ table.order td { padding: 4px; border-top: 1px solid #bbb; }
11
+ </style>
12
+
13
+ <h6>Dear <%= @order.bill_address.firstname %></h6>
14
+
15
+ <br>
16
+ <br>
17
+
18
+ <p><%= Spree.t('order_mailer.confirm_email.instructions') %></p>
19
+ <p><%= Spree.t('order_mailer.confirm_email.order_summary') %></p>
20
+
21
+ <table class="order">
22
+ <tr>
23
+ <th>Product</th>
24
+ <th width="100" align="right">Price</th>
25
+ <th width="80" align="center">Quantity</th>
26
+ <th width="100" align="right">Total</th>
27
+ </tr>
28
+ <% @order.line_items.each do |line_item| %>
29
+ <tr>
30
+ <td><%= line_item.variant.product.name %></td>
31
+ <td align="right"><%= @order.flow_line_item_price(line_item) %></td>
32
+ <td align="center"><%= line_item.quantity %></td>
33
+ <td align="right"><%= @order.flow_line_item_price(line_item, :with_quantity) %></td>
34
+ </tr>
35
+ <% end %>
36
+ </table>
37
+
38
+ <br>
39
+
40
+ <p><b>Total</b></p>
41
+
42
+ <table class="order">
43
+
44
+ <% @prices.each do |price| %>
45
+ <tr><td width="120"><%= price.name.capitalize %></td><td align="right"><%= price.label %></td></tr>
46
+ <% end %>
47
+
48
+ <tr>
49
+ <td><%= Spree.t(:total) %></td>
50
+ <td align="right"><b><%= @total_price.label %></b></td>
51
+ </tr>
52
+ <tr>
53
+ <td>Payment method</td>
54
+ <td align="right"><%= @order.flow_payment_method == 'paypal' ? 'PayPal' : 'Credit Card' %></td>
55
+ </tr>
56
+ </table>
57
+
58
+ <br>
59
+
60
+ <% ['ship', 'bill'].each do |name|
61
+ address = @order.send('%s_address' % name)
62
+ %>
63
+ <p><b><%= name.capitalize %>ing address</b></p>
64
+
65
+ <table class="order">
66
+ <tr>
67
+ <td>Full name</td>
68
+ <td><%= address.firstname %> <%= address.lastname %></td>
69
+ </tr>
70
+ <tr>
71
+ <td>Address</td>
72
+ <td><%= address.address1 %></td>
73
+ </tr>
74
+ <tr>
75
+ <td>City</td>
76
+ <td><%= address.city %></td>
77
+ </tr>
78
+ <tr>
79
+ <td>Country</td>
80
+ <td><%= address.country.name rescue '-' %>, <%= address.state.name rescue '-' %></td>
81
+ </tr>
82
+ </table>
83
+ <br />
84
+ <% end %>
85
+
86
+ <p><%= Spree.t('order_mailer.confirm_email.thanks') %></p>