solidus_importer 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'line_item'
4
+
5
+ module SolidusImporter
6
+ module Processors
7
+ class Payment < Base
8
+ attr_accessor :order
9
+
10
+ def call(context)
11
+ @data = context.fetch(:data)
12
+
13
+ return unless amount > 0
14
+
15
+ self.order = context.fetch(:order, {})
16
+
17
+ order[:payments_attributes] ||= []
18
+ order[:payments_attributes] << payment_attributes
19
+
20
+ context.merge!(order: order)
21
+ end
22
+
23
+ private
24
+
25
+ def amount
26
+ price * quantity
27
+ end
28
+
29
+ def price
30
+ @data['Lineitem price'].to_f
31
+ end
32
+
33
+ def quantity
34
+ @data['Lineitem quantity'].to_i
35
+ end
36
+
37
+ def financial_status
38
+ @data['Financial Status'].to_s.inquiry
39
+ end
40
+
41
+ def payment_attributes
42
+ {
43
+ payment_method: payment_method.name,
44
+ amount: amount
45
+ }
46
+ end
47
+
48
+ def payment_method
49
+ @payment_method ||= Spree::PaymentMethod.find_or_initialize_by(
50
+ name: 'SolidusImporter PaymentMethod',
51
+ type: 'Spree::PaymentMethod::Check'
52
+ ).tap(&:save)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class ShipAddress < Base
6
+ def call(context)
7
+ @data = context[:data]
8
+
9
+ return if @data['Shipping Address1'].blank?
10
+
11
+ order = context.fetch(:order, {})
12
+
13
+ order[:ship_address_attributes] = ship_address_attributes
14
+
15
+ context.merge!(order: order)
16
+ end
17
+
18
+ private
19
+
20
+ def country_code
21
+ @data['Shipping Country Code']
22
+ end
23
+
24
+ def province_code
25
+ @data['Shipping Province Code']
26
+ end
27
+
28
+ def ship_address_attributes
29
+ {
30
+ firstname: @data['Shipping First Name'],
31
+ lastname: @data['Shipping Last Name'],
32
+ address1: @data['Shipping Address1'],
33
+ address2: @data['Shipping Address2'],
34
+ city: @data['Shipping City'],
35
+ company: @data['Shipping Company'],
36
+ zipcode: @data['Shipping Zip'],
37
+ phone: @data['Shipping 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
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusImporter
4
+ module Processors
5
+ class Shipment < Base
6
+ attr_accessor :order
7
+
8
+ def call(context)
9
+ @data = context.fetch(:data)
10
+
11
+ self.order = context.fetch(:order, {})
12
+ order[:shipments_attributes] ||= []
13
+ order[:shipments_attributes] << shipments_attributes
14
+
15
+ context.merge!(order: order)
16
+ end
17
+
18
+ private
19
+
20
+ def cost
21
+ @data['Shipping Line Price'].to_f
22
+ end
23
+
24
+ def inventory_units
25
+ line_items_attributes.map do |_, line_item|
26
+ { sku: line_item[:sku] }
27
+ end
28
+ end
29
+
30
+ def line_items_attributes
31
+ order.fetch(:line_items_attributes, [])
32
+ end
33
+
34
+ def shipments_attributes
35
+ {
36
+ shipped_at: shipped_at,
37
+ shipping_method: shipping_method.name,
38
+ stock_location: stock_location.name,
39
+ inventory_units: inventory_units,
40
+ cost: cost
41
+ }
42
+ end
43
+
44
+ def shipped_at
45
+ Time.zone.now if fulfillment_status == 'fulfilled'
46
+ end
47
+
48
+ def fulfillment_status
49
+ @data['Lineitem fulfillment status']
50
+ end
51
+
52
+ def shipping_method_name
53
+ @data['Shipping Line Title'] || 'SolidusImporter ShippingMethod'
54
+ end
55
+
56
+ def shipping_method
57
+ Spree::ShippingMethod.find_by(name: shipping_method_name) || create_shipping_method
58
+ end
59
+
60
+ def create_shipping_method
61
+ shipping_method = Spree::ShippingMethod.new(name: shipping_method_name, calculator: calculator)
62
+ shipping_method.shipping_categories << shipping_category
63
+ shipping_method.save!
64
+ shipping_method
65
+ end
66
+
67
+ def calculator
68
+ @calculator ||= Spree::Calculator::FlatRate.find_or_create_by(
69
+ preferences: { amount: 0 }
70
+ )
71
+ end
72
+
73
+ def shipping_category
74
+ Spree::ShippingCategory.find_or_create_by(
75
+ name: 'SolidusImporter ShippingCategory'
76
+ )
77
+ end
78
+
79
+ def stock_location
80
+ @stock_location ||= Spree::StockLocation.find_or_create_by(
81
+ name: 'SolidusImporter StockLocation'
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidusImporter
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -13,8 +13,10 @@ RSpec.describe 'Import from CSV files' do # rubocop:disable RSpec/DescribeClass
13
13
  context 'with a customers source file' do
14
14
  let(:import_file) { solidus_importer_fixture_path('customers.csv') }
15
15
  let(:import_type) { :customers }
16
- let(:csv_file_rows) { 2 }
17
- let(:user_emails) { ['jane.doe01520022060@example.com', 'jane.doe11520022060@example.com'] }
16
+ let(:csv_file_rows) { 4 }
17
+ let(:user_emails) { ['jane.doe@acme.com', 'john.doe@acme.com'] }
18
+ let(:imported_customer) { Spree::User.last }
19
+ let!(:state) { create(:state, abbr: 'ON', country_iso: 'CA') }
18
20
 
19
21
  it 'imports some customers' do
20
22
  expect { import }.to change(Spree::User, :count).by(2)
@@ -22,6 +24,11 @@ RSpec.describe 'Import from CSV files' do # rubocop:disable RSpec/DescribeClass
22
24
  expect(import.state).to eq('completed')
23
25
  expect(Spree::LogEntry).to have_received(:create!).exactly(csv_file_rows).times
24
26
  end
27
+
28
+ it 'import customer with addresses' do
29
+ import
30
+ expect(imported_customer.addresses.reload).not_to be_empty
31
+ end
25
32
  end
26
33
 
27
34
  context 'with a products file' do
@@ -126,13 +133,70 @@ RSpec.describe 'Import from CSV files' do # rubocop:disable RSpec/DescribeClass
126
133
  let(:import_type) { :orders }
127
134
  let(:csv_file_rows) { 4 }
128
135
  let(:order_numbers) { ['#MA-1097', '#MA-1098'] }
129
- let!(:store) { create(:store) }
136
+ let(:product) { create(:product) }
137
+ let!(:state) { create(:state, abbr: 'ON', country_iso: 'CA') }
138
+ let(:imported_order) { Spree::Order.first }
139
+ let(:tax_category) { product.tax_category }
140
+
141
+ before do
142
+ create(:store)
143
+ create(:shipping_method, name: 'Acme Shipping')
144
+ create(:variant, sku: 'a-123', product: product)
145
+ create(:variant, sku: 'a-456', product: product)
146
+ create(:variant, sku: 'b-001', product: product)
147
+ end
130
148
 
131
149
  it 'imports some orders' do
132
- expect { import }.to change(Spree::Order, :count).by(2)
150
+ expect { import }.to change(Spree::Order, :count).from(0).to(2)
133
151
  expect(Spree::Order.where(number: order_numbers).count).to eq(2)
134
152
  expect(import.state).to eq('completed')
135
153
  expect(Spree::LogEntry).to have_received(:create!).exactly(csv_file_rows).times
136
154
  end
155
+
156
+ it 'imports order with line items' do
157
+ import
158
+ expect(imported_order.line_items).not_to be_blank
159
+ end
160
+
161
+ it 'imports an order with bill address' do
162
+ import
163
+ expect(imported_order.bill_address).not_to be_blank
164
+ expect(imported_order.bill_address.state).to eq state
165
+ expect(imported_order.bill_address.country).to eq state.country
166
+ end
167
+
168
+ it 'imports order with ship address' do
169
+ import
170
+ expect(imported_order.ship_address).not_to be_blank
171
+ expect(imported_order.ship_address.state).to eq state
172
+ expect(imported_order.ship_address.country).to eq state.country
173
+ end
174
+
175
+ it 'imports order with shipments' do
176
+ import
177
+ expect(imported_order.shipments).not_to be_blank
178
+ end
179
+
180
+ it 'imports the order with payments' do
181
+ import
182
+ expect(imported_order.payments).not_to be_empty
183
+ expect(imported_order.payment_state).to eq 'paid'
184
+ expect(imported_order.payments.first.state).to eq 'completed'
185
+ expect(imported_order.payment_total).to eq imported_order.payments.first.amount
186
+ end
187
+
188
+ context 'when there is a promotion applicable to the order' do
189
+ let(:zone) { create(:zone, countries: [country]) }
190
+ let(:country) { state.country }
191
+
192
+ before do
193
+ create(:tax_rate, tax_categories: [tax_category], zone: zone)
194
+ end
195
+
196
+ it 'has no taxes by default' do
197
+ import
198
+ expect(imported_order.tax_total).to eq 0
199
+ end
200
+ end
137
201
  end
138
202
  end
@@ -20,34 +20,37 @@ RSpec.describe 'Set up a some processors' do # rubocop:disable RSpec/DescribeCla
20
20
  let(:processor_check_domain) do
21
21
  ->(context) {
22
22
  user = context[:user].reload
23
- (context[:importer].checks ||= []) << user.email.end_with?('@example.com')
23
+ context[:valid] = user.email.end_with?('@acme.com')
24
24
  }
25
25
  end
26
26
  let(:import_source) { create(:solidus_importer_import_customers) }
27
27
  let(:importer_options) do
28
28
  {
29
- importer: CustomImporter,
29
+ importer: importer_class,
30
30
  processors: [processor_create_user, processor_check_domain]
31
31
  }
32
32
  end
33
- let(:importer) { CustomImporter.new(importer_options) }
34
-
35
- before do
36
- stub_const('CustomImporter', SolidusImporter::BaseImporter)
37
- CustomImporter.class_eval do
33
+ let(:importer) { importer_class.new(importer_options) }
34
+ let(:importer_class) do
35
+ Class.new(SolidusImporter::BaseImporter) do
38
36
  attr_accessor :checks
39
37
 
40
- def after_import(ending_context)
41
- ending_context[:importer].checks
38
+ def handle_row_import(ending_context)
39
+ self.checks ||= []
40
+ checks << ending_context[:valid]
42
41
  end
43
42
  end
43
+ end
44
+
45
+ before do
44
46
  importer
45
- allow(CustomImporter).to receive(:new).and_return(importer)
47
+ allow(importer_class).to receive(:new).and_return(importer)
46
48
  allow(importer).to receive(:after_import).and_call_original
47
49
  end
48
50
 
49
51
  it 'creates 2 users and check the result' do
50
52
  expect { process_import }.to change(Spree::User, :count).from(0).to(2)
51
- expect(importer).to have_received(:after_import)
53
+ expect(importer).to have_received(:after_import).once
54
+ expect(importer.checks).to eq [true, nil, nil, true]
52
55
  end
53
56
  end
@@ -1,3 +1,5 @@
1
- First Name,Last Name,Email
2
- Jane,Doe,jane.doe01520022060@example.com
3
- Jane,Doe,jane.doe11520022060@example.com
1
+ First Name,Last Name,Email,Company,Address1,Address2,City,Province,Province Code,Country,Country Code,Zip,Phone,Accepts Marketing,Tags,Note,Tax Exempt,Metafield Namespace,Metafield Key,Metafield Value,Metafield Value Type
2
+ John,Doe,john.doe@acme.com,Acme Warehouse,57 Erb St W,,Waterloo,Ontario,ON,Canada,CA,N2L 6C2,519-555-5555,true,"repeat_customer,member_since_2017",John wrote a very positive review of our product on his blog.,false,global,vip_status,TRUE,string
3
+ ,,john.doe@acme.com,Acme Ltd.,150 Elgin St,Unit 56,Ottawa,Ontario,ON,Canada,CA,K2P 1L4,,,,,,global,email_list,weekly,string
4
+ ,,john.doe@acme.com,,,,,,,,,,,,,,,global,corporate,true,string
5
+ Jane,Doe,jane.doe@acme.com,Acme Inc.,490 Rue de la Gauchetière O,,Montreal,Quebec,QC,Canada,CA,H2Z 0B2,613-555-6666,false,,Tendency to return clothing in size 0.,true,global,frequent_returner,TRUE,string
@@ -1,5 +1,5 @@
1
1
  Name,Email,Financial Status,Fulfillment Status,Currency,Buyer Accepts Marketing,Cancel Reason,Cancelled At,Closed At,Tags,Note,Phone,Referring Site,Processed At,Source name,Total weight,Total Tax,Shipping Company,Shipping Name,Shipping Phone,Shipping First Name,Shipping Last Name,Shipping Address1,Shipping Address2,Shipping City,Shipping Province,Shipping Province Code,Shipping Zip,Shipping Country,Shipping Country Code,Billing Company,Billing Name,Billing Phone,Billing First Name,Billing Last Name,Billing Address1,Billing Address2,Billing City,Billing Province,Billing Province Code,Billing Zip,Billing Country,Billing Country Code,Lineitem name,Lineitem quantity,Lineitem price,Lineitem sku,Lineitem requires shipping,Lineitem taxable,Lineitem fulfillment status,Tax 1 Title,Tax 1 Price,Tax 1 Rate,Tax 2 Title,Tax 2 Price,Tax 2 Rate,Tax 3 Title,Tax 3 Price,Tax 3 Rate,Transaction amount,Transaction kind,Transaction status,Shipping line code,Shipping line price,Shipping line title,Shipping line carrier identifier,Shipping Tax Price,Discount code,Discount amount,Discount type,Metafield Namespace,Metafield Key,Metafield Value,Metafield Value Type
2
2
  #MA-1097,john.doe@acme.com,paid,unfulfilled,USD,true,,,,,"Review verified, discount code e-mailed",555 555-5555,http://acme-influencers.com,2018-01-04 15:51:23 -0800,,,,Acme Warehouse,Receiver Joe,555.555.5556,Receiver,Joe,57 Erb St W,,Waterloo,Ontario,ON,N2L 6C2,Canada,CA,Acme Ltd.,Ami Shaperi,555.555.5557,Ami,Shaperi,150 Elgin St,Unit 56,Ottawa,Ontario,ON,K2P 1L4,Canaada,CA,Garden Trowel - Composite / Small,2,10,a-123,true,true,fulfilled,HST,2.60,0.13,,,,,,,1000,sale,success,,,,,,25OFF,10,fixed_amount,instructions,wash,cold_water,string
3
- #MA-1097,john.doe@acme.com,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Acme Ltd.,,,,,,,,,,,,,Mini Handle Shovel,3,20,a-456,true,true,fulfilled,HST,7.80,0.13,,,,,,500,refund,success,,,,,,,,,instructions,dry,tumble_dry,string
3
+ #MA-1097,john.doe@acme.com,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Acme Ltd.,,,,,,,,,,,,Mini Handle Shovel,3,20,a-456,true,true,fulfilled,HST,7.80,0.13,,,,,,,500,refund,success,,,,,,,,,instructions,dry,tumble_dry,string
4
4
  #MA-1097,john.doe@acme.com,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,United Parcel Service - 2nd Day Air,0,United Parcel Service - 2nd Day Air,ups_2DA,,,,,instructions,dry,tumble_dry,string
5
- #MA-1098,jane.doe@acme.com,refunded,unfulfilled,USD,false,customer changed/cancelled order,2018-01-19 12:54:42 -0800,,,,,,2018-01-19 12:54:41 -0800,,,,,,,,,,,,,,,,,Acme Inc.,,,,,,,,,,,,,Big Brown Bear Boots,1,30,b-001,true,true,null,,,,,,,,,,,,,,,,,,,,,origin,country,China,string
5
+ #MA-1098,jane.doe@acme.com,refunded,unfulfilled,USD,false,customer changed/cancelled order,2018-01-19 12:54:42 -0800,,,,,,2018-01-19 12:54:41 -0800,,,,,,,,,,,,,,,,,Acme Inc.,,,,,,,,,,,,,Big Brown Bear Boots,1,30,b-001,true,true,null,,,,,,,,,,,,,,,,,,,,,origin,country,China,string
@@ -9,6 +9,16 @@ RSpec.describe SolidusImporter::BaseImporter do
9
9
 
10
10
  describe '#after_import' do
11
11
  it { is_expected.to respond_to(:after_import) }
12
+
13
+ context 'when ending contexts of rows is a success' do
14
+ subject { described_instance.after_import(context) }
15
+
16
+ let(:context) { { success: true } }
17
+
18
+ it 'returns a successful context' do
19
+ is_expected.to match(hash_including(success: true))
20
+ end
21
+ end
12
22
  end
13
23
 
14
24
  describe '#before_import' do
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SolidusImporter::OrderImporter do
6
+ subject(:described_instance) { described_class.new(options) }
7
+
8
+ let(:options) { {} }
9
+
10
+ describe '#after_import' do
11
+ subject(:ending_context) { described_instance.after_import(context) }
12
+
13
+ let(:context) { { success: true } }
14
+
15
+ context 'when there are orders attributes' do
16
+ let(:orders) do
17
+ {
18
+ '#001' => {
19
+ number: '#001',
20
+ email: 'email@example.com'
21
+ },
22
+ '#002' => {
23
+ number: '#002',
24
+ email: 'email@example.com'
25
+ }
26
+ }
27
+ end
28
+
29
+ before do
30
+ described_instance.orders = orders
31
+ allow(SolidusImporter::SpreeCoreImporterOrder).to receive(:import)
32
+ end
33
+
34
+ it 'calls Solidus order importer with those params' do
35
+ expect(ending_context).to be_an_instance_of(Hash)
36
+ expect(SolidusImporter::SpreeCoreImporterOrder).to have_received(:import)
37
+ .with(nil, hash_including(email: 'email@example.com'))
38
+ .exactly(orders.size).times
39
+ end
40
+
41
+ context 'when something went wrong during import' do
42
+ before do
43
+ allow(SolidusImporter::SpreeCoreImporterOrder).to receive(:import).and_raise(StandardError)
44
+ end
45
+
46
+ it 'finish #after_import regardless of the error' do
47
+ expect { subject }.not_to raise_error
48
+ expect(ending_context).to match(hash_including(success: false))
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '#before_import' do
55
+ it { is_expected.to respond_to(:before_import) }
56
+ end
57
+
58
+ describe '#processors' do
59
+ subject(:described_method) { described_instance.processors }
60
+
61
+ it { is_expected.to be_empty }
62
+ end
63
+ end