shopify_transporter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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