solidus_importer 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.readme/import-products-1.gif +0 -0
  3. data/.readme/import-products-2.gif +0 -0
  4. data/README.md +9 -1
  5. data/Rakefile +4 -0
  6. data/app/models/solidus_importer/order_updater.rb +6 -0
  7. data/app/models/solidus_importer/spree_core_importer_order.rb +50 -0
  8. data/bin/rails-sandbox +1 -1
  9. data/bin/sandbox +2 -0
  10. data/lib/solidus_importer.rb +1 -0
  11. data/lib/solidus_importer/base_importer.rb +9 -2
  12. data/lib/solidus_importer/configuration.rb +7 -2
  13. data/lib/solidus_importer/order_importer.rb +39 -0
  14. data/lib/solidus_importer/process_import.rb +5 -1
  15. data/lib/solidus_importer/process_row.rb +3 -1
  16. data/lib/solidus_importer/processors/bill_address.rb +52 -0
  17. data/lib/solidus_importer/processors/customer.rb +10 -7
  18. data/lib/solidus_importer/processors/customer_address.rb +44 -0
  19. data/lib/solidus_importer/processors/line_item.rb +33 -0
  20. data/lib/solidus_importer/processors/order.rb +41 -12
  21. data/lib/solidus_importer/processors/payment.rb +56 -0
  22. data/lib/solidus_importer/processors/ship_address.rb +52 -0
  23. data/lib/solidus_importer/processors/shipment.rb +86 -0
  24. data/lib/solidus_importer/version.rb +1 -1
  25. data/spec/features/solidus_importer/import_spec.rb +68 -4
  26. data/spec/features/solidus_importer/processors_spec.rb +14 -11
  27. data/spec/fixtures/solidus_importer/customers.csv +5 -3
  28. data/spec/fixtures/solidus_importer/orders.csv +2 -2
  29. data/spec/lib/solidus_importer/base_importer_spec.rb +10 -0
  30. data/spec/lib/solidus_importer/order_importer_spec.rb +63 -0
  31. data/spec/lib/solidus_importer/processors/bill_address_spec.rb +33 -0
  32. data/spec/lib/solidus_importer/processors/customer_address_spec.rb +67 -0
  33. data/spec/lib/solidus_importer/processors/customer_spec.rb +14 -1
  34. data/spec/lib/solidus_importer/processors/line_item_spec.rb +22 -0
  35. data/spec/lib/solidus_importer/processors/order_spec.rb +2 -23
  36. data/spec/lib/solidus_importer/processors/payment_spec.rb +31 -0
  37. data/spec/lib/solidus_importer/processors/ship_address_spec.rb +33 -0
  38. data/spec/lib/solidus_importer/processors/shipment_spec.rb +22 -0
  39. metadata +31 -9
  40. data/lib/solidus_importer/processors/address.rb +0 -47
  41. data/spec/lib/solidus_importer/processors/address_spec.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c368dcdb23d45c02c54cd01f247debce0e9083119bda2dc1f358a0718e3b7d8
4
- data.tar.gz: 4336a3ad45dc07ba7e6aa5d0e12a953242439a8698c3b8c6a0b36a175e38384d
3
+ metadata.gz: 06ff1cdd3f709eebef5325d77c7c4b43d4ea41de11c11365e0490f6f52214d2f
4
+ data.tar.gz: a5f0868efe1a6c62b7f048eba8aa81af91dd98364af367b8a44811acbed9440f
5
5
  SHA512:
6
- metadata.gz: 0a3de867687407c132ffe9444a4a1e3b5913dcde1cedabc3b28353bac061f9822bd87ce66fba277904ca20a9c63cfeb8854edadcc37cbff3a9852b80092066c2
7
- data.tar.gz: e32d1dc88ccc3db3a234223d0149514aa8173b163f940ada667e43e128a74802d4e2331ec41a7773694ba0cc744051390756c76f22eb2cad7c09504e3fc9c4cd
6
+ metadata.gz: a344000551b4f69f1795d7b9c114621a523a40b50a0fa5ca2ec0a3122b61fd69642f5ee6aa3a716f6e6cd2a7198e1a426f58012ab87f99118f843bb7c1c839e3
7
+ data.tar.gz: a181da584ecc1fc043a9ad5c877fe8ca6e7369a5c3a9f5cfd28e8fb71d70a0c4a9702f00b2172c92f2b6c1630104af1943c864ae21747b3607e752192c284727
data/README.md CHANGED
@@ -20,6 +20,14 @@ bundle exec rails g solidus_importer:install
20
20
 
21
21
  ## Usage
22
22
 
23
+ The imports can be fully managed from the backend UI, following progress (image processing can take a few seconds for each image).
24
+
25
+ ![Import products CSV from the backend](.readme/import-products-1.gif)
26
+
27
+ ![Look at the newly imported products](.readme/import-products-2.gif)
28
+
29
+ ### From the console
30
+
23
31
  Sample code to import some products:
24
32
 
25
33
  ```ruby
@@ -84,7 +92,7 @@ SolidusImporter::Config.solidus_importer[:customers][:processors].map! do |proce
84
92
  if processor == 'SolidusImporter::Processors::Log'
85
93
  'CustomLoggerProcessor'
86
94
  else
87
- processors
95
+ processor
88
96
  end
89
97
  end
90
98
  ```
data/Rakefile CHANGED
@@ -4,3 +4,7 @@ require 'solidus_dev_support/rake_tasks'
4
4
  SolidusDevSupport::RakeTasks.install
5
5
 
6
6
  task default: 'extension:specs'
7
+
8
+ require 'bundler'
9
+ Bundler.require
10
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,6 @@
1
+ module SolidusImporter
2
+ class OrderUpdater < Spree::OrderUpdater
3
+ # Override this method to avoid tax calculation
4
+ def update_taxes; end
5
+ end
6
+ end
@@ -0,0 +1,50 @@
1
+ module SolidusImporter
2
+ class SpreeCoreImporterOrder < Spree::Core::Importer::Order
3
+ def self.import(user, params)
4
+ params = params.to_h
5
+ ActiveRecord::Base.transaction do
6
+ ensure_country_id_from_params params[:ship_address_attributes]
7
+ ensure_state_id_from_params params[:ship_address_attributes]
8
+ ensure_country_id_from_params params[:bill_address_attributes]
9
+ ensure_state_id_from_params params[:bill_address_attributes]
10
+
11
+ create_params = params.slice :currency
12
+ order = Spree::Order.create! create_params
13
+ order.store ||= Spree::Store.default
14
+ order.associate_user!(user)
15
+ order.save!
16
+
17
+ shipments_attrs = params.delete(:shipments_attributes)
18
+
19
+ create_line_items_from_params(params.delete(:line_items_attributes), order)
20
+ create_shipments_from_params(shipments_attrs, order)
21
+ create_adjustments_from_params(params.delete(:adjustments_attributes), order)
22
+ create_payments_from_params(params.delete(:payments_attributes), order)
23
+
24
+ params.delete(:user_id) unless user.try(:has_spree_role?, "admin") && params.key?(:user_id)
25
+
26
+ completed_at = params.delete(:completed_at)
27
+
28
+ order.update!(params)
29
+
30
+ order.create_proposed_shipments unless shipments_attrs.present?
31
+
32
+ if completed_at
33
+ order.completed_at = completed_at
34
+ order.state = 'complete'
35
+ order.save!
36
+ end
37
+
38
+ # Really ensure that the order totals & states are correct
39
+ updater = SolidusImporter::OrderUpdater.new(order)
40
+ updater.update
41
+ if shipments_attrs.present?
42
+ order.shipments.each_with_index do |shipment, index|
43
+ shipment.update_columns(cost: shipments_attrs[index][:cost].to_f) if shipments_attrs[index][:cost].present?
44
+ end
45
+ end
46
+ order.reload
47
+ end
48
+ end
49
+ end
50
+ end
@@ -5,7 +5,7 @@ app_root = 'sandbox'
5
5
  unless File.exist? "#{app_root}/bin/rails"
6
6
  warn 'Creating the sandbox app...'
7
7
  Dir.chdir "#{__dir__}/.." do
8
- system "#{__dir__}/sandbox" or begin # rubocop:disable Style/AndOr
8
+ system "#{__dir__}/sandbox" or begin
9
9
  warn 'Automatic creation of the sandbox app failed'
10
10
  exit 1
11
11
  end
@@ -72,9 +72,11 @@ unbundled bundle exec rails generate spree:install \
72
72
  --user_class=Spree::User \
73
73
  --enforce_available_locales=true \
74
74
  --with-authentication=false \
75
+ --payment-method=none
75
76
  $@
76
77
 
77
78
  unbundled bundle exec rails generate solidus:auth:install
79
+ unbundled bundle exec rails generate ${extension_name}:install
78
80
 
79
81
  echo
80
82
  echo "🚀 Sandbox app successfully created for $extension_name!"
@@ -6,6 +6,7 @@ require 'solidus_support'
6
6
  require 'solidus_importer/version'
7
7
  require 'solidus_importer/exception'
8
8
  require 'solidus_importer/base_importer'
9
+ require 'solidus_importer/order_importer'
9
10
 
10
11
  require 'solidus_importer/processors/base'
11
12
  processors = File.join(__dir__, 'solidus_importer/processors/*.rb')
@@ -20,7 +20,14 @@ module SolidusImporter
20
20
  end
21
21
 
22
22
  ##
23
- # Defines a method called after the import process is started
24
- def after_import(_ending_context); end
23
+ # Defines a method called after the import process is finished
24
+ def after_import(context)
25
+ context
26
+ end
27
+
28
+ ##
29
+ # Defines a method called after the import of each row
30
+ def handle_row_import(_ending_row_context)
31
+ end
25
32
  end
26
33
  end
@@ -6,15 +6,20 @@ module SolidusImporter
6
6
  customers: {
7
7
  importer: SolidusImporter::BaseImporter,
8
8
  processors: [
9
- SolidusImporter::Processors::Address,
10
9
  SolidusImporter::Processors::Customer,
10
+ SolidusImporter::Processors::CustomerAddress,
11
11
  SolidusImporter::Processors::Log
12
12
  ]
13
13
  },
14
14
  orders: {
15
- importer: SolidusImporter::BaseImporter,
15
+ importer: SolidusImporter::OrderImporter,
16
16
  processors: [
17
17
  SolidusImporter::Processors::Order,
18
+ SolidusImporter::Processors::BillAddress,
19
+ SolidusImporter::Processors::ShipAddress,
20
+ SolidusImporter::Processors::LineItem,
21
+ SolidusImporter::Processors::Shipment,
22
+ SolidusImporter::Processors::Payment,
18
23
  SolidusImporter::Processors::Log
19
24
  ]
20
25
  },
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module SolidusImporter
6
+ class OrderImporter < BaseImporter
7
+ attr_accessor :orders
8
+
9
+ def initialize(options)
10
+ super
11
+ self.orders = {}
12
+ end
13
+
14
+ def handle_row_import(context)
15
+ number = context.dig(:order, :number)
16
+
17
+ return unless number
18
+
19
+ orders[number] ||= {}
20
+
21
+ order_params = context[:order].to_h.reject { |_k, v| v.blank? }
22
+ payments_attributes = order_params[:payments_attributes]
23
+ orders[number][:payments_attributes] ||= []
24
+ orders[number][:payments_attributes] << payments_attributes if payments_attributes.present?
25
+ orders[number].merge!(order_params)
26
+ end
27
+
28
+ def after_import(context)
29
+ orders.each do |_, params|
30
+ user = params.delete(:user)
31
+ SolidusImporter::SpreeCoreImporterOrder.import(user, params)
32
+ rescue StandardError
33
+ context[:success] = false
34
+ end
35
+
36
+ context
37
+ end
38
+ end
39
+ end
@@ -26,7 +26,11 @@ module SolidusImporter
26
26
  initial_context = @importer.before_import(initial_context)
27
27
  unless @import.failed?
28
28
  rows = process_rows(initial_context)
29
- @import.update(state: :completed) if rows.zero?
29
+ ending_context = @importer.after_import(initial_context)
30
+ state = @import.state
31
+ state = :completed if rows.zero?
32
+ state = :failed if ending_context[:success] == false
33
+ @import.update(state: state)
30
34
  end
31
35
  @import
32
36
  end
@@ -21,6 +21,9 @@ module SolidusImporter
21
21
  break
22
22
  end
23
23
  end
24
+
25
+ @importer.handle_row_import(context)
26
+
24
27
  @row.update!(
25
28
  state: context[:success] ? :completed : :failed,
26
29
  messages: context[:messages]
@@ -34,7 +37,6 @@ module SolidusImporter
34
37
  def check_import_finished(context)
35
38
  return unless @row.import.finished?
36
39
 
37
- @importer.after_import(context)
38
40
  @row.import.update!(state: (@row.import.rows.failed.any? ? :failed : :completed))
39
41
  end
40
42
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class BillAddress < Base
6
+ def call(context)
7
+ @data = context[:data]
8
+
9
+ return if @data['Billing Address1'].blank?
10
+
11
+ order = context.fetch(:order, {})
12
+
13
+ order[:bill_address_attributes] = bill_address_attributes
14
+
15
+ context.merge!(order: order)
16
+ end
17
+
18
+ private
19
+
20
+ def country_code
21
+ @data['Billing Country Code']
22
+ end
23
+
24
+ def province_code
25
+ @data['Billing Province Code']
26
+ end
27
+
28
+ def bill_address_attributes
29
+ {
30
+ firstname: @data['Billing First Name'],
31
+ lastname: @data['Billing Last Name'],
32
+ address1: @data['Billing Address1'],
33
+ address2: @data['Billing Address2'],
34
+ city: @data['Billing City'],
35
+ company: @data['Billing Company'],
36
+ zipcode: @data['Billing Zip'],
37
+ phone: @data['Billing Phone'],
38
+ country: country,
39
+ state: state
40
+ }
41
+ end
42
+
43
+ def country
44
+ @country ||= Spree::Country.find_by(iso: country_code) if country_code
45
+ end
46
+
47
+ def state
48
+ @state ||= country&.states&.find_by(abbr: province_code) if province_code
49
+ end
50
+ end
51
+ end
52
+ end
@@ -3,15 +3,11 @@
3
3
  module SolidusImporter
4
4
  module Processors
5
5
  class Customer < Base
6
- attr_accessor :address
7
-
8
6
  def call(context)
9
7
  @data = context.fetch(:data)
10
8
  check_data
11
9
 
12
- self.address = context[:address]
13
-
14
- context.merge!(user: process_user)
10
+ context.merge!(user: process_user && persist_user)
15
11
  end
16
12
 
17
13
  def options
@@ -26,15 +22,22 @@ module SolidusImporter
26
22
  raise SolidusImporter::Exception, 'Missing required key: "Email"' if @data['Email'].blank?
27
23
  end
28
24
 
25
+ def user
26
+ @user ||= prepare_user
27
+ end
28
+
29
29
  def prepare_user
30
30
  Spree::User.find_or_initialize_by(email: @data['Email']) do |u|
31
31
  u.password = options[:password_method].call(u)
32
- u.bill_address = address if address.present?
33
32
  end
34
33
  end
35
34
 
36
35
  def process_user
37
- prepare_user.tap(&:save!)
36
+ user
37
+ end
38
+
39
+ def persist_user
40
+ user.tap(&:save!)
38
41
  end
39
42
  end
40
43
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class CustomerAddress < Base
6
+ def call(context)
7
+ @data = context.fetch(:data)
8
+
9
+ address = Spree::Address.find_or_create_by(address_attributes)
10
+ return unless address.valid?
11
+
12
+ user = context.fetch(:user)
13
+ user.addresses << address
14
+ user.bill_address ||= address
15
+ user.ship_address ||= address
16
+ user.save!
17
+ end
18
+
19
+ private
20
+
21
+ def country
22
+ @country ||= Spree::Country.find_by(iso: @data['Country Code']) if @data['Country Code']
23
+ end
24
+
25
+ def state
26
+ @state ||= country&.states&.find_by(abbr: @data['Province Code']) if @data['Province Code']
27
+ end
28
+
29
+ def address_attributes
30
+ @address_attributes ||= {
31
+ firstname: @data['First Name'],
32
+ lastname: @data['Last Name'],
33
+ address1: @data['Address1'],
34
+ address2: @data['Address2'],
35
+ city: @data['City'],
36
+ zipcode: @data['Zip'],
37
+ phone: @data['Phone'],
38
+ country: country,
39
+ state: state,
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class LineItem < Base
6
+ def call(context)
7
+ @data = context[:data]
8
+
9
+ return if @data['Lineitem sku'].blank?
10
+
11
+ order = context.fetch(:order, {})
12
+
13
+ order[:line_items_attributes] ||= {}
14
+
15
+ index = order[:line_items_attributes].size
16
+
17
+ order[:line_items_attributes][index] = line_items_attributes
18
+
19
+ context.merge!(order: order)
20
+ end
21
+
22
+ private
23
+
24
+ def line_items_attributes
25
+ {
26
+ sku: @data['Lineitem sku'],
27
+ quantity: @data['Lineitem quantity'],
28
+ price: @data['Lineitem price']
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -6,7 +6,7 @@ module SolidusImporter
6
6
  def call(context)
7
7
  @data = context.fetch(:data)
8
8
  check_data
9
- context.merge!(order: process_order)
9
+ context.merge!(order: order_attributes)
10
10
  end
11
11
 
12
12
  def options
@@ -21,19 +21,48 @@ module SolidusImporter
21
21
  raise SolidusImporter::Exception, 'Missing required key: "Name"' if @data['Name'].blank?
22
22
  end
23
23
 
24
- def prepare_order
25
- Spree::Order.find_or_initialize_by(number: @data['Name']) do |order|
26
- order.store = options[:store]
27
- end.tap do |order|
28
- # Apply the row attributes
29
- order.currency = @data['Currency'] unless @data['Currency'].nil?
30
- order.email = @data['Email'] unless @data['Email'].nil?
31
- order.special_instructions = @data['Note'] unless @data['Note'].nil?
32
- end
24
+ def completed_at
25
+ processed_at = @data['Processed At']
26
+ processed_at ? Time.parse(processed_at).in_time_zone : Time.current
27
+ rescue ArgumentError
28
+ Time.current
33
29
  end
34
30
 
35
- def process_order
36
- prepare_order.tap(&:save!)
31
+ def currency
32
+ @data['Currency']
33
+ end
34
+
35
+ def email
36
+ @data['Email']
37
+ end
38
+
39
+ def order_attributes
40
+ {
41
+ number: number,
42
+ completed_at: completed_at,
43
+ store: options[:store],
44
+ currency: currency,
45
+ email: email,
46
+ user: user,
47
+ special_instructions: special_instruction,
48
+ line_items_attributes: {},
49
+ bill_address_attributes: {},
50
+ ship_address_attributes: {},
51
+ shipments_attributes: [],
52
+ payments_attributes: []
53
+ }
54
+ end
55
+
56
+ def number
57
+ @data['Name']
58
+ end
59
+
60
+ def special_instruction
61
+ @data['Note']
62
+ end
63
+
64
+ def user
65
+ @user ||= Spree::User.find_by(email: email)
37
66
  end
38
67
  end
39
68
  end