huginn_acumen_product_agent 1.7.1 → 2.2.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.
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is responsible for reading/processing product Contributor data.
4
+ #
5
+ # The data in question here comes from multiple tables. The `ProdMkt_Contrib_Link`
6
+ # table defines the _relationship_ between products and contributors, and the
7
+ # `ProdMkt_Contributor` table defines the _contribution type_ (Author, Editor, etc)
8
+ module ProductContributorsQueryConcern
9
+ extend AcumenQueryConcern
10
+
11
+ # Updates the provided products with their associared contributor data
12
+ def fetch_product_contributors(acumen_client, products)
13
+
14
+ marketing_ids = products.map { |p| p['acumenAttributes']['product_marketing_id'] }
15
+ contributor_data = acumen_client.get_product_contributors(marketing_ids)
16
+ contributor_data = process_product_contributor_response(contributor_data)
17
+
18
+ contributor_ids = contributor_data.map { |c| c['contributor_id'] }
19
+ contributor_type_data = acumen_client.get_contributor_types(contributor_ids)
20
+ contributor_type_data = process_contributor_type_response(contributor_type_data)
21
+
22
+ return map_contributor_data(products, contributor_data, contributor_type_data)
23
+ end
24
+
25
+ # This function parses the raw data returned from the ProdMkt_Contrib_Link table
26
+ # This table holds the relationship between products and Contributors
27
+ # The resulting data is a hash mapping contributor arrays to Prod_Mkt.ID values
28
+ def process_product_contributor_response(raw_data)
29
+ contributors = []
30
+
31
+ raw_data.each do |contributor|
32
+
33
+ begin
34
+ mapped = response_mapper(contributor, {
35
+ 'ProdMkt_Contrib_Link.ProdMkt_Contrib_ID' => 'contributor_id',
36
+ 'ProdMkt_Contrib_Link.ProdMkt_ID' => 'product_marketing_id',
37
+ 'ProdMkt_Contrib_Link.Inactive' => 'inactive',
38
+ })
39
+
40
+ if mapped['inactive'] == '0'
41
+ contributors.push(mapped)
42
+ end
43
+ rescue => error
44
+ issue_error(AcumenAgentError.new(
45
+ 'process_product_contributor_response',
46
+ 'Failed while processing contributor record',
47
+ contributor,
48
+ error,
49
+ ))
50
+ end
51
+ end
52
+
53
+ return contributors
54
+ end
55
+
56
+ # This function parses the raw data returned from the ProdMkt_Contributor table
57
+ # This table holds the contributor type (e.g. Author) for the
58
+ # contributor/product relationship
59
+ def process_contributor_type_response(raw_data)
60
+ results = {}
61
+ raw_data.map do |contributor_type|
62
+
63
+ begin
64
+ mapped = response_mapper(contributor_type, {
65
+ 'ProdMkt_Contributor.ID' => 'contributor_id',
66
+ 'ProdMkt_Contributor.Contrib_Type' => 'type',
67
+ })
68
+
69
+
70
+ if !results[mapped['contributor_id']]
71
+ results[mapped['contributor_id']] = mapped['type']
72
+ end
73
+ rescue => error
74
+ issue_error(AcumenAgentError.new(
75
+ 'process_contributor_type_response',
76
+ 'Failed while processing contributor type record',
77
+ contributor_type,
78
+ error,
79
+ ))
80
+ end
81
+ end
82
+
83
+ return results
84
+ end
85
+
86
+ # This function maps parsed Contributor records to their matching Inv_Product record
87
+ def map_contributor_data(products, contributor_data, contributor_type_data)
88
+ products.each do |product|
89
+
90
+ begin
91
+ marketing_id = product['acumenAttributes']['product_marketing_id']
92
+ contributors = contributor_data.select { |c| c['product_marketing_id'] == marketing_id }
93
+
94
+ product['contributors'] = contributors.map do |c|
95
+ {
96
+ '@type' => 'Person',
97
+ 'identifier' => c['contributor_id'],
98
+ 'acumenAttributes' => {
99
+ 'contrib_type' => contributor_type_data[c['contributor_id']]
100
+ }
101
+ }
102
+ end
103
+ rescue => error
104
+ issue_error(AcumenAgentError.new(
105
+ 'map_contributor_data',
106
+ 'Failed while mapping contributor data to products',
107
+ { id: product['identifier'], sku: product['sku'] },
108
+ error
109
+ ))
110
+ end
111
+ end
112
+
113
+ return products
114
+ end
115
+
116
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: huginn_acumen_product_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacob Spizziri
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-13 00:00:00.000000000 Z
11
+ date: 2021-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -63,9 +63,16 @@ extra_rdoc_files: []
63
63
  files:
64
64
  - LICENSE.txt
65
65
  - lib/huginn_acumen_product_agent.rb
66
+ - lib/huginn_acumen_product_agent/acumen_agent_error.rb
66
67
  - lib/huginn_acumen_product_agent/acumen_client.rb
67
68
  - lib/huginn_acumen_product_agent/acumen_product_agent.rb
68
- - lib/huginn_acumen_product_agent/concerns/acumen_product_query_concern.rb
69
+ - lib/huginn_acumen_product_agent/concerns/acumen_query_concern.rb
70
+ - lib/huginn_acumen_product_agent/concerns/alternate_products_query_concern.rb
71
+ - lib/huginn_acumen_product_agent/concerns/inv_product_query_concern.rb
72
+ - lib/huginn_acumen_product_agent/concerns/inv_status_query_concern.rb
73
+ - lib/huginn_acumen_product_agent/concerns/prod_mkt_query_concern.rb
74
+ - lib/huginn_acumen_product_agent/concerns/product_categories_query_concern.rb
75
+ - lib/huginn_acumen_product_agent/concerns/product_contributors_query_concern.rb
69
76
  - spec/acumen_product_agent_spec.rb
70
77
  homepage: https://github.com/5-Stones/huginn_acumen_product_agent
71
78
  licenses:
@@ -1,479 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AcumenProductQueryConcern
4
- extend ActiveSupport::Concern
5
-
6
- UNIT_MAP = {
7
- 'oz.' => 'OZ',
8
- 'Inches (US)' => 'INH',
9
- }
10
-
11
- def get_products_by_ids(acumen_client, ids)
12
- response = acumen_client.get_products(ids)
13
- products = []
14
-
15
- products = parse_product_request(response)
16
-
17
- response = acumen_client.get_products_marketing(ids)
18
- marketing = parse_product_marketing_request(response)
19
-
20
- merge_products_and_marketing(products, marketing)
21
- end
22
-
23
- def get_variants_for_ids(acumen_client, ids)
24
- result = get_linked_products_by_ids(acumen_client, ids)
25
-
26
- # Filtering out duplicate links getting sent from acumen
27
- filter = []
28
- result.each do |link|
29
- if (link['alt_format'].to_s != 0.to_s && !link.in?(filter))
30
- filter.push(link)
31
- end
32
- end
33
-
34
- return filter
35
- end
36
-
37
- def get_linked_products_by_ids(acumen_client, ids)
38
- response = acumen_client.get_linked_products(ids)
39
- process_linked_product_query(response)
40
- end
41
-
42
- def get_product_contributors(acumen_client, products)
43
- ids = products.map {|product| product['acumenAttributes']['product_marketing_id']}
44
- response = acumen_client.get_product_contributors(ids)
45
- product_contributors = process_product_contributor_query(response)
46
-
47
- products.each do |product|
48
- id = product['acumenAttributes']['product_marketing_id']
49
- product_contributor = product_contributors[id]
50
-
51
-
52
- if product_contributor
53
- contributor_ids = product_contributor.map do |pc|
54
- pc['contributor_id']
55
- end
56
-
57
- type_response = acumen_client.get_contributor_types(contributor_ids)
58
- contributor_types = process_contributor_types_query(type_response)
59
-
60
- product['contributors'] = product_contributor.map do |pc|
61
- {
62
- '@type' => 'Person',
63
- 'identifier' => pc['contributor_id'],
64
- 'acumenAttributes' => {
65
- 'contrib_type' => contributor_types[pc['contributor_id']]
66
- }
67
- }
68
- end
69
- end
70
- end
71
- products
72
- end
73
-
74
- def get_product_variants(acumen_client, products, physical_formats, digital_formats)
75
- ids = products.map { |product| product['identifier'] }
76
- # fetch product/variant relationships
77
- variant_links = get_variants_for_ids(acumen_client, ids)
78
- variant_ids = variant_links.map { |link| link['to_id'] }
79
-
80
- variant_ids = variant_links.map { |link| link['to_id'] }
81
-
82
- # fetch product variants
83
- variants = get_products_by_ids(acumen_client, variant_ids)
84
-
85
- # merge variants and products together
86
- process_products_and_variants(products, variants, variant_links, physical_formats, digital_formats)
87
- end
88
-
89
- def get_product_categories(acumen_client, products)
90
- # fetch categories
91
-
92
- skus = products.map { |product| product['model'].map { |m| m['sku'] } }[0]
93
- response = acumen_client.get_product_categories(skus)
94
- categories = process_product_categories_query(response)
95
-
96
- # map categories to products
97
- products.each do |product|
98
- product['model'].each do |variant|
99
- variant['categories'] = []
100
- sku = variant['sku']
101
- if categories[sku]
102
- active = categories[sku].select { |c| c['inactive'] == '0' }
103
- active.map do |category|
104
- variant['categories'].push({
105
- '@type' => 'Thing',
106
- 'identifier' => category['category_id']
107
- })
108
- end
109
- end
110
- end
111
- end
112
-
113
- products
114
- end
115
-
116
- def parse_product_request(products)
117
- products.map do |p|
118
- variant = response_mapper(p, {
119
- 'Inv_Product.ID' => 'identifier',
120
- 'Inv_Product.ProdCode' => 'sku',
121
- 'Inv_Product.SubTitle' => 'disambiguatingDescription',
122
- 'Inv_Product.ISBN_UPC' => 'isbn',
123
- 'Inv_Product.Pub_Date' => 'datePublished',
124
- 'Inv_Product.Next_Release' => 'releaseDate',
125
- })
126
- variant['@type'] = 'ProductModel'
127
- variant['isDefault'] = false
128
- variant['isTaxable'] = field_value(p, 'Inv_Product.Taxable') == '1'
129
- variant['isAvailableForPurchase'] = field_value(p, 'Inv_Product.Not_On_Website') == '0'
130
- variant['acumenAttributes'] = {
131
- 'is_master' => field_value(p, 'Inv_Product.OnWeb_LinkOnly') == '0'
132
- }
133
-
134
- variant['offers'] = [{
135
- '@type' => 'Offer',
136
- 'price' => field_value(p, 'Inv_Product.Price_1'),
137
- 'availability' => field_value(p, 'Inv_Product.BO_Reason')
138
- }]
139
- if field_value(p, 'Inv_Product.Price_2')
140
- variant['offers'].push({
141
- '@type' => 'Offer',
142
- 'price' => field_value(p, 'Inv_Product.Price_2'),
143
- 'availability' => field_value(p, 'Inv_Product.BO_Reason')
144
- })
145
- end
146
-
147
- weight = field_value(p, 'Inv_Product.Weight')
148
- variant['weight'] = quantitative_value(weight, 'oz.')
149
-
150
- product = {
151
- '@type' => 'Product',
152
- 'identifier' => variant['identifier'],
153
- 'sku' => variant['sku'],
154
- 'name' => field_value(p, 'Inv_Product.Full_Title'),
155
- 'disambiguatingDescription' => field_value(p, 'Inv_Product.SubTitle'),
156
- 'model' => [
157
- variant
158
- ],
159
- 'additionalProperty' => [],
160
- 'acumenAttributes' => {
161
- 'info_alpha_1' => field_value(p, 'Inv_Product.Info_Alpha_1'),
162
- 'info_boolean_1' => field_value(p, 'Inv_Product.Info_Boolean_1'),
163
- },
164
- 'isAvailableForPurchase' => field_value(p, variant['isAvailableForPurchase']),
165
- }
166
-
167
- category = field_value(p, 'Inv_Product.Category')
168
- if category
169
-
170
- if variant['acumenAttributes']
171
- variant['acumenAttributes']['category'] = category
172
- else
173
- variant['acumenAttributes'] = { 'category' => category }
174
- end
175
-
176
- if category == 'Paperback'
177
- product['additionalType'] = variant['additionalType'] = 'Book'
178
- variant['bookFormat'] = "http://schema.org/Paperback"
179
- variant['accessMode'] = "textual"
180
- variant['isDigital'] = false
181
- elsif category == 'Hardcover'
182
- product['additionalType'] = variant['additionalType'] = 'Book'
183
- variant['bookFormat'] = "http://schema.org/Hardcover"
184
- variant['accessMode'] = "textual"
185
- variant['isDigital'] = false
186
- elsif category == 'eBook'
187
- product['additionalType'] = variant['additionalType'] = 'Book'
188
- variant['bookFormat'] = "http://schema.org/EBook"
189
- variant['accessMode'] = "textual"
190
- variant['isDigital'] = true
191
- elsif category == 'CD'
192
- product['additionalType'] = variant['additionalType'] = 'CreativeWork'
193
- variant['accessMode'] = "auditory"
194
- variant['isDigital'] = false
195
- else
196
- variant['isDigital'] = false
197
- end
198
- end
199
-
200
- product
201
- end
202
- end
203
-
204
- def process_linked_product_query(links)
205
- links.map do |link|
206
- response_mapper(link, {
207
- 'Product_Link.Link_From_ID' => 'from_id',
208
- 'Product_Link.Link_To_ID' => 'to_id',
209
- 'Product_Link.Alt_Format' => 'alt_format',
210
- })
211
- end
212
- end
213
-
214
- def parse_product_marketing_request(products)
215
- results = {}
216
- products.each do |product|
217
- mapped = response_mapper(product, {
218
- 'ProdMkt.Product_ID' => 'product_id',
219
- 'ProdMkt.Product_Code' => 'sku',
220
- 'ProdMkt.ID' => 'id',
221
- 'ProdMkt.Pages' => 'pages',
222
- 'ProdMkt.Publisher' => 'publisher',
223
- 'ProdMkt.Description_Short' => 'description_short',
224
- 'ProdMkt.Description_Long' => 'description_long',
225
- 'ProdMkt.Height' => 'height',
226
- 'ProdMkt.Width' => 'width',
227
- 'ProdMkt.Thickness' => 'depth',
228
- 'ProdMkt.Meta_Keywords' => 'meta_keywords',
229
- 'ProdMkt.Meta_Description' => 'meta_description',
230
- 'ProdMkt.Extent_Unit' => 'extent_unit',
231
- 'ProdMkt.Extent_Value' => 'extent_value',
232
- 'ProdMkt.Age_Highest' => 'age_highest',
233
- 'ProdMkt.Age_Lowest' => 'age_lowest',
234
- 'ProdMkt.Awards' => 'awards',
235
- 'ProdMkt.Dimensions_Unit_Measure' => 'dimensions_unit_measure',
236
- 'ProdMkt.Excerpt' => 'excerpt',
237
- 'ProdMkt.Grade_Highest' => 'grade_highest',
238
- 'ProdMkt.Grade_Lowest' => 'grade_lowest',
239
- 'ProdMkt.Status' => 'status',
240
- 'ProdMkt.UPC' => 'upc',
241
- 'ProdMkt.Weight_Unit_Measure' => 'weight_unit_measure',
242
- 'ProdMkt.Weight' => 'weight',
243
- 'ProdMkt.Info_Text_01' => 'info_text_01',
244
- 'ProdMkt.Info_Text_02' => 'info_text_02',
245
- 'ProdMkt.Religious_Text_Identifier' => 'religious_text_identifier',
246
- 'ProdMkt.Info_Alpha_07' => 'info_alpha_07',
247
- })
248
-
249
- results[mapped['product_id']] = mapped
250
- end
251
-
252
- results
253
- end
254
-
255
- def merge_products_and_marketing(products, product_marketing)
256
- products.each do |product|
257
- marketing = product_marketing[product['identifier']]
258
- if marketing
259
- product['acumenAttributes']['product_marketing_id'] = marketing['id']
260
-
261
- product['publisher'] = {
262
- '@type': 'Organization',
263
- 'name' => marketing['publisher']
264
- };
265
- product['description'] = marketing['description_long']
266
- product['abstract'] = marketing['description_short']
267
- product['keywords'] = marketing['meta_keywords']
268
- product['text'] = marketing['excerpt']
269
-
270
- if marketing['age_lowest'] || marketing['age_highest']
271
- product['typicalAgeRange'] = "#{marketing['age_lowest']}-#{marketing['age_highest']}"
272
- end
273
-
274
- # properties for product pages
275
- if marketing['grade_lowest'] || marketing['grade_highest']
276
- # educationalUse? educationalAlignment?
277
- product['additionalProperty'].push({
278
- '@type' => 'PropertyValue',
279
- 'name' => 'Grade',
280
- 'propertyID' => 'grade_range',
281
- 'minValue' => marketing['grade_lowest'],
282
- 'maxValue' => marketing['grade_highest'],
283
- 'value' => "#{marketing['grade_lowest']}-#{marketing['grade_highest']}",
284
- })
285
- end
286
- if marketing['awards']
287
- product['additionalProperty'].push({
288
- '@type' => 'PropertyValue',
289
- 'propertyID' => 'awards',
290
- 'name' => 'Awards',
291
- 'value' => marketing['awards'],
292
- })
293
- end
294
-
295
- # acumen specific properties
296
- product['acumenAttributes']['extent_unit'] = marketing['extent_unit']
297
- product['acumenAttributes']['extent_value'] = marketing['extent_value']
298
- product['acumenAttributes']['info_text_01'] = marketing['info_text_01']
299
- product['acumenAttributes']['info_text_02'] = marketing['info_text_02']
300
- product['acumenAttributes']['info_alpha_07'] = marketing['info_alpha_07']
301
- product['acumenAttributes']['meta_description'] = marketing['meta_description']
302
- product['acumenAttributes']['religious_text_identifier'] = marketing['religious_text_identifier']
303
- product['acumenAttributes']['status'] = marketing['status']
304
-
305
- variant = product['model'][0]
306
- variant['gtin12'] = marketing['upc']
307
- variant['numberOfPages'] = marketing['pages']
308
-
309
- variant['height'] = quantitative_value(
310
- marketing['height'], marketing['dimensions_unit_measure']
311
- )
312
- variant['width'] = quantitative_value(
313
- marketing['width'], marketing['dimensions_unit_measure']
314
- )
315
- variant['depth'] = quantitative_value(
316
- marketing['thickness'], marketing['dimensions_unit_measure']
317
- )
318
- if variant['weight']['value'] == '0'
319
- variant['weight'] = quantitative_value(
320
- marketing['weight'], marketing['weight_unit_measure']
321
- )
322
- end
323
- end
324
- end
325
-
326
- products
327
- end
328
-
329
- def process_product_categories_query(categories)
330
- results = {}
331
- categories.each do |category|
332
- mapped = response_mapper(category, {
333
- 'ProdMkt_WPC.ProdCode' => 'sku',
334
- 'ProdMkt_WPC.WPC_ID' => 'category_id',
335
- 'ProdMkt_WPC.Inactive' => 'inactive',
336
- })
337
-
338
- if results[mapped['sku']]
339
- results[mapped['sku']].push(mapped)
340
- else
341
- results[mapped['sku']] = [mapped]
342
- end
343
- end
344
-
345
- results
346
- end
347
-
348
- def process_product_contributor_query(contributors)
349
- results = {}
350
- contributors.each do |contributor|
351
- mapped = response_mapper(contributor, {
352
- 'ProdMkt_Contrib_Link.ProdMkt_Contrib_ID' => 'contributor_id',
353
- 'ProdMkt_Contrib_Link.ProdMkt_ID' => 'product_marketing_id',
354
- 'ProdMkt_Contrib_Link.Inactive' => 'inactive',
355
- })
356
-
357
- if mapped['inactive'] == '0'
358
-
359
- if results[mapped['product_marketing_id']]
360
- results[mapped['product_marketing_id']].push(mapped)
361
- else
362
- results[mapped['product_marketing_id']] = [mapped]
363
- end
364
- end
365
- end
366
- results
367
- end
368
-
369
- def process_contributor_types_query(types)
370
- results = {}
371
- types.each do |type|
372
- mapped = response_mapper(type, {
373
- 'ProdMkt_Contributor.ID' => 'contributor_id',
374
- 'ProdMkt_Contributor.Contrib_Type' => 'type',
375
- })
376
-
377
- if !results[mapped['contributor_id']]
378
- results[mapped['contributor_id']] = mapped['type']
379
- end
380
- end
381
-
382
- results
383
- end
384
-
385
- def process_products_and_variants(products, variants, links, physical_formats, digital_formats)
386
- products_map = {}
387
- products.each { |product| products_map[product['identifier']] = product }
388
-
389
- variants_map = {}
390
- variants.each { |variant| variants_map[variant['identifier']] = variant }
391
-
392
- links.each do |link|
393
- from_id = link['from_id']
394
- to_id = link['to_id']
395
- if variants_map.key?(to_id)
396
- variant = variants_map[to_id]
397
- variant['isDefault'] = false
398
- products_map[from_id]['model'].push(*variant['model'])
399
- end
400
- end
401
-
402
- result = []
403
- products_map.each_value { |p| result.push(p) }
404
-
405
- result.each do |product|
406
- if product['model'].length == 1
407
- product['model'][0]['isDefault'] = true
408
- set_base_sku(product, product['model'][0]['sku'])
409
- next
410
- else
411
- physical_formats.each do |val|
412
- match = product['model'].select { |v| v['acumenAttributes']['category'] == val }
413
-
414
- if match && match.length > 0
415
- match[0]['isDefault'] = true
416
- break
417
- end
418
- end
419
-
420
- digital_formats.each do |val|
421
- match = product['model'].select { |v| v['acumenAttributes']['category'] == val }
422
-
423
- if match && match.length > 0
424
- match[0]['isDefault'] = true
425
- break
426
- end
427
- end
428
- end
429
-
430
- model_ids = product['model'].map { |m| m['identifier'] }
431
- primary_variant = product['model'].select { |m| m['identifier'] == model_ids.min }.first
432
-
433
- # Set the base SKU to the SKU of the oldest record.
434
- # The base_sku property is designed to be a system value specific to the inegration using this agent.
435
- # As a result, we don't particularly care what that value is so long as we can retrieve it consistently
436
- # across executions. If a paperback product is created first, this will always return that product's SKU
437
- # as the base. This gives us a consistent way to link Acumen products to an external system where database
438
- # IDs may not match.
439
- set_base_sku(product, primary_variant ? primary_variant['sku'] : product['model'][0]['sku'])
440
- end
441
- result
442
- end
443
-
444
- private
445
-
446
- def response_mapper(data, map)
447
- result = {}
448
- map.each do |key,val|
449
- result[val] = field_value(data, key)
450
- end
451
-
452
- result
453
- end
454
-
455
- def field_value(field, key)
456
- field[key]['__content__'] if field[key]
457
- end
458
-
459
- def set_base_sku(product, sku)
460
-
461
- product['additionalProperty'].push({
462
- '@type' => 'PropertyValue',
463
- 'propertyID' => 'baseSku',
464
- 'name' => 'Base SKU',
465
- 'value' => sku,
466
- })
467
-
468
- product
469
- end
470
-
471
- def quantitative_value(value, unit)
472
- {
473
- '@type' => 'QuantitativeValue',
474
- 'value' => value,
475
- 'unitText' => unit,
476
- 'unitCode' => (UNIT_MAP[unit] if unit),
477
- } if value
478
- end
479
- end