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.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +39 -31
- data/exe/shopify_transporter +33 -2
- data/lib/shopify_transporter/exporters/exporter.rb +103 -0
- data/lib/shopify_transporter/exporters/magento/customer_exporter.rb +44 -0
- data/lib/shopify_transporter/exporters/magento/database_cache.rb +21 -0
- data/lib/shopify_transporter/exporters/magento/database_table_exporter.rb +68 -0
- data/lib/shopify_transporter/exporters/magento/magento_exporter.rb +28 -0
- data/lib/shopify_transporter/exporters/magento/order_exporter.rb +46 -0
- data/lib/shopify_transporter/exporters/magento/product_exporter.rb +129 -0
- data/lib/shopify_transporter/exporters/magento/product_options.rb +98 -0
- data/lib/shopify_transporter/exporters/magento/soap.rb +101 -0
- data/lib/shopify_transporter/exporters/magento/sql.rb +38 -0
- data/lib/shopify_transporter/exporters/magento.rb +5 -0
- data/lib/shopify_transporter/exporters.rb +3 -0
- data/lib/shopify_transporter/generators/generate.rb +4 -4
- data/lib/shopify_transporter/generators/new.rb +2 -2
- data/lib/shopify_transporter/pipeline/magento/order/line_items.rb +7 -5
- data/lib/shopify_transporter/pipeline/magento/product/top_level_attributes.rb +73 -4
- data/lib/shopify_transporter/pipeline/magento/product/top_level_variant_attributes.rb +50 -0
- data/lib/shopify_transporter/pipeline/magento/product/variant_attributes.rb +28 -0
- data/lib/shopify_transporter/pipeline/magento/product/variant_image.rb +50 -0
- data/lib/shopify_transporter/record_builder/product_record_builder.rb +33 -0
- data/lib/shopify_transporter/{record_builder.rb → record_builder/record_builder.rb} +0 -0
- data/lib/shopify_transporter/shopify/attributes_helpers.rb +1 -1
- data/lib/shopify_transporter/shopify/order.rb +1 -1
- data/lib/shopify_transporter/shopify/product.rb +4 -3
- data/lib/shopify_transporter/shopify/record.rb +1 -1
- data/lib/shopify_transporter/version.rb +1 -1
- data/lib/shopify_transporter.rb +15 -7
- data/lib/tasks/factory_bot.rake +1 -1
- data/lib/templates/magento/config.tt +19 -0
- data/shopify_transporter.gemspec +3 -0
- 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
|