shopify_transporter 1.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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +23 -0
  3. data/LICENSE +20 -0
  4. data/README.md +290 -0
  5. data/RELEASING +16 -0
  6. data/Rakefile +11 -0
  7. data/exe/shopify_transporter +47 -0
  8. data/lib/shopify_transporter/generators/base_group.rb +25 -0
  9. data/lib/shopify_transporter/generators/generate.rb +64 -0
  10. data/lib/shopify_transporter/generators/new.rb +23 -0
  11. data/lib/shopify_transporter/generators.rb +3 -0
  12. data/lib/shopify_transporter/pipeline/all_platforms/metafields.rb +61 -0
  13. data/lib/shopify_transporter/pipeline/magento/customer/addresses_attribute.rb +60 -0
  14. data/lib/shopify_transporter/pipeline/magento/customer/top_level_attributes.rb +47 -0
  15. data/lib/shopify_transporter/pipeline/magento/order/addresses_attribute.rb +46 -0
  16. data/lib/shopify_transporter/pipeline/magento/order/line_items.rb +55 -0
  17. data/lib/shopify_transporter/pipeline/magento/order/top_level_attributes.rb +39 -0
  18. data/lib/shopify_transporter/pipeline/magento/product/top_level_attributes.rb +36 -0
  19. data/lib/shopify_transporter/pipeline/stage.rb +16 -0
  20. data/lib/shopify_transporter/pipeline.rb +4 -0
  21. data/lib/shopify_transporter/record_builder.rb +51 -0
  22. data/lib/shopify_transporter/shopify/attributes_accumulator.rb +43 -0
  23. data/lib/shopify_transporter/shopify/attributes_helpers.rb +53 -0
  24. data/lib/shopify_transporter/shopify/customer.rb +79 -0
  25. data/lib/shopify_transporter/shopify/order.rb +107 -0
  26. data/lib/shopify_transporter/shopify/product.rb +118 -0
  27. data/lib/shopify_transporter/shopify/record.rb +60 -0
  28. data/lib/shopify_transporter/shopify.rb +7 -0
  29. data/lib/shopify_transporter/version.rb +4 -0
  30. data/lib/shopify_transporter.rb +247 -0
  31. data/lib/tasks/factory_bot.rake +9 -0
  32. data/lib/templates/custom_stage.tt +31 -0
  33. data/lib/templates/gemfile.tt +5 -0
  34. data/lib/templates/magento/config.tt +23 -0
  35. data/shopify_transporter.gemspec +36 -0
  36. metadata +152 -0
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require 'shopify_transporter/pipeline/stage'
3
+ require 'shopify_transporter/shopify'
4
+
5
+ module ShopifyTransporter
6
+ module Pipeline
7
+ module Magento
8
+ module Customer
9
+ class TopLevelAttributes < Pipeline::Stage
10
+ def convert(hash, record)
11
+ accumulator = TopLevelAttributesAccumulator.new(record)
12
+ accumulator.accumulate(hash)
13
+ record.merge!(accumulator.output)
14
+ end
15
+
16
+ class TopLevelAttributesAccumulator < Shopify::AttributesAccumulator
17
+ COLUMN_MAPPING = {
18
+ 'firstname' => 'first_name',
19
+ 'lastname' => 'last_name',
20
+ 'email' => 'email',
21
+ }
22
+
23
+ private
24
+
25
+ def input_applies?(_input)
26
+ true
27
+ end
28
+
29
+ def attributes_from(input)
30
+ attributes = COLUMN_MAPPING.each_with_object({}) do |(key, value), obj|
31
+ obj[value] = input[key] if input[key]
32
+ end
33
+ attributes['accepts_marketing'] = accepts_marketing(input)
34
+ attributes
35
+ end
36
+
37
+ def accepts_marketing(input)
38
+ value = input['is_subscribed']
39
+ return unless value.present?
40
+ (value == '1').to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require 'shopify_transporter/pipeline/stage'
3
+ require 'shopify_transporter/shopify'
4
+ module ShopifyTransporter
5
+ module Pipeline
6
+ module Magento
7
+ module Order
8
+ class AddressesAttribute < Pipeline::Stage
9
+ BILLING_PREFIX = 'billing_'
10
+ SHIPPING_PREFIX = 'shipping_'
11
+
12
+ def convert(input, record)
13
+ record['billing_address'] = address_attributes(input, BILLING_PREFIX)
14
+ record['shipping_address'] = address_attributes(input, SHIPPING_PREFIX)
15
+ record
16
+ end
17
+
18
+ private
19
+
20
+ def get_nested_address(input, prefix)
21
+ items = input['items']
22
+ result = items && items['result']
23
+ address = result && result["#{prefix}address"]
24
+ address || []
25
+ end
26
+
27
+ def address_attributes(input, prefix)
28
+ address_attrs = get_nested_address(input, prefix)
29
+ return address_attrs unless address_attrs.present?
30
+
31
+ {
32
+ first_name: address_attrs['firstname'],
33
+ last_name: address_attrs['lastname'],
34
+ phone: address_attrs['telephone'],
35
+ address1: address_attrs['street'],
36
+ city: address_attrs['city'],
37
+ province_code: address_attrs['region'],
38
+ zip: address_attrs['postcode'],
39
+ country_code: address_attrs['country_id'],
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ require 'shopify_transporter/pipeline/stage'
3
+ require 'shopify_transporter/shopify'
4
+
5
+ module ShopifyTransporter
6
+ module Pipeline
7
+ module Magento
8
+ module Order
9
+ class LineItems < Pipeline::Stage
10
+ def convert(input, record)
11
+ record.merge!(
12
+ {
13
+ line_items: line_items(input),
14
+ }.stringify_keys
15
+ )
16
+ end
17
+
18
+ private
19
+
20
+ def line_items(input)
21
+ line_items = line_items_array(input)
22
+ line_items.map { |item| line_item(item) }
23
+ end
24
+
25
+ def line_items_array(input)
26
+ items_1 = input['items']
27
+ result = items_1 && items_1['result']
28
+ items_2 = result && result['items']
29
+ item = items_2 && items_2['item']
30
+ return [] unless item
31
+ item.is_a?(Array) ? item : [item]
32
+ end
33
+
34
+ def line_item(item)
35
+ {
36
+ quantity: item['qty_ordered'],
37
+ sku: item['sku'],
38
+ name: item['name'],
39
+ price: item['price'],
40
+ tax_lines: tax_lines(item),
41
+ }.stringify_keys
42
+ end
43
+
44
+ def tax_lines(item)
45
+ {
46
+ title: 'Tax',
47
+ price: item['tax_amount'],
48
+ rate: item['tax_percent'],
49
+ }.stringify_keys
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require 'shopify_transporter/pipeline/stage'
3
+ require 'shopify_transporter/shopify'
4
+
5
+ module ShopifyTransporter
6
+ module Pipeline
7
+ module Magento
8
+ module Order
9
+ class TopLevelAttributes < Pipeline::Stage
10
+ def convert(hash, record)
11
+ record.merge!(
12
+ {
13
+ name: hash['increment_id'],
14
+ email: hash['customer_email'],
15
+ processed_at: hash['created_at'],
16
+ subtotal_price: hash['subtotal'],
17
+ total_tax: hash['tax_amount'],
18
+ total_price: hash['grand_total'],
19
+ }.stringify_keys
20
+ )
21
+ customer = build_customer(hash)
22
+ record['customer'] = customer unless customer.empty?
23
+ record
24
+ end
25
+
26
+ private
27
+
28
+ def build_customer(hash)
29
+ {
30
+ email: hash['customer_email'],
31
+ first_name: hash['customer_firstname'],
32
+ last_name: hash['customer_lastname'],
33
+ }.stringify_keys
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ require 'shopify_transporter/pipeline/stage'
3
+ require 'shopify_transporter/shopify'
4
+
5
+ module ShopifyTransporter
6
+ module Pipeline
7
+ module Magento
8
+ module Product
9
+ class TopLevelAttributes < Pipeline::Stage
10
+ def convert(hash, record)
11
+ accumulator = TopLevelAttributesAccumulator.new(record)
12
+ accumulator.accumulate(hash)
13
+ end
14
+
15
+ class TopLevelAttributesAccumulator < Shopify::AttributesAccumulator
16
+ COLUMN_MAPPING = {
17
+ 'sku' => 'sku',
18
+ 'name' => 'title',
19
+ 'description' => 'body_html',
20
+ }
21
+
22
+ private
23
+
24
+ def input_applies?(_input)
25
+ true
26
+ end
27
+
28
+ def attributes_from(input)
29
+ map_from_key_to_val(COLUMN_MAPPING, input)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyTransporter
3
+ module Pipeline
4
+ class Stage
5
+ attr_reader :params
6
+
7
+ def initialize(params = nil)
8
+ @params = params
9
+ end
10
+
11
+ def convert(_hash, _record)
12
+ raise NotImplementedError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'pipeline/stage.rb'
3
+ Dir["#{File.dirname(__FILE__)}/pipeline/all_platforms/*.rb"].each { |f| require f }
4
+ Dir["#{File.dirname(__FILE__)}/pipeline/magento/**/*.rb"].each { |f| require f }
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyTransporter
4
+ class KeyMissing < StandardError
5
+ def initialize(key_name)
6
+ super("cannot process entry. Required field not found: '#{key_name}'")
7
+ end
8
+ end
9
+
10
+ class RequiredKeyMissing < KeyMissing; end
11
+
12
+ class MissingParentObject < KeyMissing; end
13
+
14
+ class RecordBuilder
15
+ attr_accessor :instances
16
+
17
+ def initialize(key_name, key_required)
18
+ @instances = {}
19
+ @key_name = key_name
20
+ @key_required = key_required
21
+ end
22
+
23
+ def build(input)
24
+ validate_related(input)
25
+
26
+ if key_of(input).nil?
27
+ yield @last_record
28
+ return
29
+ end
30
+
31
+ yield record_from(input)
32
+ end
33
+
34
+ private
35
+
36
+ def validate_related(input)
37
+ raise MissingParentObject, @key_name if key_of(input).nil? && @last_record.nil?
38
+ end
39
+
40
+ def record_from(input)
41
+ record = @instances[key_of(input)] ||= {}
42
+ @last_record = record
43
+ end
44
+
45
+ def key_of(input)
46
+ record_key = input[@key_name]
47
+ raise RequiredKeyMissing, @key_name if @key_required && record_key.nil?
48
+ record_key
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'attributes_helpers'
3
+
4
+ module ShopifyTransporter
5
+ module Shopify
6
+ class AttributesAccumulator
7
+ include AttributesHelpers
8
+
9
+ attr_reader :output
10
+
11
+ def initialize(initial_value)
12
+ @output = initial_value
13
+ end
14
+
15
+ def accumulate(input)
16
+ return @output unless input_applies?(input)
17
+ attributes = attributes_from(input)
18
+ accumulate_attributes(attributes)
19
+ end
20
+
21
+ private
22
+
23
+ def input_applies?(_input)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def attributes_from(_input)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def accumulate_attributes(attributes)
32
+ case @output
33
+ when Array
34
+ @output << attributes
35
+ when Hash
36
+ @output.merge!(attributes)
37
+ else
38
+ raise 'Unexpected initial value. Initial value must be an array or a hash.'
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require "active_support/core_ext/string"
3
+ module ShopifyTransporter
4
+ module Shopify
5
+ module AttributesHelpers
6
+ def attributes_present?(row, *attributes)
7
+ row.slice(*attributes).any? { |_, v| v.present? }
8
+ end
9
+
10
+ def add_prefix(prefix, *attributes)
11
+ [*attributes].map { |attribute| "#{prefix}#{attribute}".to_sym }
12
+ end
13
+
14
+ def drop_prefix(row, prefix)
15
+ row.transform_keys { |key| key.to_s.remove(prefix).to_sym }
16
+ end
17
+
18
+ def delete_empty_attributes(row)
19
+ row.delete_if { |_, v| v.respond_to?(:empty?) && v.empty? }
20
+ end
21
+
22
+ def normalize_keys(row)
23
+ row.compact.transform_keys { |key| normalize_string(key).to_sym }
24
+ end
25
+
26
+ def normalize_string(str)
27
+ str.to_s.parameterize.underscore
28
+ end
29
+
30
+ def rename_fields(row, field_map)
31
+ return row unless field_map.present?
32
+ row = row.dup
33
+ field_map.each { |from, to| row[to] = row.delete from if row[from].present? }
34
+ row
35
+ end
36
+
37
+ def map_from_key_to_val(mapper, input)
38
+ mapper.each_with_object({}) do |(key, value), attributes|
39
+ attributes[value] = input[key] if input[key]
40
+ end
41
+ end
42
+
43
+ def shopify_metafield_hash(key:, value:, value_type: 'string', namespace:)
44
+ {
45
+ 'key' => key,
46
+ 'value' => value,
47
+ 'value_type' => value_type,
48
+ 'namespace' => namespace,
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'record'
3
+ require 'active_support/inflector'
4
+ require 'csv'
5
+
6
+ module ShopifyTransporter
7
+ module Shopify
8
+ class Customer < Record
9
+ class << self
10
+ def header
11
+ columns.map(&:titleize).to_csv
12
+ end
13
+
14
+ def keys
15
+ %w(email phone).freeze
16
+ end
17
+
18
+ def columns
19
+ %w(
20
+ first_name last_name email phone accepts_marketing tags
21
+ note tax_exempt company address1 address2 city province
22
+ province_code zip country country_code metafield_namespace
23
+ metafield_key metafield_value metafield_value_type
24
+ ).freeze
25
+ end
26
+ end
27
+
28
+ def to_csv
29
+ CSV.generate do |csv|
30
+ csv << top_level_row_values
31
+ address_row_values.each { |row| csv << row }
32
+ metafield_row_values.each { |row| csv << row }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ TOP_LEVEL_ATTRIBUTES = %w(
39
+ first_name last_name email phone
40
+ accepts_marketing
41
+ tags
42
+ note
43
+ tax_exempt
44
+ ).freeze
45
+
46
+ ADDRESS_ATTRIBUTES = %w(
47
+ first_name last_name phone
48
+ company
49
+ address1 address2
50
+ city
51
+ province province_code zip
52
+ country country_code
53
+ ).freeze
54
+
55
+ SHARED_ADDRESS_ATTRIBUTES = %w(
56
+ first_name last_name phone
57
+ ).freeze
58
+
59
+ def top_level_row_values
60
+ base_hash.merge(record_hash.slice(*TOP_LEVEL_ATTRIBUTES)).values
61
+ end
62
+
63
+ def address_row_values
64
+ return [] unless record_hash['addresses']
65
+ record_hash['addresses'].map do |address_hash|
66
+ address = address_hash.slice(*ADDRESS_ATTRIBUTES)
67
+ populate_missing_address_attributes!(address)
68
+ row_values_from(address) if self.class.has_values?(address)
69
+ end.compact
70
+ end
71
+
72
+ def populate_missing_address_attributes!(address)
73
+ SHARED_ADDRESS_ATTRIBUTES.each do |shared_attribute|
74
+ address[shared_attribute] = record_hash[shared_attribute] if address[shared_attribute].nil?
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/inflector'
3
+ require 'csv'
4
+ require_relative 'attributes_helpers'
5
+ require_relative 'record'
6
+
7
+ module ShopifyTransporter
8
+ module Shopify
9
+ class Order < Record
10
+ class << self
11
+ include AttributesHelpers
12
+
13
+ def header
14
+ [
15
+ 'Name', 'Email', 'Financial Status', 'Fulfillment Status', 'Currency',
16
+ 'Buyer Accepts Marketing', 'Cancel Reason', 'Cancelled At', 'Closed At', 'Tags', 'Note',
17
+ 'Phone', 'Referring Site', 'Processed At', 'Source name', 'Total discounts', 'Total weight',
18
+ 'Total Tax', 'Shipping Company', 'Shipping Name', 'Shipping Phone', 'Shipping First Name',
19
+ 'Shipping Last Name', 'Shipping Address1', 'Shipping Address2', 'Shipping City',
20
+ 'Shipping Province', 'Shipping Province Code', 'Shipping Zip', 'Shipping Country',
21
+ 'Shipping Country Code', 'Billing Company', 'Billing Name', 'Billing Phone',
22
+ 'Billing First Name', 'Billing Last Name', 'Billing Address1', 'Billing Address2',
23
+ 'Billing City', 'Billing Province', 'Billing Province Code', 'Billing Zip',
24
+ 'Billing Country', 'Billing Country Code', 'Lineitem name', 'Lineitem quantity',
25
+ 'Lineitem price', 'Lineitem discount', 'Lineitem compare at price', 'Lineitem sku',
26
+ 'Lineitem requires shipping', 'Lineitem taxable', 'Lineitem fulfillment status',
27
+ 'Tax 1 Title', 'Tax 1 Price', 'Tax 1 Rate', 'Tax 2 Title', 'Tax 2 Price', 'Tax 2 Rate',
28
+ 'Tax 3 Title', 'Tax 3 Price', 'Tax 3 Rate', 'Metafield Namespace', 'Metafield Key',
29
+ 'Metafield Value', 'Metafield Value Type'
30
+ ].to_csv
31
+ end
32
+
33
+ def keys
34
+ %w(name).freeze
35
+ end
36
+
37
+ def columns
38
+ @columns ||= header.split(',').map do |header_column|
39
+ normalize_string(header_column)
40
+ end
41
+ end
42
+ end
43
+
44
+ def to_csv
45
+ CSV.generate do |csv|
46
+ csv << top_level_row_values
47
+ line_item_row_values.each { |row| csv << row }
48
+ metafield_row_values.each { |row| csv << row }
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ TOP_LEVEL_ATTRIBUTES = %w(
55
+ name email financial_status fulfillment_status currency
56
+ buyer_accepts_marketing cancel_reason cancelled_at closed_at tags note
57
+ phone referring_site processed_at source_name total_discounts total_weight total_tax
58
+ )
59
+
60
+ ADDRESS_ATTRIBUTES = %w(
61
+ company name phone first_name last_name address1 address2 city province province_code zip country country_code
62
+ )
63
+
64
+ LINE_ITEM_PREFIX = "lineitem_"
65
+
66
+ LINE_ITEM_ATTRIBUTES = %w(
67
+ name quantity price discount compare_at_price sku requires_shipping taxable fulfillment_status
68
+ )
69
+
70
+ def address_hash_for(address_hash, prefix)
71
+ return {} if address_hash.blank?
72
+
73
+ address_hash.transform_keys { |k| "#{prefix}#{k}" }
74
+ end
75
+
76
+ def top_level_row_values
77
+ top_level_hash = record_hash
78
+ .slice(*TOP_LEVEL_ATTRIBUTES)
79
+ .merge(address_hash_for(record_hash['billing_address'], 'billing_'))
80
+ .merge(address_hash_for(record_hash['shipping_address'], 'shipping_'))
81
+ self.class.has_values?(top_level_hash) ? row_values_from(top_level_hash) : nil
82
+ end
83
+
84
+ def tax_line_hash(line_item_hash)
85
+ return {} if line_item_hash['tax_lines'].blank?
86
+
87
+ line_item_hash['tax_lines'].each_with_index.map do |tax_line, index|
88
+ tax_line.each_with_object({}) do |(key, val), hash|
89
+ hash["tax_#{index + 1}_#{key}"] = val
90
+ end
91
+ end.reduce({}, :merge)
92
+ end
93
+
94
+ def line_item_row_values
95
+ return [] unless record_hash['line_items']
96
+
97
+ record_hash['line_items'].map do |line_item_hash|
98
+ line_item = line_item_hash.slice(*LINE_ITEM_ATTRIBUTES)
99
+ .transform_keys! { |k| "#{LINE_ITEM_PREFIX}#{k}" }
100
+ .merge(tax_line_hash(line_item_hash))
101
+
102
+ row_values_from(line_item) if self.class.has_values?(line_item)
103
+ end.compact
104
+ end
105
+ end
106
+ end
107
+ end