huginn_acumen_product_agent 1.7.3 → 2.3.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,144 @@
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
+ # Nullify blank dates
37
+ if product['datePublished'] == '0000-00-00T00:00:00'
38
+ product['datePublished'] = nil
39
+ end
40
+
41
+ if product['releaseDate'] == '0000-00-00T00:00:00'
42
+ product['releaseDate'] = nil
43
+ end
44
+
45
+ product['@type'] = 'Product'
46
+ product['isTaxable'] = get_field_value(p, 'Inv_Product.Taxable') == '1'
47
+ product['productAvailability'] = get_field_value(p, 'Inv_Product.Not_On_Website') == '0'
48
+ product['noBackorderFill'] = get_field_value(p, 'Inv_Product.No_Backorder_Fill') == '1'
49
+ product['trackInventory'] = !(get_field_value(p, 'Inv_Product.Non_Inventory') == '1' || get_field_value(p, 'Inv_Product.Assembly') == '1')
50
+ product['acumenAttributes'] = {
51
+ 'info_alpha_1' => get_field_value(p, 'Inv_Product.Info_Alpha_1'),
52
+ 'info_boolean_1' => get_field_value(p, 'Inv_Product.Info_Boolean_1'), # is_available_on_formed
53
+ }
54
+ product['additionalProperty'] = [
55
+ {
56
+ '@type' => 'PropertyValue',
57
+ 'propertyID' => 'is_master',
58
+ 'value' => get_field_value(p, 'Inv_Product.OnWeb_LinkOnly') == '0',
59
+ },
60
+ {
61
+ # NOTE: This is different than isAvailableForPurchase. This
62
+ '@type' => 'PropertyValue',
63
+ 'propertyID' => 'disable_web_purchase',
64
+ 'value' => get_field_value(p, 'Inv_Product.Disable_Web_Purchase'),
65
+ }
66
+ ]
67
+
68
+ product['offers'] = [{
69
+ '@type' => 'Offer',
70
+ 'price' => get_field_value(p, 'Inv_Product.Price_1'),
71
+ 'availability' => get_field_value(p, 'Inv_Product.BO_Reason')
72
+ }]
73
+
74
+ if get_field_value(p, 'Inv_Product.Price_2')
75
+ product['offers'].push({
76
+ '@type' => 'Offer',
77
+ 'price' => get_field_value(p, 'Inv_Product.Price_2'),
78
+ 'availability' => get_field_value(p, 'Inv_Product.BO_Reason')
79
+ })
80
+ end
81
+
82
+ not_on_website = get_field_value(p, 'Inv_Product.Not_On_Website')
83
+ disable_web_purchase = get_field_value(p, 'Inv_Product.Disable_Web_Purchase')
84
+
85
+ product_availability = 'available'
86
+
87
+
88
+ if (disable_web_purchase == '1')
89
+ product_availability = 'disabled'
90
+ end
91
+
92
+ if (not_on_website == '1')
93
+ product_availability = 'not available'
94
+ end
95
+
96
+ product['productAvailability'] = product_availability
97
+
98
+ weight = get_field_value(p, 'Inv_Product.Weight')
99
+ product['weight'] = get_quantitative_value(weight, 'oz.')
100
+
101
+ # The category used here is the Acumen Product category. Functionally, this
102
+ # serves as the product's _format_. This field is different from Web Categories
103
+ # which behave more like traditional category taxonomies.
104
+ category = get_field_value(p, 'Inv_Product.Category')
105
+ if category
106
+ product['acumenAttributes']['category'] = category
107
+ product['isDigital'] = digital_format_list.find { |f| f == category } ? true : false
108
+
109
+ if category == 'Paperback'
110
+ product['additionalType'] = 'Book'
111
+ product['bookFormat'] = "http://schema.org/Paperback"
112
+ product['accessMode'] = "textual"
113
+ elsif category == 'Hardcover'
114
+ product['additionalType'] = 'Book'
115
+ product['bookFormat'] = "http://schema.org/Hardcover"
116
+ product['accessMode'] = "textual"
117
+ elsif category == 'eBook'
118
+ product['additionalType'] = 'Book'
119
+ product['bookFormat'] = "http://schema.org/EBook"
120
+ product['accessMode'] = "textual"
121
+ elsif category == 'CD'
122
+ product['additionalType'] = 'CreativeWork'
123
+ product['accessMode'] = "auditory"
124
+ end
125
+ end
126
+
127
+ product
128
+
129
+ rescue => error
130
+ issue_error(AcumenAgentError.new(
131
+ 'process_inv_product_response',
132
+ 'Failed to load Inventory Product Records',
133
+ {
134
+ product_id: get_field_value(p, 'Inv_Product.ID'),
135
+ sku: get_field_value(p, 'Inv_Product.ProdCode')
136
+ },
137
+ error,
138
+ ))
139
+
140
+ return
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module is responsible for reading/processing the Inv_Status table. This table
4
+ # contains stock information for the product and is tied to product availability.
5
+ module ProdMktQueryConcern
6
+ extend AcumenQueryConcern
7
+
8
+ # Update the provided products with their associated marketing data
9
+ # NOTE: The `products` here are Shema.org/Product records mapped from Inv_Product
10
+ # data
11
+ def fetch_inv_status(acumen_client, products)
12
+
13
+ product_skus = products.map { |p| p['sku'] }
14
+ inventory_data = acumen_client.get_inv_status(product_skus)
15
+ inventory_data = process_inv_status_response(inventory_data)
16
+
17
+ return map_inv_status_data(products, inventory_data)
18
+ end
19
+
20
+ # This function parses the raw data returned from the Prod_Mkt table
21
+ def process_inv_status_response(raw_data)
22
+ results = []
23
+ raw_data.map do |inv_status|
24
+
25
+ begin
26
+ mapped = response_mapper(inv_status, {
27
+ 'Inv_Status.Warehouse' => 'warehouse',
28
+ 'Inv_Status.ProdCode' => 'sku',
29
+ 'Inv_Status.Available' => 'quantity',
30
+ })
31
+
32
+ results << mapped
33
+ rescue => error
34
+ issue_error(AcumenAgentError.new(
35
+ 'process_inv_status_response',
36
+ 'Failed while processing Prod_Mkt record',
37
+ { sku: get_field_value(inv_status, 'Inv_Status.ProdCode') },
38
+ error,
39
+ ))
40
+ end
41
+ end
42
+
43
+ results
44
+ end
45
+
46
+ # This function maps parsed Prod_Mkt records to their matching product record
47
+ # and updates the product object with the additional data
48
+ def map_inv_status_data(products, inventory)
49
+ products.map do |product|
50
+ inventory_data = inventory.select { |i| i['sku'] == product['sku'] }
51
+ begin
52
+
53
+ if inventory_data
54
+ quantity = 0
55
+ inventory_data.each do |i|
56
+ quantity = quantity + i['quantity'].to_i if quantity.present?
57
+ end
58
+
59
+ product['acumenAttributes']['stock_quantity'] = quantity
60
+ end
61
+
62
+ rescue => error
63
+ issue_error(AcumenAgentError.new(
64
+ 'map_inv_status_data',
65
+ 'Failed to map inventory data for product',
66
+ { sku: product['sku'] },
67
+ error,
68
+ ))
69
+ end
70
+
71
+ product
72
+ end
73
+
74
+ return products
75
+ end
76
+ 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' => 'thickness',
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
+ { sku: get_field_value(product_marketing, 'ProdMkt.Product_Code') },
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
+ { id: product['identifier'], sku: marketing['sku'] },
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_error(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