solidus_importer 0.1.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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +35 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.gitignore +20 -0
  6. data/.hound.yml +8 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +2 -0
  9. data/Gemfile +33 -0
  10. data/LICENSE +26 -0
  11. data/README.md +170 -0
  12. data/Rakefile +6 -0
  13. data/app/assets/javascripts/spree/backend/solidus_importer.js +2 -0
  14. data/app/assets/javascripts/spree/frontend/solidus_importer.js +2 -0
  15. data/app/assets/stylesheets/spree/backend/solidus_importer.css +16 -0
  16. data/app/assets/stylesheets/spree/frontend/solidus_importer.css +4 -0
  17. data/app/controllers/spree/admin/solidus_importer/import_rows_controller.rb +24 -0
  18. data/app/controllers/spree/admin/solidus_importer/imports_controller.rb +44 -0
  19. data/app/jobs/solidus_importer/import_job.rb +30 -0
  20. data/app/models/solidus_importer/import.rb +48 -0
  21. data/app/models/solidus_importer/row.rb +26 -0
  22. data/app/views/spree/admin/solidus_importer/import_rows/show.html.erb +46 -0
  23. data/app/views/spree/admin/solidus_importer/imports/_form.html.erb +14 -0
  24. data/app/views/spree/admin/solidus_importer/imports/index.html.erb +80 -0
  25. data/app/views/spree/admin/solidus_importer/imports/new.html.erb +13 -0
  26. data/app/views/spree/admin/solidus_importer/imports/show.html.erb +87 -0
  27. data/bin/console +17 -0
  28. data/bin/rails +7 -0
  29. data/bin/rails-engine +13 -0
  30. data/bin/rails-sandbox +16 -0
  31. data/bin/rake +7 -0
  32. data/bin/sandbox +84 -0
  33. data/bin/setup +8 -0
  34. data/config/initializers/spree.rb +11 -0
  35. data/config/locales/en.yml +28 -0
  36. data/config/routes.rb +11 -0
  37. data/db/migrate/20191216101011_create_solidus_importer_imports.rb +19 -0
  38. data/db/migrate/20191216101012_create_solidus_importer_rows.rb +14 -0
  39. data/examples/importers/custom_importer.rb +20 -0
  40. data/examples/processors/notify_on_failure.rb +22 -0
  41. data/lib/generators/solidus_importer/install/install_generator.rb +22 -0
  42. data/lib/solidus_importer.rb +25 -0
  43. data/lib/solidus_importer/base_importer.rb +26 -0
  44. data/lib/solidus_importer/configuration.rb +39 -0
  45. data/lib/solidus_importer/engine.rb +23 -0
  46. data/lib/solidus_importer/exception.rb +5 -0
  47. data/lib/solidus_importer/factories.rb +4 -0
  48. data/lib/solidus_importer/process_import.rb +88 -0
  49. data/lib/solidus_importer/process_row.rb +46 -0
  50. data/lib/solidus_importer/processors/address.rb +47 -0
  51. data/lib/solidus_importer/processors/base.rb +15 -0
  52. data/lib/solidus_importer/processors/customer.rb +41 -0
  53. data/lib/solidus_importer/processors/log.rb +15 -0
  54. data/lib/solidus_importer/processors/option_types.rb +43 -0
  55. data/lib/solidus_importer/processors/option_values.rb +49 -0
  56. data/lib/solidus_importer/processors/order.rb +40 -0
  57. data/lib/solidus_importer/processors/product.rb +51 -0
  58. data/lib/solidus_importer/processors/product_images.rb +30 -0
  59. data/lib/solidus_importer/processors/taxon.rb +52 -0
  60. data/lib/solidus_importer/processors/variant.rb +51 -0
  61. data/lib/solidus_importer/processors/variant_images.rb +30 -0
  62. data/lib/solidus_importer/version.rb +5 -0
  63. data/solidus_importer.gemspec +36 -0
  64. data/spec/factories/solidus_importer_imports.rb +27 -0
  65. data/spec/factories/solidus_importer_rows.rb +82 -0
  66. data/spec/features/admin/solidus_importer/import_rows_spec.rb +30 -0
  67. data/spec/features/admin/solidus_importer/imports_spec.rb +121 -0
  68. data/spec/features/solidus_importer/import_spec.rb +138 -0
  69. data/spec/features/solidus_importer/processors_spec.rb +53 -0
  70. data/spec/fixtures/solidus_importer/apparel.csv +23 -0
  71. data/spec/fixtures/solidus_importer/customers.csv +3 -0
  72. data/spec/fixtures/solidus_importer/home-and-garden.csv +22 -0
  73. data/spec/fixtures/solidus_importer/invalid_headers.csv +3 -0
  74. data/spec/fixtures/solidus_importer/invalid_product.csv +2 -0
  75. data/spec/fixtures/solidus_importer/jewelery.csv +55 -0
  76. data/spec/fixtures/solidus_importer/orders.csv +5 -0
  77. data/spec/fixtures/solidus_importer/products.csv +8 -0
  78. data/spec/fixtures/solidus_importer/thinking-cat.jpg +0 -0
  79. data/spec/jobs/solidus_importer/import_job_spec.rb +54 -0
  80. data/spec/lib/solidus_importer/base_importer_spec.rb +23 -0
  81. data/spec/lib/solidus_importer/process_import_spec.rb +74 -0
  82. data/spec/lib/solidus_importer/process_row_spec.rb +24 -0
  83. data/spec/lib/solidus_importer/processors/address_spec.rb +18 -0
  84. data/spec/lib/solidus_importer/processors/base_spec.rb +9 -0
  85. data/spec/lib/solidus_importer/processors/customer_spec.rb +65 -0
  86. data/spec/lib/solidus_importer/processors/log_spec.rb +18 -0
  87. data/spec/lib/solidus_importer/processors/option_types_spec.rb +38 -0
  88. data/spec/lib/solidus_importer/processors/option_values_spec.rb +43 -0
  89. data/spec/lib/solidus_importer/processors/order_spec.rb +56 -0
  90. data/spec/lib/solidus_importer/processors/product_images_spec.rb +42 -0
  91. data/spec/lib/solidus_importer/processors/product_spec.rb +66 -0
  92. data/spec/lib/solidus_importer/processors/taxon_spec.rb +33 -0
  93. data/spec/lib/solidus_importer/processors/variant_images_spec.rb +44 -0
  94. data/spec/lib/solidus_importer/processors/variant_spec.rb +86 -0
  95. data/spec/lib/solidus_importer_spec.rb +14 -0
  96. data/spec/models/solidus_importer/import_spec.rb +60 -0
  97. data/spec/models/solidus_importer/row_spec.rb +18 -0
  98. data/spec/spec_helper.rb +27 -0
  99. data/spec/support/solidus_importer_helpers.rb +13 -0
  100. metadata +227 -0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Base
6
+ def options
7
+ {}
8
+ end
9
+
10
+ class << self
11
+ delegate :call, to: :new
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Customer < Base
6
+ attr_accessor :address
7
+
8
+ def call(context)
9
+ @data = context.fetch(:data)
10
+ check_data
11
+
12
+ self.address = context[:address]
13
+
14
+ context.merge!(user: process_user)
15
+ end
16
+
17
+ def options
18
+ @options ||= {
19
+ password_method: ->(_user) { SecureRandom.alphanumeric(8) }
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def check_data
26
+ raise SolidusImporter::Exception, 'Missing required key: "Email"' if @data['Email'].blank?
27
+ end
28
+
29
+ def prepare_user
30
+ Spree::User.find_or_initialize_by(email: @data['Email']) do |u|
31
+ u.password = options[:password_method].call(u)
32
+ u.bill_address = address if address.present?
33
+ end
34
+ end
35
+
36
+ def process_user
37
+ prepare_user.tap(&:save!)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Log < Base
6
+ def call(context)
7
+ Spree::LogEntry.create!(
8
+ source_id: context[:row_id],
9
+ source_type: 'SolidusImporter::Row',
10
+ details: context.except(:importer, :data).to_json
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class OptionTypes < Base
6
+ def call(context)
7
+ @data = context.fetch(:data)
8
+ return unless option_types?
9
+
10
+ product = context.fetch(:product)
11
+ context.merge!(option_types: process_option_types(product))
12
+ end
13
+
14
+ private
15
+
16
+ def process_option_types(product)
17
+ option_type_names.each_with_index.map do |name, i|
18
+ option_type = product.option_types.find_or_initialize_by(
19
+ name: name
20
+ )
21
+ option_type.presentation = name
22
+ option_type.name = name.downcase
23
+ option_type.position = i + 1
24
+ option_type.save
25
+ end
26
+ end
27
+
28
+ def option_type_names
29
+ @option_type_names ||= @data.values_at(
30
+ 'Option1 Name',
31
+ 'Option2 Name',
32
+ 'Option3 Name'
33
+ ).compact
34
+ end
35
+
36
+ def option_types?
37
+ # NOTE: according to https://help.shopify.com/en/manual/products/import-export
38
+ # when `Option Name1` is equal to 'Title`, means that product has no variants.
39
+ (@data['Option1 Name'] != 'Title') && option_type_names.any?
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class OptionValues < Base
6
+ attr_accessor :option_types, :variant
7
+
8
+ def call(context)
9
+ @data = context.fetch(:data)
10
+ return unless option_values?
11
+
12
+ self.variant = context.fetch(:variant)
13
+ process_option_values
14
+ end
15
+
16
+ private
17
+
18
+ def process_option_values
19
+ option_value_names.each_with_index do |name, i|
20
+ option_value = Spree::OptionValue.find_or_initialize_by(
21
+ option_type: option_type(i),
22
+ name: name
23
+ )
24
+ option_value.presentation = name
25
+ variant.option_values << option_value
26
+ end
27
+ end
28
+
29
+ def option_type(index)
30
+ variant.product.option_types.find { |ot| ot.position == index + 1 }
31
+ end
32
+
33
+ def option_value_names
34
+ @option_value_names ||= @data.values_at(
35
+ 'Option1 Value',
36
+ 'Option2 Value',
37
+ 'Option3 Value'
38
+ ).compact
39
+ end
40
+
41
+ def option_values?
42
+ # NOTE: according to https://help.shopify.com/en/manual/products/import-export
43
+ # when `Option Value1` is equal to 'Default Title`, means that product has no
44
+ # variants.
45
+ (@data['Option1 Value'] != 'Default Title') && option_value_names.any?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Order < Base
6
+ def call(context)
7
+ @data = context.fetch(:data)
8
+ check_data
9
+ context.merge!(order: process_order)
10
+ end
11
+
12
+ def options
13
+ @options ||= {
14
+ store: Spree::Store.default
15
+ }
16
+ end
17
+
18
+ private
19
+
20
+ def check_data
21
+ raise SolidusImporter::Exception, 'Missing required key: "Name"' if @data['Name'].blank?
22
+ end
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
33
+ end
34
+
35
+ def process_order
36
+ prepare_order.tap(&:save!)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Product < Base
6
+ def call(context)
7
+ @data = context.fetch(:data)
8
+ check_data
9
+ context.merge!(product: process_product)
10
+ end
11
+
12
+ def options
13
+ @options ||= {
14
+ available_on: Date.current.yesterday,
15
+ not_available: nil,
16
+ price: 0,
17
+ shipping_category: Spree::ShippingCategory.find_by(name: 'Default') || Spree::ShippingCategory.first
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def check_data
24
+ raise SolidusImporter::Exception, 'Missing required key: "Handle"' if @data['Handle'].blank?
25
+ end
26
+
27
+ def prepare_product
28
+ Spree::Product.find_or_initialize_by(slug: @data['Handle'])
29
+ end
30
+
31
+ def process_product
32
+ prepare_product.tap do |product|
33
+ product.slug = @data['Handle']
34
+ product.price = options[:price]
35
+ product.available_on = available? ? options[:available_on] : options[:not_available]
36
+ product.shipping_category = options[:shipping_category]
37
+
38
+ # Apply the row attributes
39
+ product.name = @data['Title'] unless @data['Title'].nil?
40
+
41
+ # Save the product
42
+ product.save!
43
+ end
44
+ end
45
+
46
+ def available?
47
+ @data['Published'] == 'true'
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class ProductImages < Base
6
+ def call(context)
7
+ @data = context.fetch(:data)
8
+ return unless product_image?
9
+
10
+ product = context.fetch(:product)
11
+ process_images(product)
12
+ end
13
+
14
+ private
15
+
16
+ def prepare_image
17
+ attachment = URI.open(@data['Image Src'])
18
+ Spree::Image.new(attachment: attachment, alt: @data['Alt Text'])
19
+ end
20
+
21
+ def process_images(product)
22
+ product.images << prepare_image
23
+ end
24
+
25
+ def product_image?
26
+ @product_image ||= @data['Image Src'].present?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Taxon < Base
6
+ attr_accessor :product, :taxonomy
7
+
8
+ def call(context)
9
+ @data = context.fetch(:data)
10
+
11
+ self.product = context.fetch(:product)
12
+
13
+ process_taxons_type
14
+ process_taxons_tags
15
+ end
16
+
17
+ private
18
+
19
+ def options
20
+ @options ||= {
21
+ type_taxonomy: Spree::Taxonomy.find_or_create_by(name: 'Type'),
22
+ tags_taxonomy: Spree::Taxonomy.find_or_create_by(name: 'Tags')
23
+ }
24
+ end
25
+
26
+ def process_taxons_type
27
+ product.taxons << prepare_taxon(type, options[:type_taxonomy])
28
+ end
29
+
30
+ def process_taxons_tags
31
+ tags.map do |tag|
32
+ product.taxons << prepare_taxon(tag, options[:tags_taxonomy])
33
+ end
34
+ end
35
+
36
+ def prepare_taxon(name, taxonomy)
37
+ Spree::Taxon.find_or_initialize_by(
38
+ name: name,
39
+ taxonomy_id: taxonomy.id
40
+ )
41
+ end
42
+
43
+ def tags
44
+ @data['Tags'].split(',').map(&:strip)
45
+ end
46
+
47
+ def type
48
+ @data['Type']
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Variant < Base
6
+ attr_accessor :product
7
+
8
+ def call(context)
9
+ @data = context.fetch(:data)
10
+ self.product = context.fetch(:product) || raise(ArgumentError, 'missing :product in context')
11
+
12
+ context.merge!(variant: process_variant)
13
+ end
14
+
15
+ private
16
+
17
+ def prepare_variant
18
+ return product.master if master_variant?
19
+
20
+ @prepare_variant ||= Spree::Variant.find_or_initialize_by(sku: sku) do |variant|
21
+ variant.product = product
22
+ end
23
+ end
24
+
25
+ def process_variant
26
+ prepare_variant.tap do |variant|
27
+ # Apply the row attributes
28
+ variant.weight = @data['Variant Weight'] unless @data['Variant Weight'].nil?
29
+ variant.price = @data['Variant Price'] if @data['Variant Price'].present?
30
+
31
+ # Save the product
32
+ variant.save!
33
+ end
34
+ end
35
+
36
+ def master_variant?
37
+ ov1 = @data['Option1 Value']
38
+ ov1.blank? || ov1 == 'Default Title'
39
+ end
40
+
41
+ def sku
42
+ @data['Variant SKU'] || generate_sku
43
+ end
44
+
45
+ def generate_sku
46
+ variant_part = @data['Option1 Value'].parameterize
47
+ "#{product.slug}-#{variant_part}"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class VariantImages < Base
6
+ def call(context)
7
+ @data = context.fetch(:data)
8
+ return unless variant_image?
9
+
10
+ variant = context.fetch(:variant)
11
+ process_images(variant)
12
+ end
13
+
14
+ private
15
+
16
+ def prepare_image
17
+ attachment = URI.open(@data['Variant Image'])
18
+ Spree::Image.new(attachment: attachment)
19
+ end
20
+
21
+ def process_images(variant)
22
+ variant.images << prepare_image
23
+ end
24
+
25
+ def variant_image?
26
+ @variant_image ||= @data['Variant Image'].present?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/solidus_importer/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'solidus_importer'
7
+ spec.version = SolidusImporter::VERSION
8
+ spec.authors = ['Mattia Roccoberton']
9
+ spec.email = 'contact@solidus.io'
10
+
11
+ spec.summary = 'Solidus Importer extension'
12
+ spec.description = 'Solidus Importer extension to migrate data from other eCommerce systems'
13
+ spec.homepage = 'https://github.com/nebulab/solidus_importer#readme'
14
+ spec.license = 'BSD-3-Clause'
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = 'https://github.com/nebulab/solidus_importer#readme'
18
+ spec.metadata['changelog_uri'] = 'https://github.com/nebulab/solidus_importer/releases'
19
+
20
+ spec.required_ruby_version = Gem::Requirement.new('~> 2.5')
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
25
+
26
+ spec.files = files.grep_v(%r{^(test|spec|features)/})
27
+ spec.test_files = files.grep(%r{^(test|spec|features)/})
28
+ spec.bindir = "exe"
29
+ spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 3']
33
+ spec.add_dependency 'solidus_support', '~> 0.5'
34
+
35
+ spec.add_development_dependency 'solidus_dev_support'
36
+ end