solidus_importer 0.1.0

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