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