shopify_transporter 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.md +39 -31
  4. data/exe/shopify_transporter +33 -2
  5. data/lib/shopify_transporter/exporters/exporter.rb +103 -0
  6. data/lib/shopify_transporter/exporters/magento/customer_exporter.rb +44 -0
  7. data/lib/shopify_transporter/exporters/magento/database_cache.rb +21 -0
  8. data/lib/shopify_transporter/exporters/magento/database_table_exporter.rb +68 -0
  9. data/lib/shopify_transporter/exporters/magento/magento_exporter.rb +28 -0
  10. data/lib/shopify_transporter/exporters/magento/order_exporter.rb +46 -0
  11. data/lib/shopify_transporter/exporters/magento/product_exporter.rb +129 -0
  12. data/lib/shopify_transporter/exporters/magento/product_options.rb +98 -0
  13. data/lib/shopify_transporter/exporters/magento/soap.rb +101 -0
  14. data/lib/shopify_transporter/exporters/magento/sql.rb +38 -0
  15. data/lib/shopify_transporter/exporters/magento.rb +5 -0
  16. data/lib/shopify_transporter/exporters.rb +3 -0
  17. data/lib/shopify_transporter/generators/generate.rb +4 -4
  18. data/lib/shopify_transporter/generators/new.rb +2 -2
  19. data/lib/shopify_transporter/pipeline/magento/order/line_items.rb +7 -5
  20. data/lib/shopify_transporter/pipeline/magento/product/top_level_attributes.rb +73 -4
  21. data/lib/shopify_transporter/pipeline/magento/product/top_level_variant_attributes.rb +50 -0
  22. data/lib/shopify_transporter/pipeline/magento/product/variant_attributes.rb +28 -0
  23. data/lib/shopify_transporter/pipeline/magento/product/variant_image.rb +50 -0
  24. data/lib/shopify_transporter/record_builder/product_record_builder.rb +33 -0
  25. data/lib/shopify_transporter/{record_builder.rb → record_builder/record_builder.rb} +0 -0
  26. data/lib/shopify_transporter/shopify/attributes_helpers.rb +1 -1
  27. data/lib/shopify_transporter/shopify/order.rb +1 -1
  28. data/lib/shopify_transporter/shopify/product.rb +4 -3
  29. data/lib/shopify_transporter/shopify/record.rb +1 -1
  30. data/lib/shopify_transporter/version.rb +1 -1
  31. data/lib/shopify_transporter.rb +15 -7
  32. data/lib/tasks/factory_bot.rake +1 -1
  33. data/lib/templates/magento/config.tt +19 -0
  34. data/shopify_transporter.gemspec +3 -0
  35. metadata +61 -3
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './customer_exporter.rb'
4
+ require_relative './order_exporter.rb'
5
+ require_relative './product_exporter.rb'
6
+
7
+ module ShopifyTransporter
8
+ module Exporters
9
+ module Magento
10
+ class MissingExporterError < ExportError; end
11
+
12
+ class MagentoExporter
13
+ def self.for(type: nil)
14
+ case type
15
+ when 'customer'
16
+ CustomerExporter
17
+ when 'order'
18
+ OrderExporter
19
+ when 'product'
20
+ ProductExporter
21
+ else
22
+ raise MissingExporterError
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyTransporter
4
+ module Exporters
5
+ module Magento
6
+ class OrderExporter
7
+ def initialize(soap_client: nil, database_adapter: nil)
8
+ @client = soap_client
9
+ @database_adapter = database_adapter
10
+ end
11
+
12
+ def key
13
+ :order_id
14
+ end
15
+
16
+ def export
17
+ base_orders.each do |order|
18
+ yield with_attributes(order)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def with_attributes(base_order)
25
+ base_order.merge(items: info_for(base_order[:increment_id]))
26
+ end
27
+
28
+ def base_orders
29
+ Enumerator.new do |enumerator|
30
+ @client.call_in_batches(method: :sales_order_list, batch_index_column: 'order_id').each do |batch|
31
+ result = batch.body[:sales_order_list_response][:result][:item] || []
32
+ result = [result] unless result.is_a? Array
33
+ result.each { |order| enumerator << order }
34
+ end
35
+ end
36
+ end
37
+
38
+ def info_for(order_increment_id)
39
+ @client
40
+ .call(:sales_order_info, order_increment_id: order_increment_id)
41
+ .body[:sales_order_info_response]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './database_table_exporter.rb'
4
+ require_relative './database_cache.rb'
5
+ require_relative './product_options.rb'
6
+
7
+ module ShopifyTransporter
8
+ module Exporters
9
+ module Magento
10
+ class ProductExporter
11
+ def initialize(soap_client: nil, database_adapter: nil)
12
+ @client = soap_client
13
+ @database_table_exporter = DatabaseTableExporter.new(database_adapter)
14
+ @database_cache = DatabaseCache.new
15
+ @product_options = ProductOptions.new(@database_table_exporter, @database_cache)
16
+ end
17
+
18
+ def key
19
+ :product_id
20
+ end
21
+
22
+ def export
23
+ base_products.each do |product|
24
+ yield with_attributes(product)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def base_products
31
+ Enumerator.new do |enumerator|
32
+ @client.call_in_batches(method: :catalog_product_list, batch_index_column: 'product_id').each do |batch|
33
+ result = batch.body[:catalog_product_list_response][:store_view][:item] || []
34
+ result = [result] unless result.is_a? Array
35
+ with_parent_mappings(result).each { |product| enumerator << product }
36
+ end
37
+ end
38
+ end
39
+
40
+ def product_mappings
41
+ @product_mappings ||= begin
42
+ @database_table_exporter.export_table('catalog_product_relation', 'parent_id')
43
+
44
+ {}.tap do |product_mapping_table|
45
+ @database_cache.table('catalog_product_relation').each do |row|
46
+ product_mapping_table[row['child_id']] = row['parent_id']
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def with_parent_mappings(product_list)
53
+ product_list.map do |product|
54
+ case product[:type]
55
+ when 'simple'
56
+ merge_simple_product_with_parent(product, product_mappings)
57
+ else
58
+ product
59
+ end
60
+ end
61
+ end
62
+
63
+ def merge_simple_product_with_parent(product, product_mappings)
64
+ return product unless product_mappings[product[:product_id]].present?
65
+
66
+ product.merge(parent_id: product_mappings[product[:product_id]])
67
+ end
68
+
69
+ def with_attributes(product)
70
+ product_with_base_attributes = product
71
+ .merge(images: images_attribute(product[:product_id]))
72
+ .merge(info_for(product))
73
+ .merge(tags: product_tags(product[:product_id]))
74
+
75
+ case product[:type]
76
+ when 'simple'
77
+ product_with_base_attributes
78
+ .merge(inventory_quantity: inventory_quantity_for(product[:product_id]))
79
+ .merge(variant_option_values(product_with_base_attributes))
80
+ when 'configurable'
81
+ product_with_base_attributes
82
+ .merge(configurable_product_options(product_with_base_attributes))
83
+ else
84
+ product_with_base_attributes
85
+ end
86
+ end
87
+
88
+ def variant_option_values(simple_product)
89
+ @product_options.shopify_variant_options(simple_product)
90
+ end
91
+
92
+ def configurable_product_options(product)
93
+ @product_options.shopify_option_names(product[:product_id])
94
+ end
95
+
96
+ def info_for(product)
97
+ additional_attributes = @product_options.lowercase_option_names(product[:parent_id]) if product[:parent_id]
98
+ attributes = { 'additional_attributes' => { item: additional_attributes } } if additional_attributes
99
+
100
+ @client
101
+ .call(
102
+ :catalog_product_info,
103
+ product_id: product[:product_id],
104
+ attributes: attributes
105
+ )
106
+ .body[:catalog_product_info_response][:info]
107
+ end
108
+
109
+ def inventory_quantity_for(product_id)
110
+ @client
111
+ .call(:catalog_inventory_stock_item_list, products: { product_id: product_id })
112
+ .body[:catalog_inventory_stock_item_list_response][:result][:item][:qty].to_i
113
+ end
114
+
115
+ def images_attribute(product_id)
116
+ @client
117
+ .call(:catalog_product_attribute_media_list, product: product_id.to_i)
118
+ .body[:catalog_product_attribute_media_list_response][:result][:item]
119
+ end
120
+
121
+ def product_tags(product_id)
122
+ @client
123
+ .call(:catalog_product_tag_list, product_id: product_id.to_i)
124
+ .body[:catalog_product_tag_list_response][:result][:item]
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyTransporter
4
+ module Exporters
5
+ module Magento
6
+ class ProductOptions
7
+ def initialize(database_table_exporter, database_cache)
8
+ @database_table_exporter = database_table_exporter
9
+ @database_cache = database_cache
10
+
11
+ export_required_tables
12
+ end
13
+
14
+ def shopify_option_names(parent_product_id)
15
+ option_names(parent_product_id).each_with_index.with_object({}) do |(option_name, index), obj|
16
+ obj["option#{index + 1}_name".to_sym] = option_name
17
+ end
18
+ end
19
+
20
+ def lowercase_option_names(parent_product_id)
21
+ option_names(parent_product_id).map(&:downcase)
22
+ end
23
+
24
+ def shopify_variant_options(simple_product)
25
+ return {} unless simple_product_has_required_option_keys(simple_product)
26
+
27
+ parent_product_options = lowercase_option_names(simple_product[:parent_id])
28
+ variant_attributes = simple_product[:additional_attributes][:item]
29
+ variant_attributes = [variant_attributes] unless variant_attributes.is_a?(Array)
30
+ parent_product_options.each_with_index.with_object({}) do |(option_name, index), obj|
31
+ option_value_id = fetch_option_value_id(option_name, variant_attributes)
32
+
33
+ obj["option#{index + 1}_name".to_sym] = option_name.capitalize
34
+ obj["option#{index + 1}_value".to_sym] = option_value(option_value_id)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def option_names(product_id)
41
+ option_lookup[product_id] || []
42
+ end
43
+
44
+ def option_value(soap_value_id)
45
+ option_value_lookup[soap_value_id] || nil
46
+ end
47
+
48
+ def fetch_option_value_id(option_name, variant_attributes)
49
+ option_attribute_hash = variant_attributes.select do |attribute|
50
+ attribute[:key] == option_name
51
+ end.first
52
+
53
+ return nil if option_attribute_hash.nil?
54
+ option_attribute_hash[:value]
55
+ end
56
+
57
+ def simple_product_has_required_option_keys(simple_product)
58
+ simple_product.key?(:parent_id) && simple_product.key?(:additional_attributes)
59
+ end
60
+
61
+ def export_required_tables
62
+ @database_table_exporter.export_table('catalog_product_super_attribute', 'product_super_attribute_id')
63
+ @database_table_exporter.export_table('catalog_product_super_attribute_label', 'value_id')
64
+ @database_table_exporter.export_table('eav_attribute_option_value', 'value_id')
65
+ end
66
+
67
+ def option_lookup
68
+ @option_lookup ||= @database_cache
69
+ .table('catalog_product_super_attribute')
70
+ .each_with_object({}) do |attribute, option_hash|
71
+ option_hash[attribute['product_id']] ||= []
72
+ option_hash[attribute['product_id']] << option_label_lookup[attribute['product_super_attribute_id']]
73
+ end
74
+ end
75
+
76
+ def option_label_lookup
77
+ @option_label_lookup ||= @database_cache
78
+ .table('catalog_product_super_attribute_label')
79
+ .each_with_object({}) do |label, label_lookup|
80
+ label_lookup[label['product_super_attribute_id']] = label['value']
81
+ end
82
+ end
83
+
84
+ def option_value_lookup
85
+ @option_value_lookup ||= begin
86
+ soap_value_id_column_key = 'option_id'
87
+
88
+ @database_cache
89
+ .table('eav_attribute_option_value')
90
+ .each_with_object({}) do |option_value, value_lookup|
91
+ value_lookup[option_value[soap_value_id_column_key]] = option_value['value']
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'savon'
5
+
6
+ module ShopifyTransporter
7
+ module Exporters
8
+ module Magento
9
+ class Soap
10
+ RETRY_SLEEP_TIME = 0.1
11
+ MAX_RETRIES = 4
12
+
13
+ def initialize(hostname: '', username: '', api_key: '', batch_config:)
14
+ @hostname = hostname
15
+ @username = username
16
+ @api_key = api_key
17
+ @batch_config = batch_config
18
+ end
19
+
20
+ def call(method, params)
21
+ call_with_retries(method, params)
22
+ end
23
+
24
+ def call_in_batches(method:, params: {}, batch_index_column:)
25
+ current_id = @batch_config['first_id']
26
+ max_id = @batch_config['last_id']
27
+ batch_size = @batch_config['batch_size']
28
+
29
+ Enumerator.new do |enumerator|
30
+ while current_id <= max_id
31
+ end_of_range = current_id + batch_size - 1
32
+ end_of_range = max_id if end_of_range >= max_id
33
+
34
+ $stderr.puts "Processing batch: #{current_id}..#{end_of_range}"
35
+
36
+ enumerator << call(
37
+ method,
38
+ params.merge(batching_filter(current_id, end_of_range, batch_index_column)),
39
+ )
40
+
41
+ current_id += batch_size
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def call_with_retries(method, params, retry_count = 0)
49
+ soap_client.call(
50
+ method,
51
+ message: { session_id: soap_session_id }.merge(params)
52
+ )
53
+ rescue Savon::Error
54
+ raise if retry_count >= MAX_RETRIES
55
+ sleep(RETRY_SLEEP_TIME * (retry_count + 1))
56
+ call_with_retries(method, params, retry_count + 1)
57
+ end
58
+
59
+ def soap_client
60
+ @soap_client ||= Savon.client(
61
+ wsdl: "https://#{@hostname}/api/v2_soap?wsdl",
62
+ open_timeout: 500,
63
+ read_timeout: 500,
64
+ )
65
+ end
66
+
67
+ def soap_session_id
68
+ @soap_session_id ||= soap_client.call(
69
+ :login,
70
+ message: {
71
+ username: @username,
72
+ api_key: @api_key,
73
+ }
74
+ ).body[:login_response][:login_return]
75
+ end
76
+
77
+ def batching_filter(starting_id, ending_id, batch_index_column)
78
+ {
79
+ filters: {
80
+ 'complex_filter' => [
81
+ item: [
82
+ {
83
+ key: batch_index_column,
84
+ value: {
85
+ key: 'in',
86
+ value: range_id_string(starting_id, ending_id),
87
+ },
88
+ },
89
+ ],
90
+ ],
91
+ },
92
+ }
93
+ end
94
+
95
+ def range_id_string(starting_id, ending_id)
96
+ (starting_id..ending_id).to_a.join(',')
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require 'sequel'
3
+
4
+ module ShopifyTransporter
5
+ module Exporters
6
+ module Magento
7
+ class SQL
8
+ def initialize(
9
+ database: '',
10
+ host: '',
11
+ port: 3306,
12
+ user: '',
13
+ password: ''
14
+ )
15
+
16
+ @database = database
17
+ @host = host
18
+ @port = port
19
+ @user = user
20
+ @password = password
21
+ end
22
+
23
+ def connect
24
+ Sequel.connect(
25
+ adapter: :mysql2,
26
+ user: @user,
27
+ password: @password,
28
+ host: @host,
29
+ port: @port,
30
+ database: @database
31
+ ) do |connection|
32
+ yield connection
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end