huginn_acumen_product_agent 1.6.1 → 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.
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is responsible for reading/processing data recrods from the Inv_Product
4
+ # table in Acumen. This table contains the baseline product information -- name, sku,
5
+ # format, etc.
6
+ module InvProductQueryConcern
7
+ extend AcumenQueryConcern
8
+
9
+ # Fetch/Process the Acumen data,
10
+ def fetch_inv_product_data(acumen_client, product_ids, digital_format_list)
11
+ product_data = acumen_client.get_products(product_ids)
12
+
13
+ return process_inv_product_response(product_data, digital_format_list)
14
+ end
15
+
16
+ # This function returns an array of Acumen products mapped to Schema.org/Product
17
+ # objects. We've added additional fields of:
18
+ #
19
+ # * `isAvailableForPurchase` -- used to control product deletion in external systems
20
+ # * `acumenAttributes` -- Additional acumen data that doesn't have a direct 1:1 field
21
+ # on the Product, but may be useful in other platforms
22
+ def process_inv_product_response(raw_data, digital_format_list)
23
+ raw_data.map do |p|
24
+
25
+ begin
26
+ product = response_mapper(p, {
27
+ 'Inv_Product.ID' => 'identifier',
28
+ 'Inv_Product.ProdCode' => 'sku',
29
+ 'Inv_Product.Full_Title' => 'name',
30
+ 'Inv_Product.SubTitle' => 'disambiguatingDescription',
31
+ 'Inv_Product.ISBN_UPC' => 'isbn',
32
+ 'Inv_Product.Pub_Date' => 'datePublished',
33
+ 'Inv_Product.Next_Release' => 'releaseDate',
34
+ })
35
+
36
+ product['@type'] = 'Product'
37
+ product['isTaxable'] = get_field_value(p, 'Inv_Product.Taxable') == '1'
38
+ product['isAvailableForPurchase'] = get_field_value(p, 'Inv_Product.Not_On_Website') == '0'
39
+ product['acumenAttributes'] = {
40
+ 'info_alpha_1' => get_field_value(p, 'Inv_Product.Info_Alpha_1'),
41
+ 'info_boolean_1' => get_field_value(p, 'Inv_Product.Info_Boolean_1'), # is_available_on_formed
42
+ }
43
+ product['additionalProperty'] = [
44
+ {
45
+ '@type' => 'PropertyValue',
46
+ 'propertyID' => 'is_master',
47
+ 'value' => get_field_value(p, 'Inv_Product.OnWeb_LinkOnly') == '0',
48
+ }
49
+ ]
50
+
51
+ product['offers'] = [{
52
+ '@type' => 'Offer',
53
+ 'price' => get_field_value(p, 'Inv_Product.Price_1'),
54
+ 'availability' => get_field_value(p, 'Inv_Product.BO_Reason')
55
+ }]
56
+
57
+ if get_field_value(p, 'Inv_Product.Price_2')
58
+ product['offers'].push({
59
+ '@type' => 'Offer',
60
+ 'price' => get_field_value(p, 'Inv_Product.Price_2'),
61
+ 'availability' => get_field_value(p, 'Inv_Product.BO_Reason')
62
+ })
63
+ end
64
+
65
+ weight = get_field_value(p, 'Inv_Product.Weight')
66
+ product['weight'] = get_quantitative_value(weight, 'oz.')
67
+
68
+ # The category used here is the Acumen Product category. Functionally, this
69
+ # serves as the product's _format_. This field is different from Web Categories
70
+ # which behave more like traditional category taxonomies.
71
+ category = get_field_value(p, 'Inv_Product.Category')
72
+ if category
73
+ product['acumenAttributes']['category'] = category
74
+ product['isDigital'] = digital_format_list.find { |f| f == category } ? true : false
75
+
76
+ if category == 'Paperback'
77
+ product['additionalType'] = 'Book'
78
+ product['bookFormat'] = "http://schema.org/Paperback"
79
+ product['accessMode'] = "textual"
80
+ elsif category == 'Hardcover'
81
+ product['additionalType'] = 'Book'
82
+ product['bookFormat'] = "http://schema.org/Hardcover"
83
+ product['accessMode'] = "textual"
84
+ elsif category == 'eBook'
85
+ product['additionalType'] = 'Book'
86
+ product['bookFormat'] = "http://schema.org/EBook"
87
+ product['accessMode'] = "textual"
88
+ elsif category == 'CD'
89
+ product['additionalType'] = 'CreativeWork'
90
+ product['accessMode'] = "auditory"
91
+ end
92
+ end
93
+
94
+ product
95
+
96
+ rescue => error
97
+ issue_error(AcumenAgentError.new(
98
+ 'process_inv_product_response',
99
+ 'Failed to load Inventory Product Records',
100
+ { product_ids: product_ids },
101
+ error,
102
+ ))
103
+
104
+ return
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is responsible for reading/processing the ProdMkt table. This table
4
+ # contains more detailed product meta information: Publisher, page counts, age range,
5
+ # description, etc.
6
+ module ProdMktQueryConcern
7
+ extend AcumenQueryConcern
8
+
9
+ # Update the provided products with their associated marketing data
10
+ # NOTE: The `products` here are Shema.org/Product records mapped from Inv_Product
11
+ # data
12
+ def fetch_product_marketing(acumen_client, products)
13
+
14
+ product_ids = products.map { |p| p['identifier'] }
15
+ marketing_data = acumen_client.get_products_marketing(product_ids)
16
+ marketing_data = process_prod_mkt_response(marketing_data)
17
+
18
+ return map_marketing_data(products, marketing_data)
19
+ end
20
+
21
+ # This function parses the raw data returned from the Prod_Mkt table
22
+ def process_prod_mkt_response(raw_data)
23
+ results = {}
24
+ raw_data.map do |product_marketing|
25
+
26
+ begin
27
+ mapped = response_mapper(product_marketing, {
28
+ 'ProdMkt.Product_ID' => 'product_id',
29
+ 'ProdMkt.Product_Code' => 'sku',
30
+ 'ProdMkt.ID' => 'id',
31
+ 'ProdMkt.Pages' => 'pages',
32
+ 'ProdMkt.Publisher' => 'publisher',
33
+ 'ProdMkt.Description_Short' => 'description_short',
34
+ 'ProdMkt.Description_Long' => 'description_long',
35
+ 'ProdMkt.Height' => 'height',
36
+ 'ProdMkt.Width' => 'width',
37
+ 'ProdMkt.Thickness' => 'depth',
38
+ 'ProdMkt.Meta_Keywords' => 'meta_keywords',
39
+ 'ProdMkt.Meta_Description' => 'meta_description',
40
+ 'ProdMkt.Extent_Unit' => 'extent_unit',
41
+ 'ProdMkt.Extent_Value' => 'extent_value',
42
+ 'ProdMkt.Age_Highest' => 'age_highest',
43
+ 'ProdMkt.Age_Lowest' => 'age_lowest',
44
+ 'ProdMkt.Awards' => 'awards',
45
+ 'ProdMkt.Dimensions_Unit_Measure' => 'dimensions_unit_measure',
46
+ 'ProdMkt.Excerpt' => 'excerpt',
47
+ 'ProdMkt.Grade_Highest' => 'grade_highest',
48
+ 'ProdMkt.Grade_Lowest' => 'grade_lowest',
49
+ 'ProdMkt.Status' => 'status',
50
+ 'ProdMkt.UPC' => 'upc',
51
+ 'ProdMkt.Weight_Unit_Measure' => 'weight_unit_measure',
52
+ 'ProdMkt.Weight' => 'weight',
53
+ 'ProdMkt.Info_Text_01' => 'info_text_01',
54
+ 'ProdMkt.Info_Text_02' => 'info_text_02',
55
+ 'ProdMkt.Religious_Text_Identifier' => 'religious_text_identifier',
56
+ 'ProdMkt.Info_Alpha_07' => 'info_alpha_07',
57
+ })
58
+
59
+ results[mapped['product_id']] = mapped
60
+ # NOTE: In this case, product_id matches the Inv_Product.ID fields
61
+ rescue => error
62
+ issue_error(AcumenAgentError.new(
63
+ 'process_prod_mkt_response',
64
+ 'Failed while processing Prod_Mkt record',
65
+ product_marketing,
66
+ error,
67
+ ))
68
+ end
69
+ end
70
+
71
+ results
72
+ end
73
+
74
+ # This function maps parsed Prod_Mkt records to their matching product record
75
+ # and updates the product object with the additional data
76
+ def map_marketing_data(products, marketing_data)
77
+ products.map do |product|
78
+ marketing = marketing_data[product['identifier']]
79
+
80
+ begin
81
+
82
+ if marketing
83
+ product['acumenAttributes']['product_marketing_id'] = marketing['id']
84
+
85
+ product['publisher'] = {
86
+ '@type': 'Organization',
87
+ 'name' => marketing['publisher']
88
+ };
89
+ product['description'] = marketing['description_long']
90
+ product['abstract'] = marketing['description_short']
91
+ product['keywords'] = marketing['meta_keywords']
92
+ product['text'] = marketing['excerpt']
93
+
94
+ if marketing['age_lowest'] && marketing['age_highest']
95
+ product['typicalAgeRange'] = "#{marketing['age_lowest']}-#{marketing['age_highest']}"
96
+ end
97
+
98
+ #---------- Product Page Attributes ----------#
99
+ if marketing['grade_lowest'] || marketing['grade_highest']
100
+ # educationalUse? educationalAlignment?
101
+ product['additionalProperty'].push({
102
+ '@type' => 'PropertyValue',
103
+ 'name' => 'Grade',
104
+ 'propertyID' => 'grade_range',
105
+ 'minValue' => marketing['grade_lowest'],
106
+ 'maxValue' => marketing['grade_highest'],
107
+ 'value' => "#{marketing['grade_lowest']}-#{marketing['grade_highest']}",
108
+ })
109
+ end
110
+
111
+ if marketing['awards']
112
+ product['additionalProperty'].push({
113
+ '@type' => 'PropertyValue',
114
+ 'propertyID' => 'awards',
115
+ 'name' => 'Awards',
116
+ 'value' => marketing['awards'],
117
+ })
118
+ end
119
+
120
+ #---------- Acumen Specific Properties ----------#
121
+ product['acumenAttributes']['extent_unit'] = marketing['extent_unit']
122
+ product['acumenAttributes']['extent_value'] = marketing['extent_value']
123
+ product['acumenAttributes']['info_text_01'] = marketing['info_text_01'] # editorial_reviews
124
+ product['acumenAttributes']['info_text_02'] = marketing['info_text_02'] # product_samples
125
+ product['acumenAttributes']['info_alpha_07'] = marketing['info_alpha_07'] # video_urls
126
+ product['acumenAttributes']['meta_description'] = marketing['meta_description']
127
+ product['acumenAttributes']['religious_text_identifier'] = marketing['religious_text_identifier']
128
+ product['acumenAttributes']['status'] = marketing['status']
129
+
130
+ product['gtin12'] = marketing['upc']
131
+ product['numberOfPages'] = marketing['pages']
132
+
133
+ product['height'] = get_quantitative_value(
134
+ marketing['height'], marketing['dimensions_unit_measure']
135
+ )
136
+ product['width'] = get_quantitative_value(
137
+ marketing['width'], marketing['dimensions_unit_measure']
138
+ )
139
+ product['depth'] = get_quantitative_value(
140
+ marketing['thickness'], marketing['dimensions_unit_measure']
141
+ )
142
+ if product['weight']['value'] == '0'
143
+ product['weight'] = get_quantitative_value(
144
+ marketing['weight'], marketing['weight_unit_measure']
145
+ )
146
+ end
147
+ end
148
+
149
+ rescue => error
150
+ issue_error(AcumenAgentError.new(
151
+ 'map_marketing_data',
152
+ 'Failed to map marketing data for product',
153
+ { product: product, marketing: marketing },
154
+ error,
155
+ ))
156
+ end
157
+
158
+ product
159
+ end
160
+
161
+ return products
162
+ end
163
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is responsible for reading/processing data from the ProdMkt_WPC table
4
+ # NOTE: Records on this table are linked to products by SKU
5
+ module ProductCategoriesQueryConcern
6
+ extend AcumenQueryConcern
7
+
8
+ # Updates the provided products with their associated category data
9
+ def fetch_product_categories(acumen_client, products)
10
+
11
+ product_skus = products.map { |p| p['sku'] }
12
+ category_data = acumen_client.get_product_categories(product_skus)
13
+ category_data = process_product_category_response(category_data)
14
+
15
+ return map_category_data(products, category_data)
16
+ end
17
+
18
+ # This function parses the raw data returned from the ProdMkt_WPC table
19
+ # This table holds the relationship between products and Web Categories
20
+ # The resulting data contains arrays of Category records keyed to product SKUs
21
+ #
22
+ # NOTE: In the Acumen data, some Category assignments are marked as inactive.
23
+ # This function only returns _active_ categories in the resulting data.
24
+ def process_product_category_response(raw_data)
25
+ results = {}
26
+
27
+ raw_data.map do |product_category|
28
+
29
+ begin
30
+ mapped = response_mapper(product_category, {
31
+ 'ProdMkt_WPC.ProdCode' => 'sku',
32
+ 'ProdMkt_WPC.WPC_ID' => 'category_id',
33
+ 'ProdMkt_WPC.Inactive' => 'inactive',
34
+ })
35
+
36
+ product_sku = mapped['sku']
37
+
38
+ if results[product_sku]
39
+ results[product_sku].push(mapped) if mapped['inactive'] == '0'
40
+ else
41
+ results[product_sku] = [mapped] if mapped['inactive'] == '0'
42
+ end
43
+ rescue => error
44
+ issue_errpr(AcumenAgentError.new(
45
+ 'process_product_category_response',
46
+ 'Failed while processing category data',
47
+ product_category,
48
+ error,
49
+ ))
50
+ end
51
+ end
52
+
53
+ results
54
+ end
55
+
56
+ # This function maps parsed Web Category records to their matching Product
57
+ def map_category_data(products, category_data)
58
+ products.each do |product|
59
+ product['categories'] = []
60
+ categories = category_data[product['sku']]
61
+
62
+ unless categories.nil?
63
+ categories.map do |cat|
64
+ product['categories'].push({
65
+ '@type' => 'Thing',
66
+ 'identifier' => cat['category_id']
67
+ })
68
+ end
69
+ end
70
+ end
71
+
72
+ return products
73
+ end
74
+ end
@@ -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
+ { product: product, contributors: contributor_data, contributor_types: contributor_type_data },
108
+ error
109
+ ))
110
+ end
111
+ end
112
+
113
+ return products
114
+ end
115
+
116
+ end