huginn_acumen_product_agent 1.7.3 → 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.
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.3
4
+ version: 2.0.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-12-03 00:00:00.000000000 Z
11
+ date: 2021-01-14 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/agent_error_concern.rb
71
+ - lib/huginn_acumen_product_agent/concerns/alternate_products_query_concern.rb
72
+ - lib/huginn_acumen_product_agent/concerns/inv_product_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,511 +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_master_products_by_id(client, products)
75
- master_products = []
76
-
77
- products.each do |product|
78
- wrapper_id = product['identifier']
79
- master_id = 0
80
- product['model'].each do |variant|
81
- if variant['acumenAttributes']['is_master'] && variant['isAvailableForPurchase']
82
- master_id = variant['identifier']
83
- end
84
- end
85
- if wrapper_id == master_id || master_id == 0
86
- master_products.push(product)
87
- else
88
- if (master_products.find { |p| p['identifier'] == master_id }).nil?
89
- reloaded_product = get_products_by_ids(client, [master_id.to_s])[0]
90
-
91
- unless reloaded_product.nil?
92
- reloaded_product['model'] = product['model']
93
- reloaded_product['additionalProperty'].push(product['additionalProperty'].select { |m| m['propertyID'] == 'baseSku'}[0])
94
- master_products.push(reloaded_product)
95
- end
96
- end
97
- end
98
- end
99
-
100
- master_products
101
- end
102
-
103
- def get_product_variants(acumen_client, products, physical_formats, digital_formats)
104
- ids = products.map { |product| product['identifier'] }
105
- # fetch product/variant relationships
106
- variant_links = get_variants_for_ids(acumen_client, ids)
107
- variant_ids = variant_links.map { |link| link['to_id'] }
108
-
109
- variant_ids = variant_links.map { |link| link['to_id'] }
110
-
111
- # fetch product variants
112
- variants = get_products_by_ids(acumen_client, variant_ids)
113
-
114
- # merge variants and products together
115
- process_products_and_variants(products, variants, variant_links, physical_formats, digital_formats)
116
- end
117
-
118
- def get_product_categories(acumen_client, products)
119
- # fetch categories
120
- categories_map = {}
121
-
122
- skus = products.map { |product| product['model'].map { |m| m['sku'] } }
123
- skus.each do |sku_set|
124
- sku_set.each do |sku|
125
- response = acumen_client.get_product_categories([sku])
126
- categories = process_product_categories_query(response)
127
- categories_map[sku] = categories != {} ? categories[sku] : []
128
- end
129
- end
130
-
131
- # map categories to products
132
- products.each do |product|
133
- product['model'].each do |variant|
134
- variant['categories'] = []
135
- categories = categories_map[variant['sku']].select { |c| c['inactive'] == '0' }
136
- categories.map do |c|
137
- variant['categories'].push({
138
- '@type' => 'Thing',
139
- 'identifier' => c['category_id']
140
- })
141
- end
142
- end
143
- end
144
-
145
- products
146
- end
147
-
148
- def parse_product_request(products)
149
- products.map do |p|
150
- variant = response_mapper(p, {
151
- 'Inv_Product.ID' => 'identifier',
152
- 'Inv_Product.ProdCode' => 'sku',
153
- 'Inv_Product.SubTitle' => 'disambiguatingDescription',
154
- 'Inv_Product.ISBN_UPC' => 'isbn',
155
- 'Inv_Product.Pub_Date' => 'datePublished',
156
- 'Inv_Product.Next_Release' => 'releaseDate',
157
- })
158
- variant['@type'] = 'ProductModel'
159
- variant['isDefault'] = false
160
- variant['isTaxable'] = field_value(p, 'Inv_Product.Taxable') == '1'
161
- variant['isAvailableForPurchase'] = field_value(p, 'Inv_Product.Not_On_Website') == '0'
162
- variant['acumenAttributes'] = {
163
- 'is_master' => field_value(p, 'Inv_Product.OnWeb_LinkOnly') == '0'
164
- }
165
-
166
- variant['offers'] = [{
167
- '@type' => 'Offer',
168
- 'price' => field_value(p, 'Inv_Product.Price_1'),
169
- 'availability' => field_value(p, 'Inv_Product.BO_Reason')
170
- }]
171
- if field_value(p, 'Inv_Product.Price_2')
172
- variant['offers'].push({
173
- '@type' => 'Offer',
174
- 'price' => field_value(p, 'Inv_Product.Price_2'),
175
- 'availability' => field_value(p, 'Inv_Product.BO_Reason')
176
- })
177
- end
178
-
179
- weight = field_value(p, 'Inv_Product.Weight')
180
- variant['weight'] = quantitative_value(weight, 'oz.')
181
-
182
- product = {
183
- '@type' => 'Product',
184
- 'identifier' => variant['identifier'],
185
- 'sku' => variant['sku'],
186
- 'name' => field_value(p, 'Inv_Product.Full_Title'),
187
- 'disambiguatingDescription' => field_value(p, 'Inv_Product.SubTitle'),
188
- 'model' => [
189
- variant
190
- ],
191
- 'additionalProperty' => [],
192
- 'acumenAttributes' => {
193
- 'info_alpha_1' => field_value(p, 'Inv_Product.Info_Alpha_1'),
194
- 'info_boolean_1' => field_value(p, 'Inv_Product.Info_Boolean_1'),
195
- },
196
- 'isAvailableForPurchase' => field_value(p, variant['isAvailableForPurchase']),
197
- }
198
-
199
- category = field_value(p, 'Inv_Product.Category')
200
- if category
201
-
202
- if variant['acumenAttributes']
203
- variant['acumenAttributes']['category'] = category
204
- else
205
- variant['acumenAttributes'] = { 'category' => category }
206
- end
207
-
208
- if category == 'Paperback'
209
- product['additionalType'] = variant['additionalType'] = 'Book'
210
- variant['bookFormat'] = "http://schema.org/Paperback"
211
- variant['accessMode'] = "textual"
212
- variant['isDigital'] = false
213
- elsif category == 'Hardcover'
214
- product['additionalType'] = variant['additionalType'] = 'Book'
215
- variant['bookFormat'] = "http://schema.org/Hardcover"
216
- variant['accessMode'] = "textual"
217
- variant['isDigital'] = false
218
- elsif category == 'eBook'
219
- product['additionalType'] = variant['additionalType'] = 'Book'
220
- variant['bookFormat'] = "http://schema.org/EBook"
221
- variant['accessMode'] = "textual"
222
- variant['isDigital'] = true
223
- elsif category == 'CD'
224
- product['additionalType'] = variant['additionalType'] = 'CreativeWork'
225
- variant['accessMode'] = "auditory"
226
- variant['isDigital'] = false
227
- else
228
- variant['isDigital'] = false
229
- end
230
- end
231
-
232
- product
233
- end
234
- end
235
-
236
- def process_linked_product_query(links)
237
- links.map do |link|
238
- response_mapper(link, {
239
- 'Product_Link.Link_From_ID' => 'from_id',
240
- 'Product_Link.Link_To_ID' => 'to_id',
241
- 'Product_Link.Alt_Format' => 'alt_format',
242
- })
243
- end
244
- end
245
-
246
- def parse_product_marketing_request(products)
247
- results = {}
248
- products.each do |product|
249
- mapped = response_mapper(product, {
250
- 'ProdMkt.Product_ID' => 'product_id',
251
- 'ProdMkt.Product_Code' => 'sku',
252
- 'ProdMkt.ID' => 'id',
253
- 'ProdMkt.Pages' => 'pages',
254
- 'ProdMkt.Publisher' => 'publisher',
255
- 'ProdMkt.Description_Short' => 'description_short',
256
- 'ProdMkt.Description_Long' => 'description_long',
257
- 'ProdMkt.Height' => 'height',
258
- 'ProdMkt.Width' => 'width',
259
- 'ProdMkt.Thickness' => 'depth',
260
- 'ProdMkt.Meta_Keywords' => 'meta_keywords',
261
- 'ProdMkt.Meta_Description' => 'meta_description',
262
- 'ProdMkt.Extent_Unit' => 'extent_unit',
263
- 'ProdMkt.Extent_Value' => 'extent_value',
264
- 'ProdMkt.Age_Highest' => 'age_highest',
265
- 'ProdMkt.Age_Lowest' => 'age_lowest',
266
- 'ProdMkt.Awards' => 'awards',
267
- 'ProdMkt.Dimensions_Unit_Measure' => 'dimensions_unit_measure',
268
- 'ProdMkt.Excerpt' => 'excerpt',
269
- 'ProdMkt.Grade_Highest' => 'grade_highest',
270
- 'ProdMkt.Grade_Lowest' => 'grade_lowest',
271
- 'ProdMkt.Status' => 'status',
272
- 'ProdMkt.UPC' => 'upc',
273
- 'ProdMkt.Weight_Unit_Measure' => 'weight_unit_measure',
274
- 'ProdMkt.Weight' => 'weight',
275
- 'ProdMkt.Info_Text_01' => 'info_text_01',
276
- 'ProdMkt.Info_Text_02' => 'info_text_02',
277
- 'ProdMkt.Religious_Text_Identifier' => 'religious_text_identifier',
278
- 'ProdMkt.Info_Alpha_07' => 'info_alpha_07',
279
- })
280
-
281
- results[mapped['product_id']] = mapped
282
- end
283
-
284
- results
285
- end
286
-
287
- def merge_products_and_marketing(products, product_marketing)
288
- products.each do |product|
289
- marketing = product_marketing[product['identifier']]
290
- if marketing
291
- product['acumenAttributes']['product_marketing_id'] = marketing['id']
292
-
293
- product['publisher'] = {
294
- '@type': 'Organization',
295
- 'name' => marketing['publisher']
296
- };
297
- product['description'] = marketing['description_long']
298
- product['abstract'] = marketing['description_short']
299
- product['keywords'] = marketing['meta_keywords']
300
- product['text'] = marketing['excerpt']
301
-
302
- if marketing['age_lowest'] || marketing['age_highest']
303
- product['typicalAgeRange'] = "#{marketing['age_lowest']}-#{marketing['age_highest']}"
304
- end
305
-
306
- # properties for product pages
307
- if marketing['grade_lowest'] || marketing['grade_highest']
308
- # educationalUse? educationalAlignment?
309
- product['additionalProperty'].push({
310
- '@type' => 'PropertyValue',
311
- 'name' => 'Grade',
312
- 'propertyID' => 'grade_range',
313
- 'minValue' => marketing['grade_lowest'],
314
- 'maxValue' => marketing['grade_highest'],
315
- 'value' => "#{marketing['grade_lowest']}-#{marketing['grade_highest']}",
316
- })
317
- end
318
- if marketing['awards']
319
- product['additionalProperty'].push({
320
- '@type' => 'PropertyValue',
321
- 'propertyID' => 'awards',
322
- 'name' => 'Awards',
323
- 'value' => marketing['awards'],
324
- })
325
- end
326
-
327
- # acumen specific properties
328
- product['acumenAttributes']['extent_unit'] = marketing['extent_unit']
329
- product['acumenAttributes']['extent_value'] = marketing['extent_value']
330
- product['acumenAttributes']['info_text_01'] = marketing['info_text_01']
331
- product['acumenAttributes']['info_text_02'] = marketing['info_text_02']
332
- product['acumenAttributes']['info_alpha_07'] = marketing['info_alpha_07']
333
- product['acumenAttributes']['meta_description'] = marketing['meta_description']
334
- product['acumenAttributes']['religious_text_identifier'] = marketing['religious_text_identifier']
335
- product['acumenAttributes']['status'] = marketing['status']
336
-
337
- variant = product['model'][0]
338
- variant['gtin12'] = marketing['upc']
339
- variant['numberOfPages'] = marketing['pages']
340
-
341
- variant['height'] = quantitative_value(
342
- marketing['height'], marketing['dimensions_unit_measure']
343
- )
344
- variant['width'] = quantitative_value(
345
- marketing['width'], marketing['dimensions_unit_measure']
346
- )
347
- variant['depth'] = quantitative_value(
348
- marketing['thickness'], marketing['dimensions_unit_measure']
349
- )
350
- if variant['weight']['value'] == '0'
351
- variant['weight'] = quantitative_value(
352
- marketing['weight'], marketing['weight_unit_measure']
353
- )
354
- end
355
- end
356
- end
357
-
358
- products
359
- end
360
-
361
- def process_product_categories_query(categories)
362
- results = {}
363
- categories.each do |category|
364
- mapped = response_mapper(category, {
365
- 'ProdMkt_WPC.ProdCode' => 'sku',
366
- 'ProdMkt_WPC.WPC_ID' => 'category_id',
367
- 'ProdMkt_WPC.Inactive' => 'inactive',
368
- })
369
-
370
- if results[mapped['sku']]
371
- results[mapped['sku']].push(mapped)
372
- else
373
- results[mapped['sku']] = [mapped]
374
- end
375
- end
376
-
377
- results
378
- end
379
-
380
- def process_product_contributor_query(contributors)
381
- results = {}
382
- contributors.each do |contributor|
383
- mapped = response_mapper(contributor, {
384
- 'ProdMkt_Contrib_Link.ProdMkt_Contrib_ID' => 'contributor_id',
385
- 'ProdMkt_Contrib_Link.ProdMkt_ID' => 'product_marketing_id',
386
- 'ProdMkt_Contrib_Link.Inactive' => 'inactive',
387
- })
388
-
389
- if mapped['inactive'] == '0'
390
-
391
- if results[mapped['product_marketing_id']]
392
- results[mapped['product_marketing_id']].push(mapped)
393
- else
394
- results[mapped['product_marketing_id']] = [mapped]
395
- end
396
- end
397
- end
398
- results
399
- end
400
-
401
- def process_contributor_types_query(types)
402
- results = {}
403
- types.each do |type|
404
- mapped = response_mapper(type, {
405
- 'ProdMkt_Contributor.ID' => 'contributor_id',
406
- 'ProdMkt_Contributor.Contrib_Type' => 'type',
407
- })
408
-
409
- if !results[mapped['contributor_id']]
410
- results[mapped['contributor_id']] = mapped['type']
411
- end
412
- end
413
-
414
- results
415
- end
416
-
417
- def process_products_and_variants(products, variants, links, physical_formats, digital_formats)
418
- products_map = {}
419
- products.each { |product| products_map[product['identifier']] = product }
420
-
421
- variants_map = {}
422
- variants.each { |variant| variants_map[variant['identifier']] = variant }
423
-
424
- links.each do |link|
425
- from_id = link['from_id']
426
- to_id = link['to_id']
427
- if variants_map.key?(to_id)
428
- variant = variants_map[to_id]
429
- variant['isDefault'] = false
430
- products_map[from_id]['model'].push(*variant['model'])
431
- end
432
- end
433
-
434
- result = []
435
- products_map.each_value { |p| result.push(p) }
436
-
437
- result.each do |product|
438
- if product['model'].length == 1
439
- product['model'][0]['isDefault'] = true
440
- set_base_sku(product, product['model'][0]['sku'])
441
- next
442
- else
443
- physical_formats.each do |val|
444
- match = product['model'].select { |v| v['acumenAttributes']['category'] == val }
445
-
446
- if match && match.length > 0
447
- match[0]['isDefault'] = true
448
- break
449
- end
450
- end
451
-
452
- digital_formats.each do |val|
453
- match = product['model'].select { |v| v['acumenAttributes']['category'] == val }
454
-
455
- if match && match.length > 0
456
- match[0]['isDefault'] = true
457
- break
458
- end
459
- end
460
- end
461
-
462
- model_ids = product['model'].map { |m| m['identifier'] }
463
- primary_variant = product['model'].select { |m| m['identifier'] == model_ids.min }.first
464
-
465
- # Set the base SKU to the SKU of the oldest record.
466
- # The base_sku property is designed to be a system value specific to the inegration using this agent.
467
- # As a result, we don't particularly care what that value is so long as we can retrieve it consistently
468
- # across executions. If a paperback product is created first, this will always return that product's SKU
469
- # as the base. This gives us a consistent way to link Acumen products to an external system where database
470
- # IDs may not match.
471
- set_base_sku(product, primary_variant ? primary_variant['sku'] : product['model'][0]['sku'])
472
- end
473
- result
474
- end
475
-
476
- private
477
-
478
- def response_mapper(data, map)
479
- result = {}
480
- map.each do |key,val|
481
- result[val] = field_value(data, key)
482
- end
483
-
484
- result
485
- end
486
-
487
- def field_value(field, key)
488
- field[key]['__content__'] if field[key]
489
- end
490
-
491
- def set_base_sku(product, sku)
492
-
493
- product['additionalProperty'].push({
494
- '@type' => 'PropertyValue',
495
- 'propertyID' => 'baseSku',
496
- 'name' => 'Base SKU',
497
- 'value' => sku,
498
- })
499
-
500
- product
501
- end
502
-
503
- def quantitative_value(value, unit)
504
- {
505
- '@type' => 'QuantitativeValue',
506
- 'value' => value,
507
- 'unitText' => unit,
508
- 'unitCode' => (UNIT_MAP[unit] if unit),
509
- } if value
510
- end
511
- end