solidus_importer 0.1.0 → 0.2.0

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