huginn_bigcommerce_product_agent 1.10.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ module BigcommerceProductAgent
2
+ module Mapper
3
+ class CustomFieldMapper
4
+
5
+ def self.map(field_map, product, bc_product)
6
+ fields = {
7
+ upsert: [],
8
+ delete: [],
9
+ }
10
+
11
+ existing_fields = {}
12
+
13
+ if bc_product
14
+ bc_product['custom_fields'].each do |cf|
15
+ cf['product_id'] = bc_product['id']
16
+ existing_fields[cf['name'].to_s] = cf
17
+ end
18
+ end
19
+
20
+ if field_map && field_map['additionalProperty']
21
+ field_map['additionalProperty'].each do |key, val|
22
+ field = self.from_additional_property(product, existing_fields, key, val)
23
+ fields[:upsert].push(field) unless field.nil?
24
+ end
25
+ end
26
+
27
+ if field_map
28
+ field_map.each do |key, val|
29
+ if key == 'additionalProperty'
30
+ next
31
+ end
32
+
33
+ field = self.from_property(product, existing_fields, key, val)
34
+ fields[:upsert].push(field) unless field.nil?
35
+ end
36
+ end
37
+
38
+ # return values that need deleted
39
+ fields[:delete] = existing_fields.values
40
+ return fields
41
+ end
42
+
43
+ def self.map_one(bc_product, key, value)
44
+ field = bc_product['custom_fields'].select {|field| key == 'related_product_id'}.first
45
+
46
+ mapped = {
47
+ name: key,
48
+ value: value.to_s,
49
+ }
50
+
51
+ if field
52
+ mapped[:id] = field['id']
53
+ end
54
+
55
+ return mapped
56
+ end
57
+
58
+ private
59
+
60
+ def self.from_property(product, existing_fields, from_key, to_key)
61
+ if !product[from_key].nil?
62
+ field = {
63
+ name: to_key,
64
+ value: product[from_key].to_s
65
+ }
66
+
67
+ if existing_fields[to_key]
68
+ field[:id] = existing_fields[to_key]['id']
69
+ existing_fields.delete(to_key)
70
+ end
71
+
72
+ return field
73
+ end
74
+ end
75
+
76
+ def self.from_additional_property(product, existing_fields, from_key, to_key)
77
+ # date published
78
+ item = product['additionalProperty'].select {|p| p['propertyID'] == from_key}.first
79
+ if !item.nil?
80
+ field = {
81
+ name: to_key,
82
+ value: item['value'].to_s
83
+ }
84
+
85
+ if existing_fields[to_key]
86
+ field[:id] = existing_fields[to_key]['id']
87
+ existing_fields.delete(to_key)
88
+ end
89
+
90
+ return field
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,100 @@
1
+ module BigcommerceProductAgent
2
+ module Mapper
3
+ class MetaFieldMapper
4
+
5
+ def self.map(field_map, product, bc_product, meta_fields, namespace)
6
+ fields = {
7
+ upsert: [],
8
+ delete: [],
9
+ }
10
+
11
+ existing_fields = {}
12
+
13
+ if bc_product
14
+ meta_fields.each do |mf|
15
+
16
+ unless mf['namespace'] != namespace
17
+ # Only delete meta fields managed by this sync
18
+ existing_fields[mf['key'].to_s] = mf
19
+ end
20
+ end
21
+ end
22
+
23
+ if field_map && field_map['additionalProperty']
24
+ field_map['additionalProperty'].each do |key, val|
25
+ field = self.from_additional_property(product, existing_fields, key, val, namespace, bc_product)
26
+ fields[:upsert].push(field) unless field.nil?
27
+ end
28
+ end
29
+
30
+ if field_map
31
+ field_map.each do |key, val|
32
+ if key == 'additionalProperty'
33
+ next
34
+ end
35
+
36
+ field = self.from_property(product, existing_fields, key, val, namespace, bc_product)
37
+ fields[:upsert].push(field) unless field.nil?
38
+ end
39
+ end
40
+
41
+ # return values that need deleted
42
+ fields[:delete] = existing_fields.values
43
+ return fields
44
+ end
45
+
46
+ private
47
+
48
+ def self.from_property(product, existing_fields, from_key, to_key, namespace, bc_product)
49
+
50
+ if !product[from_key].nil?
51
+ field = {
52
+ namespace: namespace,
53
+ permission_set: 'write',
54
+ resource_type: 'product',
55
+ key: to_key,
56
+ value: product[from_key]
57
+ }
58
+
59
+ if bc_product
60
+ field[:resource_id] = bc_product['id']
61
+ end
62
+
63
+ if existing_fields[to_key]
64
+ field[:id] = existing_fields[to_key]['id']
65
+ existing_fields.delete(to_key)
66
+ end
67
+
68
+ return field
69
+ end
70
+ end
71
+
72
+ def self.from_additional_property(product, existing_fields, from_key, to_key, namespace, bc_product)
73
+
74
+ item = product['additionalProperty'].select {|p| p['propertyID'] == from_key}.first
75
+ if !item.nil?
76
+
77
+ field = {
78
+ namespace: namespace,
79
+ permission_set: 'write',
80
+ resource_type: 'product',
81
+ key: to_key,
82
+ value: item['value']
83
+ }
84
+
85
+ if bc_product
86
+ field[:resource_id] = bc_product['id']
87
+ end
88
+
89
+ if existing_fields[to_key]
90
+ field[:id] = existing_fields[to_key]['id']
91
+ existing_fields.delete(to_key)
92
+ end
93
+
94
+ return field
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,83 @@
1
+ module BigcommerceProductAgent
2
+ module Mapper
3
+ class ModifierMapper
4
+
5
+ def self.map(bc_product, bc_children, sku_option_map, is_default_map)
6
+ modifier = {
7
+ display_name: 'Option',
8
+ type: 'product_list',
9
+ required: true,
10
+ sort_order: 1,
11
+ config: {
12
+ product_list_adjusts_inventory: true,
13
+ product_list_adjusts_pricing: true,
14
+ product_list_shipping_calc: 'none'
15
+ },
16
+ option_values: []
17
+ }
18
+
19
+ existing_modifier = nil
20
+ existing_option_ids = []
21
+ if bc_product && !bc_product['modifiers'].nil?
22
+ existing_modifier = bc_product['modifiers'].select {|m| m['display_name'] == modifier[:display_name]}.first
23
+ modifier[:product_id] = bc_product['id']
24
+
25
+ if !existing_modifier.nil?
26
+ modifier[:id] = existing_modifier['id']
27
+ existing_option_ids = existing_modifier['option_values'].map {|value| value['id']}
28
+ end
29
+ end
30
+
31
+ bc_children.each do |child|
32
+ existing_option = nil
33
+ if existing_modifier
34
+ existing_option = existing_modifier['option_values'].select do |val|
35
+ val['value_data'] && val['value_data']['product_id'] == child['id']
36
+ end.first
37
+ end
38
+
39
+ option = {
40
+ label: sku_option_map[child['sku']],
41
+ sort_order: 0,
42
+ value_data: {
43
+ product_id: child['id']
44
+ },
45
+ is_default: is_default_map[child['sku']],
46
+ adjusters: {
47
+ price: nil,
48
+ weight: nil,
49
+ image_url: '',
50
+ purchasing_disabled: {
51
+ status: false,
52
+ message: ''
53
+ }
54
+ }
55
+ }
56
+
57
+ if existing_option
58
+ option[:id] = existing_option['id']
59
+ option[:option_id] = existing_option['option_id']
60
+ existing_option_ids.delete(existing_option['id'])
61
+ end
62
+
63
+ modifier[:option_values].push(option)
64
+ end
65
+
66
+ # any left over option value should be removed
67
+ modifier_values_delete = existing_option_ids.map do |id|
68
+ {
69
+ product_id: bc_product['id'],
70
+ modifier_id: existing_modifier['id'],
71
+ value_id: id
72
+ }
73
+ end
74
+
75
+ return {
76
+ upsert: modifier,
77
+ delete: modifier_values_delete
78
+ }
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,65 @@
1
+ module BigcommerceProductAgent
2
+ module Mapper
3
+ class OptionMapper
4
+
5
+ def self.variant_option_name
6
+ 'Options'
7
+ end
8
+
9
+ def self.map(product_id, option, option_values)
10
+ mapped = {
11
+ product_id: product_id,
12
+ display_name: self.variant_option_name,
13
+ type: 'radio_buttons',
14
+ sort_order: 0,
15
+ option_values: option_values,
16
+ }
17
+
18
+ if option && option['id']
19
+ mapped[:id] = option['id']
20
+ end
21
+
22
+ return mapped
23
+ end
24
+
25
+ def self.option_value_operations(bc_option, option_values)
26
+ option_value_operations = {
27
+ create: [],
28
+ update: [],
29
+ delete: [],
30
+ }
31
+
32
+ if bc_option && bc_option['option_values']
33
+ bc_option['option_values'].each do |option_value|
34
+ if option_values.include?(option_value['label'])
35
+ option_value_operations[:update].push(option_value)
36
+ else
37
+ option_value_operations[:delete].push(option_value)
38
+ end
39
+ end
40
+
41
+ option_values.each do |option_value_label|
42
+ options_exists = bc_option['option_values'].any? {|option_value| option_value['label'] == option_value_label}
43
+ if !options_exists
44
+ option_value_operations[:create].push(
45
+ OptionValueMapper.map(option_value_label)
46
+ )
47
+ end
48
+ end
49
+ else
50
+ option_values.each do |option_value_label|
51
+ option_value_operations[:create].push(
52
+ OptionValueMapper.map(option_value_label)
53
+ )
54
+ end
55
+ end
56
+
57
+ return option_value_operations
58
+ end
59
+
60
+ private
61
+
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,16 @@
1
+ module BigcommerceProductAgent
2
+ module Mapper
3
+ class OptionValueMapper
4
+
5
+ def self.map(label)
6
+ {
7
+ label: label,
8
+ sort_order: 0,
9
+ }
10
+ end
11
+
12
+ private
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,226 @@
1
+ module BigcommerceProductAgent
2
+ module Mapper
3
+ class ProductMapper
4
+
5
+ def self.map(product, variant, additional_data = {}, is_digital = false, default_sku='')
6
+ name = product['name']
7
+
8
+ if variant
9
+ # variants inherit from and override parent product info
10
+ name = "#{name} (#{self.get_option(variant)})"
11
+ product = product.merge(variant)
12
+ else
13
+ # wrapper product
14
+ default_variant = self.get_variant_by_sku(product['sku'], product)
15
+ if default_variant
16
+ # pull up some properties from default variant (since bc doesn't display them otherwise)
17
+ product = {
18
+ "weight" => default_variant['weight'],
19
+ "width" => default_variant['width'],
20
+ "depth" => default_variant['depth'],
21
+ "height" => default_variant['height'],
22
+ "releaseDate" => default_variant['releaseDate'],
23
+ "datePublished" => default_variant['datePublished'],
24
+ "availability" => self.get_availability_offer(product, default_variant),
25
+ }.merge(product)
26
+ end
27
+ end
28
+
29
+ result = {
30
+ name: name,
31
+ sku: product ? product['sku'] : default_sku,
32
+ is_default: product['isDefault'],
33
+ type: product['isDigital'] == true || is_digital ? 'digital' : 'physical',
34
+ description: product['description'] || '',
35
+ price: product['offers'] && product['offers'][0] ? product['offers'][0]['price'] : '0',
36
+ categories: self.get_categories(product),
37
+ availability: self.get_availability(product),
38
+ weight: product['weight'] ? product['weight']['value'] : '0',
39
+ width: product['width'] ? product['width']['value'] : '0',
40
+ depth: product['depth'] ? product['depth']['value'] : '0',
41
+ height: product['height'] ? product['height']['value'] : '0',
42
+ meta_keywords: self.meta_keywords(product),
43
+ meta_description: self.meta_description(product) || '',
44
+ search_keywords: self.get_search_keywords(additional_data.delete(:additional_search_terms), product),
45
+ is_visible: variant ? false : true,
46
+ preorder_release_date: product['releaseDate'] && product['releaseDate'].to_datetime ? product['releaseDate'].to_datetime.strftime("%FT%T%:z") : nil,
47
+ preorder_message: self.get_availability(product) == 'preorder' ? product['availability'] : '',
48
+ is_preorder_only: self.get_availability(product) == 'preorder' ? true : false,
49
+ page_title: product['page_title'] || '',
50
+ }
51
+ result[:upc] = product['gtin12'] if product['gtin12']
52
+
53
+ result.merge(additional_data)
54
+ end
55
+
56
+ def self.get_wrapper_sku(product)
57
+ if product
58
+ "#{product['sku']}-W"
59
+ end
60
+ end
61
+
62
+ def self.payload(sku, product, product_id = nil, additional_data = {}, is_digital = false)
63
+ variant = self.get_variant_by_sku(sku, product)
64
+ payload = self.map(product, variant, additional_data, is_digital, sku)
65
+ payload[:id] = product_id unless product_id.nil?
66
+ payload[:sku] = self.get_wrapper_sku(product)
67
+
68
+ return payload
69
+ end
70
+
71
+ def self.get_product_skus(product)
72
+ product['model'].map { |model| model['sku'] }
73
+ end
74
+
75
+ def self.get_sku_option_label_map(product)
76
+ map = {}
77
+
78
+ product['model'].each do |model|
79
+ map[model['sku']] = self.get_option(model)
80
+ end
81
+
82
+ return map
83
+ end
84
+
85
+ def self.get_is_default(product)
86
+ map = {}
87
+
88
+ product['model'].each do |model|
89
+ map[model['sku']] = model["isDefault"]
90
+ end
91
+
92
+ return map
93
+ end
94
+
95
+ def self.has_digital_variants?(product)
96
+ product['model'].any? {|m| m['isDigital'] == true}
97
+ end
98
+
99
+ def self.has_physical_variants?(product)
100
+ product['model'].any? {|m| m['isDigital'] != true}
101
+ end
102
+
103
+ def self.split_digital_and_physical(product, field_map)
104
+ result = {}
105
+
106
+ digitals = product['model'].select {|m| m['isDigital'] == true}
107
+
108
+ if digitals.length > 0
109
+ clone = Marshal.load(Marshal.dump(product))
110
+ clone['model'] = digitals
111
+ clone['sku'] = clone['model'].select {|m| m['isDefault'] = true}.first['sku']
112
+ self.merge_additional_properties(clone, field_map)
113
+ result[:digital] = clone
114
+ end
115
+
116
+ physicals = product['model'].select {|m| m['isDigital'] != true}
117
+
118
+ if physicals.length > 0
119
+ clone = Marshal.load(Marshal.dump(product))
120
+ clone['model'] = physicals
121
+ clone['sku'] = clone['model'].select {|m| m['isDefault'] = true}.first['sku']
122
+ self.merge_additional_properties(clone, field_map)
123
+ result[:physical] = clone
124
+ end
125
+
126
+ return result
127
+ end
128
+
129
+ private
130
+
131
+ def self.get_categories(product)
132
+ categories = []
133
+
134
+ if product['categories']
135
+ categories = product['categories'].map do |category|
136
+ category['identifier'].to_i
137
+ end
138
+ end
139
+
140
+ return categories
141
+ end
142
+
143
+ def self.get_option(variant)
144
+ if variant['encodingFormat']
145
+ return variant['encodingFormat']
146
+ elsif variant['bookFormat']
147
+ parts = variant['bookFormat'].split('/')
148
+ return parts[parts.length - 1]
149
+ else
150
+ return self.get_additional_property_value(variant, 'option')
151
+ end
152
+ end
153
+
154
+ def self.meta_description(product)
155
+ return self.get_additional_property_value(product, 'meta_description', '')
156
+ end
157
+
158
+ def self.meta_keywords(product)
159
+ meta_keywords = []
160
+
161
+ product['keywords'].split(',') unless product['keywords'].nil?
162
+
163
+ return meta_keywords
164
+ end
165
+
166
+ def self.get_additional_property_value(product, name, default = nil)
167
+ value = default
168
+
169
+ return value if product['additionalProperty'].nil?
170
+
171
+ idx = product['additionalProperty'].index {|item| item['propertyID'] == name}
172
+ value = product['additionalProperty'][idx]['value'] unless idx.nil?
173
+
174
+ return value
175
+ end
176
+
177
+ def self.get_variant_by_sku(sku, product)
178
+ product['model'].select {|m| m['sku'] == sku}.first
179
+ end
180
+
181
+ def self.merge_additional_properties(clone, field_map)
182
+ defaultVariant = clone['model'].select { |v| v['isDefault'] }.first
183
+ if defaultVariant['isDefault'] && defaultVariant['additionalProperty']
184
+ unless field_map.nil? || field_map['additionalProperty'].nil?
185
+ field_map['additionalProperty'].each do |field, key|
186
+ prop = defaultVariant['additionalProperty'].select { |prop| prop['propertyID'] == field[key] }.first
187
+ clone['additionalProperty'].push(prop) unless prop.nil?
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ # get a list of search keywords for the products
194
+ def self.get_search_keywords(additional_search_terms, product)
195
+ return (self.meta_keywords(product) + additional_search_terms.split(",")).join(",")
196
+ end
197
+
198
+ def self.get_availability(product)
199
+ if product['datePublished'] && product['datePublished'].to_datetime && product['datePublished'].to_datetime < DateTime.now
200
+ return 'available'
201
+ else
202
+ if product['releaseDate'] && product['releaseDate'].to_datetime
203
+ if product['releaseDate'].to_datetime > DateTime.now
204
+ return 'preorder'
205
+ else
206
+ return 'available'
207
+ end
208
+ else
209
+ return 'disabled'
210
+ end
211
+ end
212
+ end
213
+
214
+ def self.get_availability_offer(product, variant)
215
+ if product['offers'] && product['offers'][0]
216
+ return product['offers'][0]['availability']
217
+ else
218
+ if variant['offers'] && variant['offers'][0]
219
+ return variant['offers'][0]['availability']
220
+ end
221
+ end
222
+ return ''
223
+ end
224
+ end
225
+ end
226
+ end