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,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :solidus_importer_import_customers, class: 'SolidusImporter::Import' do
5
+ import_type { :customers }
6
+
7
+ after(:build) do |import|
8
+ import.import_file = solidus_importer_fixture_path('customers.csv')
9
+ end
10
+ end
11
+
12
+ factory :solidus_importer_import_orders, class: 'SolidusImporter::Import' do
13
+ import_type { :orders }
14
+
15
+ after(:build) do |import|
16
+ import.import_file = solidus_importer_fixture_path('orders.csv')
17
+ end
18
+ end
19
+
20
+ factory :solidus_importer_import_products, class: 'SolidusImporter::Import' do
21
+ import_type { :products }
22
+
23
+ after(:build) do |import|
24
+ import.import_file = solidus_importer_fixture_path('products.csv')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :solidus_importer_row_address, class: 'SolidusImporter::Row' do
5
+ data do
6
+ {
7
+ 'First Name' => 'John',
8
+ 'Last Name' => 'Doe',
9
+ 'Address1' => 'My Cool Address, n.1',
10
+ 'Address2' => '',
11
+ 'City' => 'My beautiful City',
12
+ 'Zip' => '12345',
13
+ 'Phone' => '(555)-123123123',
14
+ 'Country Code' => 'US',
15
+ 'Province Code' => 'WA'
16
+ }
17
+ end
18
+
19
+ trait :with_import do
20
+ after(:build) do |row|
21
+ row.import = build_stubbed(:solidus_importer_import_orders)
22
+ end
23
+ end
24
+ end
25
+
26
+ factory :solidus_importer_row_customer, class: 'SolidusImporter::Row' do
27
+ data do
28
+ { 'Email' => 'an_email@example.com' }.merge(build(:solidus_importer_row_address).data)
29
+ end
30
+
31
+ trait :with_import do
32
+ after(:build) do |row|
33
+ row.import = build_stubbed(:solidus_importer_import_customers)
34
+ end
35
+ end
36
+ end
37
+
38
+ factory :solidus_importer_row_order, class: 'SolidusImporter::Row' do
39
+ data { { 'Name' => 'R123456789', 'Email' => 'an_email@example.com', 'Currency' => 'USD', 'Note' => nil } }
40
+
41
+ trait :with_import do
42
+ after(:build) do |row|
43
+ row.import = build_stubbed(:solidus_importer_import_orders)
44
+ end
45
+ end
46
+ end
47
+
48
+ factory :solidus_importer_row_product, class: 'SolidusImporter::Row' do
49
+ data { { 'Handle' => 'a-product-slug', 'Title' => 'A product name' } }
50
+
51
+ trait :with_import do
52
+ after(:build) do |row|
53
+ row.import = build_stubbed(:solidus_importer_import_products)
54
+ end
55
+ end
56
+
57
+ trait :with_tags do
58
+ after(:build) { |row| row.data.merge!('Tags' => 'Tag1,Tag2, Tag3') }
59
+ end
60
+
61
+ trait :with_type do
62
+ after(:build) { |row| row.data.merge!('Type' => 'Type1') }
63
+ end
64
+ end
65
+
66
+ factory :solidus_importer_row_variant, class: 'SolidusImporter::Row' do
67
+ data do
68
+ {
69
+ 'Handle' => 'a-product-slug-2',
70
+ 'Variant SKU' => 'a-variant-sku',
71
+ 'Variant Weight' => 20.0,
72
+ 'Variant Price' => 60.5
73
+ }
74
+ end
75
+
76
+ trait :with_import do
77
+ after(:build) do |row|
78
+ row.import = build_stubbed(:solidus_importer_import_products)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Import rows', type: :feature do
6
+ stub_authorization!
7
+
8
+ describe 'Imports show' do
9
+ subject(:described_path) do
10
+ spree.admin_solidus_importer_import_import_row_path(import_id: import.id, id: row.id)
11
+ end
12
+
13
+ let(:import) { build_stubbed(:solidus_importer_import_products) }
14
+ let(:row) { build_stubbed(:solidus_importer_row_product, import: import) }
15
+ let(:log_details) { '{"success":true,"id":2,"class_name":"Spree::Order","new_record":true}' }
16
+ let(:log_entry) { instance_double(Spree::LogEntry, source: row, details: log_details) }
17
+
18
+ before do
19
+ allow(SolidusImporter::Row).to receive(:find_by!).and_return(row)
20
+ allow(Spree::LogEntry).to receive(:find_by).and_return(log_entry)
21
+ visit described_path
22
+ end
23
+
24
+ it 'loads the import row details' do
25
+ expect(page).to have_css('[data-hook="admin_solidus_importer_import_rows_show"]')
26
+ expect(page).to have_css('.import_row_attribute', count: row.data.keys.size)
27
+ expect(page.body).to include(row.state)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Imports', type: :feature do
6
+ stub_authorization!
7
+
8
+ describe 'New import form' do
9
+ subject(:described_path) { spree.new_admin_solidus_importer_import_path }
10
+
11
+ before { visit described_path }
12
+
13
+ let(:import_file) { page.find 'input[name="solidus_importer_import[file]"]' }
14
+ let(:import_button) { page.find '[type="submit"]' }
15
+
16
+ it 'display import form with available types and "import!" button' do
17
+ expect(import_file).to be_visible
18
+ expect(page).to have_css('select[name="solidus_importer_import[import_type]"] > option', count: 4)
19
+ expect(import_button).to be_visible
20
+ end
21
+ end
22
+
23
+ describe 'Import create' do
24
+ subject(:described_path) { spree.new_admin_solidus_importer_import_path }
25
+
26
+ let!(:shipping_method) { create :shipping_method }
27
+
28
+ let(:import_file) { page.find 'input[name="solidus_importer_import[file]"]' }
29
+ let(:import_button) { page.find '[type="submit"]' }
30
+ let(:products_csv_file) { solidus_importer_fixture_path('products.csv') }
31
+ let(:import_type) { "products" }
32
+
33
+ before do
34
+ allow(::SolidusImporter::ImportJob).to receive(:perform_later)
35
+
36
+ visit described_path
37
+ import_file.set products_csv_file
38
+ select import_type, from: :solidus_importer_import_import_type if import_type
39
+ import_button.click
40
+ end
41
+
42
+ it 'creates a new import' do
43
+ expect(::SolidusImporter::ImportJob).to have_received(:perform_later)
44
+ expect(page).to have_content('Import has been successfully created!')
45
+ end
46
+
47
+ context 'when no import type is selected' do
48
+ let(:import_type) { nil }
49
+
50
+ it 'fails creating a new import' do
51
+ expect(page).to have_content("Import type can't be blank")
52
+ end
53
+ end
54
+
55
+ context 'when the wrong import type is selected', skip: 'Not implemented yet' do
56
+ let(:import_type) { "orders" }
57
+
58
+ it 'fails creating a new import' do
59
+ expect(page).to have_content('Import failed!')
60
+ end
61
+ end
62
+
63
+ context 'when an invalid CSV file is selected', skip: 'Not implemented yet' do
64
+ let(:products_csv_file) { 'unknow-file.csv' }
65
+
66
+ it 'fails creating a new import' do
67
+ expect(page).to have_content('Import failed!')
68
+ end
69
+ end
70
+ end
71
+
72
+ describe 'Imports index' do
73
+ subject(:described_path) { spree.admin_solidus_importer_imports_path }
74
+
75
+ let(:imports) {}
76
+
77
+ before do
78
+ imports
79
+ visit described_path
80
+ end
81
+
82
+ it 'loads the imports listing' do
83
+ expect(page.body).to include('No Import found')
84
+ end
85
+
86
+ context 'with some imports' do
87
+ let(:imports) do
88
+ [
89
+ create(:solidus_importer_import_customers),
90
+ create(:solidus_importer_import_orders),
91
+ create(:solidus_importer_import_products)
92
+ ]
93
+ end
94
+
95
+ it { expect(page).to have_css('.solidus_importer_import', count: imports.size) }
96
+ end
97
+ end
98
+
99
+ describe 'Imports show' do
100
+ subject(:described_path) { spree.admin_solidus_importer_import_path(import) }
101
+
102
+ let(:import) { create(:solidus_importer_import_customers) }
103
+ let(:rows) { [] }
104
+
105
+ before do
106
+ rows
107
+ visit described_path
108
+ end
109
+
110
+ it 'loads the imports details' do
111
+ expect(page).to have_css('[data-hook="admin_solidus_importer_imports_show"]')
112
+ expect(page.body).to include('No Row found')
113
+ end
114
+
115
+ context 'with some rows' do
116
+ let(:rows) { create_list(:solidus_importer_row_customer, 3, import: import) }
117
+
118
+ it { expect(page).to have_css('.solidus_importer_import_row', count: rows.size) }
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Import from CSV files' do # rubocop:disable RSpec/DescribeClass
6
+ subject(:import) { SolidusImporter::ProcessImport.import_from_file(import_file, import_type) }
7
+
8
+ let(:import_file) {}
9
+ let(:import_type) {}
10
+
11
+ before { allow(Spree::LogEntry).to receive(:create!) }
12
+
13
+ context 'with a customers source file' do
14
+ let(:import_file) { solidus_importer_fixture_path('customers.csv') }
15
+ let(:import_type) { :customers }
16
+ let(:csv_file_rows) { 2 }
17
+ let(:user_emails) { ['jane.doe01520022060@example.com', 'jane.doe11520022060@example.com'] }
18
+
19
+ it 'imports some customers' do
20
+ expect { import }.to change(Spree::User, :count).by(2)
21
+ expect(Spree::User.where(email: user_emails).count).to eq(2)
22
+ expect(import.state).to eq('completed')
23
+ expect(Spree::LogEntry).to have_received(:create!).exactly(csv_file_rows).times
24
+ end
25
+ end
26
+
27
+ context 'with a products file' do
28
+ let(:import_file) { solidus_importer_fixture_path('products.csv') }
29
+ let(:import_type) { :products }
30
+ let(:csv_file_rows) { 7 }
31
+ let(:product_slug) { 'hightop-sports-sneaker' }
32
+ let(:image_url) { 'https://cdn.shopify.com/shopify-marketing_assets/static/tobias-lutke-shopify.jpg' }
33
+ let!(:shipping_category) { create(:shipping_category) }
34
+
35
+ before do
36
+ allow(URI).to receive(:open).and_call_original
37
+ allow(URI).to receive(:open).with(image_url) do
38
+ File.open(solidus_importer_fixture_path('thinking-cat.jpg'))
39
+ end
40
+ end
41
+
42
+ it 'imports some products' do # rubocop:disable RSpec/MultipleExpectations
43
+ expect { import }.to change(Spree::Product, :count).by(1)
44
+ product = Spree::Product.last
45
+ expect(product.variants.count).to eq(3)
46
+ expect(product.slug).to eq(product_slug)
47
+ expect(import.state).to eq('completed')
48
+ expect(product.images).not_to be_empty
49
+ expect(product.option_types.count).to eq 2
50
+ expect(product.variants.sample.option_values.count).to eq 2
51
+ expect(product.variants.sample.images).not_to be_empty
52
+ expect(Spree::Product.last.images).not_to be_empty
53
+ expect(Spree::Variant.last.images).not_to be_empty
54
+ expect(Spree::LogEntry).to have_received(:create!).exactly(csv_file_rows).times
55
+ end
56
+ end
57
+
58
+ context 'with an invalid products file' do
59
+ let(:import_file) { solidus_importer_fixture_path('invalid_product.csv') }
60
+ let(:import_type) { :products }
61
+ let!(:shipping_category) { create(:shipping_category) }
62
+
63
+ it 'fails to import the product' do
64
+ expect { import }.not_to change(Spree::Product, :count)
65
+ expect(import.rows.first.messages).to eq("Validation failed: Name can't be blank")
66
+ end
67
+ end
68
+
69
+ context 'with Shopify Product CSVs' do
70
+ before do
71
+ allow(URI).to receive(:open)
72
+ end
73
+
74
+ context 'with the export from Shopify Product CSVs - Apparel' do
75
+ let(:import_file) { solidus_importer_fixture_path('apparel.csv') }
76
+ let(:import_type) { :products }
77
+ let!(:shipping_category) { create(:shipping_category) }
78
+
79
+ it 'imports a some products and a blue shirt with no variants' do
80
+ expect { import }.to change(Spree::Product, :count).from(0)
81
+ expect(import.state).to eq('completed')
82
+
83
+ product = Spree::Product.find_by(slug: 'ocean-blue-shirt')
84
+
85
+ expect(product.variants.count).to be_zero
86
+ end
87
+ end
88
+
89
+ context 'with the export from Shopify Product CSVs - Jewelry' do
90
+ let(:import_file) { solidus_importer_fixture_path('jewelery.csv') }
91
+ let(:import_type) { :products }
92
+ let!(:shipping_category) { create(:shipping_category) }
93
+
94
+ it 'imports a some products and a clay pot with two variants' do
95
+ expect { import }.to change(Spree::Product, :count).from(0)
96
+ expect(import.state).to eq('completed')
97
+
98
+ product = Spree::Product.find_by(slug: 'gemstone')
99
+
100
+ expect(product.variants.count).to eq 2
101
+ expect(product.variants.find_by(sku: 'gemstone-blue')).to be_present
102
+ expect(product.variants.find_by(sku: 'gemstone-purple')).to be_present
103
+ end
104
+ end
105
+
106
+ context 'with the export from Shopify Product CSVs - Home and Garden' do
107
+ let(:import_file) { solidus_importer_fixture_path('home-and-garden.csv') }
108
+ let(:import_type) { :products }
109
+ let!(:shipping_category) { create(:shipping_category) }
110
+
111
+ it 'imports a some products' do
112
+ expect { import }.to change(Spree::Product, :count).from(0)
113
+ expect(import.state).to eq('completed')
114
+
115
+ product = Spree::Product.find_by(slug: 'clay-plant-pot')
116
+
117
+ expect(product.variants.count).to eq 2
118
+ expect(product.variants.find_by(sku: 'clay-plant-pot-large')).to be_present
119
+ expect(product.variants.find_by(sku: 'clay-plant-pot-regular')).to be_present
120
+ end
121
+ end
122
+ end
123
+
124
+ context 'with a orders file' do
125
+ let(:import_file) { solidus_importer_fixture_path('orders.csv') }
126
+ let(:import_type) { :orders }
127
+ let(:csv_file_rows) { 4 }
128
+ let(:order_numbers) { ['#MA-1097', '#MA-1098'] }
129
+ let!(:store) { create(:store) }
130
+
131
+ it 'imports some orders' do
132
+ expect { import }.to change(Spree::Order, :count).by(2)
133
+ expect(Spree::Order.where(number: order_numbers).count).to eq(2)
134
+ expect(import.state).to eq('completed')
135
+ expect(Spree::LogEntry).to have_received(:create!).exactly(csv_file_rows).times
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Set up a some processors' do # rubocop:disable RSpec/DescribeClass
6
+ subject(:process_import) do
7
+ SolidusImporter::ProcessImport.new(
8
+ import_source,
9
+ importer_options: importer_options
10
+ ).process
11
+ end
12
+
13
+ let(:processor_create_user) do
14
+ ->(context) {
15
+ user = Spree::User.new(email: context[:data]['Email'])
16
+ user.password = 'a very secure password'
17
+ context.merge!(success: user.save, user: user)
18
+ }
19
+ end
20
+ let(:processor_check_domain) do
21
+ ->(context) {
22
+ user = context[:user].reload
23
+ (context[:importer].checks ||= []) << user.email.end_with?('@example.com')
24
+ }
25
+ end
26
+ let(:import_source) { create(:solidus_importer_import_customers) }
27
+ let(:importer_options) do
28
+ {
29
+ importer: CustomImporter,
30
+ processors: [processor_create_user, processor_check_domain]
31
+ }
32
+ end
33
+ let(:importer) { CustomImporter.new(importer_options) }
34
+
35
+ before do
36
+ stub_const('CustomImporter', SolidusImporter::BaseImporter)
37
+ CustomImporter.class_eval do
38
+ attr_accessor :checks
39
+
40
+ def after_import(ending_context)
41
+ ending_context[:importer].checks
42
+ end
43
+ end
44
+ importer
45
+ allow(CustomImporter).to receive(:new).and_return(importer)
46
+ allow(importer).to receive(:after_import).and_call_original
47
+ end
48
+
49
+ it 'creates 2 users and check the result' do
50
+ expect { process_import }.to change(Spree::User, :count).from(0).to(2)
51
+ expect(importer).to have_received(:after_import)
52
+ end
53
+ end