shopify_transporter 1.0.0 → 2.0.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 (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