flowcommerce_spree 0.0.1

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