huginn_acumen_product_agent 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a6d6febab93205d4676c198c74dc8eb8d956f775eaa3ce800387c1b84e849921
4
+ data.tar.gz: 5dd201cfb88672b67a8e4350056c30204843a9460afb2573c0e0587acabfff8f
5
+ SHA512:
6
+ metadata.gz: a1bd50ef65b8361ebfbd2b1b6e938c971790426620b4d56f62d95d1ccc7b19eca6a6f017336fd23b840d446472d65cd9f133f0e0e5635a67fb160af81c22dadb
7
+ data.tar.gz: 6931f53b778d415e2e3f4a51a242bb9a97e47fcb6692818d2e9c6489a7c96864dee5264a7fd6351d2541d07da5f25b014e30358991eb22084aeb96f17bc52c8f
data/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2020 Jacob Spizziri
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,6 @@
1
+ require 'huginn_agent'
2
+
3
+ HuginnAgent.load 'huginn_acumen_product_agent/concerns/acumen_product_query_concern'
4
+ HuginnAgent.load 'huginn_acumen_product_agent/acumen_client'
5
+
6
+ HuginnAgent.register 'huginn_acumen_product_agent/acumen_product_agent'
@@ -0,0 +1,257 @@
1
+ class AcumenClient
2
+ @faraday
3
+ @auth
4
+
5
+ def initialize(faraday, auth)
6
+ @faraday = faraday
7
+ @auth = auth
8
+ end
9
+
10
+ def get_products(ids)
11
+ body = build_product_request(ids)
12
+ response = execute_in_list_query(body, {})
13
+ get_results(response, 'Inv_Product')
14
+ end
15
+
16
+ def get_products_marketing(ids)
17
+ body = build_product_marketing_query(ids)
18
+ response = execute_in_list_query(body, {})
19
+ get_results(response, 'ProdMkt')
20
+ end
21
+
22
+ def get_linked_products(ids)
23
+ body = build_linked_product_query(ids)
24
+ response = execute_in_list_query(body, {})
25
+ get_results(response, 'Product_Link')
26
+ end
27
+
28
+ def get_product_contributors(ids)
29
+ body = build_product_contributor_link_query(ids)
30
+ response = execute_in_list_query(body, {})
31
+ get_results(response, 'ProdMkt_Contrib_Link')
32
+ end
33
+
34
+ def get_product_categories(skus)
35
+ q = build_product_categories_query(skus)
36
+ response = execute_in_list_query(q, {})
37
+ get_results(response, 'ProdMkt_WPC')
38
+ end
39
+
40
+
41
+ def execute_query(body, headers)
42
+ response = @faraday.run_request(:post, "#{@auth['endpoint']}/Query", body, headers)
43
+ ::MultiXml.parse(response.body, {})
44
+ end
45
+
46
+ def execute_in_list_query(body, headers)
47
+ response = @faraday.run_request(:post, "#{@auth['endpoint']}/QueryByInList", body, headers)
48
+ ::MultiXml.parse(response.body, {})
49
+ end
50
+
51
+ def get_results(response, name)
52
+ result_set = response['Envelope']['Body']['acusoapResponse']['result_set.' + name]
53
+ results = result_set.nil? ? [] : result_set[name]
54
+ results.is_a?(Array) ? results : [results]
55
+ end
56
+
57
+ private
58
+
59
+ def build_product_request(ids)
60
+ <<~XML
61
+ <acusoapRequest>
62
+ #{build_acumen_query_auth()}
63
+ <query>
64
+ <statement>
65
+ <column_name>Inv_Product.ID</column_name>
66
+ <comparator>in</comparator>
67
+ <value>#{ids.join(',')}</value>
68
+ </statement>
69
+ </query>
70
+ <requested_output>
71
+ <view_owner_table_name>Inv_Product</view_owner_table_name>
72
+ <view_name>Inv_ProductAllRead</view_name>
73
+ <column_name>Inv_Product.ID</column_name>
74
+ <column_name>Inv_Product.ProdCode</column_name>
75
+ <column_name>Inv_Product.Title</column_name>
76
+ <column_name>Inv_Product.Full_Title</column_name>
77
+ <column_name>Inv_Product.SubTitle</column_name>
78
+ <column_name>Inv_Product.ISBN_UPC</column_name>
79
+ <column_name>Inv_Product.Price_1</column_name>
80
+ <column_name>Inv_Product.Price_2</column_name>
81
+ <column_name>Inv_Product.Weight</column_name>
82
+ <column_name>Inv_Product.Taxable</column_name>
83
+ <column_name>Inv_Product.Pub_Date</column_name>
84
+ <column_name>Inv_Product.DateTimeStamp</column_name>
85
+ <column_name>Inv_Product.OnWeb_LinkOnly</column_name>
86
+ <column_name>Inv_Product.Download_Product</column_name>
87
+ <column_name>Inv_Product.Info_Alpha_1</column_name>
88
+ <column_name>Inv_Product.Info_Boolean_1</column_name>>
89
+ <column_name>Inv_Product.Category</column_name>
90
+ </requested_output>
91
+ </acusoapRequest>
92
+ XML
93
+ end
94
+
95
+ def build_product_marketing_query(ids)
96
+ <<~XML
97
+ <acusoapRequest>
98
+ #{build_acumen_query_auth()}
99
+ <query>
100
+ <statement>
101
+ <column_name>ProdMkt.Product_ID</column_name>
102
+ <comparator>in</comparator>
103
+ <value>#{ids.join(',')}</value>
104
+ </statement>
105
+ </query>
106
+ <requested_output>
107
+ <view_owner_table_name>ProdMkt</view_owner_table_name>
108
+ <view_name>ProdMktAllRead</view_name>
109
+ <column_name>ProdMkt.Product_ID</column_name>
110
+ <column_name>ProdMkt.Product_Code</column_name>
111
+ <column_name>ProdMkt.ID</column_name>
112
+ <column_name>ProdMkt.DateTimeStamp</column_name>
113
+ <column_name>ProdMkt.Pages</column_name>
114
+ <column_name>ProdMkt.Publisher</column_name>
115
+ <column_name>ProdMkt.Description_Short</column_name>
116
+ <column_name>ProdMkt.Description_Long</column_name>
117
+ <column_name>ProdMkt.Height</column_name>
118
+ <column_name>ProdMkt.Width</column_name>
119
+ <column_name>ProdMkt.Thickness</column_name>
120
+ <column_name>ProdMkt.Meta_Keywords</column_name>
121
+ <column_name>ProdMkt.Meta_Description</column_name>
122
+ <column_name>ProdMkt.Extent_Unit</column_name>
123
+ <column_name>ProdMkt.Extent_Value</column_name>
124
+ <column_name>ProdMkt.Age_Highest</column_name>
125
+ <column_name>ProdMkt.Age_Lowest</column_name>
126
+ <column_name>ProdMkt.Awards</column_name>
127
+ <column_name>ProdMkt.Dimensions_Unit_Measure</column_name>
128
+ <column_name>ProdMkt.Excerpt</column_name>
129
+ <column_name>ProdMkt.Grade_Highest</column_name>
130
+ <column_name>ProdMkt.Grade_Lowest</column_name>
131
+ <column_name>ProdMkt.Status</column_name>
132
+ <column_name>ProdMkt.UPC</column_name>
133
+ <column_name>ProdMkt.Weight_Unit_Measure</column_name>
134
+ <column_name>ProdMkt.Weight</column_name>
135
+ <column_name>ProdMkt.Info_Text_01</column_name>
136
+ <column_name>ProdMkt.Info_Text_02</column_name>
137
+ <column_name>ProdMkt.Religious_Text_Identifier</column_name>
138
+ </requested_output>
139
+ </acusoapRequest>
140
+ XML
141
+ end
142
+
143
+ def build_product_ids_since_request(since)
144
+ <<~XML
145
+ <acusoapRequest>
146
+ #{build_acumen_query_auth()}
147
+ <query>
148
+ <statement>
149
+ <column_name>Inv_Product.Not_Active</column_name>
150
+ <comparator>equals</comparator>
151
+ <value>false</value>
152
+ <conjunction>and</conjunction>
153
+ </statement>
154
+ <statement>
155
+ <column_name>Inv_Product.Not_On_Website</column_name>
156
+ <comparator>equals</comparator>
157
+ <value>false</value>
158
+ <conjunction>and</conjunction>
159
+ </statement>
160
+ <statement>
161
+ <column_name>Inv_Product.DateTimeStamp</column_name>
162
+ <comparator>greater than</comparator>
163
+ <value>#{since}</value>
164
+ <conjunction>and</conjunction>
165
+ </statement>
166
+ <statement>
167
+ <column_name>Inv_Product.OnWeb_LinkOnly</column_name>
168
+ <comparator>equals</comparator>
169
+ <value>false</value>
170
+ <conjunction>and</conjunction>
171
+ </statement>
172
+ </query>
173
+ <requested_output>
174
+ <view_owner_table_name>Inv_Product</view_owner_table_name>
175
+ <view_name>Inv_ProductAllRead</view_name>
176
+ <column_name>Inv_Product.ID</column_name>
177
+ </requested_output>
178
+ </acusoapRequest>
179
+ XML
180
+ end
181
+
182
+ def build_linked_product_query(ids)
183
+ <<~XML
184
+ <acusoapRequest>
185
+ #{build_acumen_query_auth()}
186
+ <query>
187
+ <statement>
188
+ <column_name>Product_Link.Link_From_ID</column_name>
189
+ <comparator>in</comparator>
190
+ <value>#{ids.join(',')}</value>
191
+ </statement>
192
+ </query>
193
+ <requested_output>
194
+ <view_owner_table_name>Product_Link</view_owner_table_name>
195
+ <view_name>Product_LinkAllRead</view_name>
196
+ <column_name>Product_Link.Link_From_ID</column_name>
197
+ <column_name>Product_Link.Link_To_ID</column_name>
198
+ <column_name>Product_Link.Alt_Format</column_name>
199
+ </requested_output>
200
+ </acusoapRequest>
201
+ XML
202
+ end
203
+
204
+ def build_product_categories_query(ids)
205
+ <<~XML
206
+ <acusoapRequest>
207
+ #{build_acumen_query_auth()}
208
+ <query>
209
+ <statement>
210
+ <column_name>ProdMkt_WPC.ProdCode</column_name>
211
+ <comparator>in</comparator>
212
+ <value>#{ids.join(',')}</value>
213
+ </statement>
214
+ </query>
215
+ <requested_output>
216
+ <view_owner_table_name>ProdMkt_WPC</view_owner_table_name>
217
+ <view_name>ProdMkt_WPCAllRead</view_name>
218
+ <column_name>ProdMkt_WPC.ProdCode</column_name>
219
+ <column_name>ProdMkt_WPC.WPC_ID</column_name>
220
+ <column_name>ProdMkt_WPC.Inactive</column_name>
221
+ </requested_output>
222
+ </acusoapRequest>
223
+ XML
224
+ end
225
+
226
+ def build_product_contributor_link_query(ids)
227
+ <<~XML
228
+ <acusoapRequest>
229
+ #{build_acumen_query_auth()}
230
+ <query>
231
+ <statement>
232
+ <column_name>ProdMkt_Contrib_Link.ProdMkt_ID</column_name>
233
+ <comparator>in</comparator>
234
+ <value>#{ids.join(',')}</value>
235
+ </statement>
236
+ </query>
237
+ <requested_output>
238
+ <view_owner_table_name>ProdMkt_Contrib_Link</view_owner_table_name>
239
+ <view_name>ProdMkt_Contrib_LinkAllRead</view_name>
240
+ <column_name>ProdMkt_Contrib_Link.ProdMkt_Contrib_ID</column_name>
241
+ <column_name>ProdMkt_Contrib_Link.ProdMkt_ID</column_name>
242
+ <column_name>ProdMkt_Contrib_Link.Inactive</column_name>
243
+ </requested_output>
244
+ </acusoapRequest>
245
+ XML
246
+ end
247
+
248
+ def build_acumen_query_auth()
249
+ <<~XML
250
+ <authentication>
251
+ <site_code>#{@auth['site_code']}</site_code>
252
+ <password>#{@auth['password']}</password>
253
+ </authentication>
254
+ <message_version>1.00</message_version>
255
+ XML
256
+ end
257
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agents
4
+ class AcumenProductAgent < Agent
5
+ include WebRequestConcern
6
+ include AcumenProductQueryConcern
7
+
8
+ default_schedule '12h'
9
+
10
+ can_dry_run!
11
+ default_schedule 'never'
12
+
13
+ description <<-MD
14
+ Huginn agent for sane ACUMEN product data.
15
+ MD
16
+
17
+ def default_options
18
+ {
19
+ 'endpoint' => 'https://example.com',
20
+ 'site_code' => '',
21
+ 'password' => '',
22
+ 'attribute_to_property' => {},
23
+ }
24
+ end
25
+
26
+ def validate_options
27
+ unless options['endpoint'].present?
28
+ errors.add(:base, 'endpoint is a required field')
29
+ end
30
+
31
+ unless options['site_code'].present?
32
+ errors.add(:base, 'site_code is a required field')
33
+ end
34
+
35
+ unless options['password'].present?
36
+ errors.add(:base, 'password is a required field')
37
+ end
38
+
39
+ unless options['attribute_to_property'].is_a?(Hash)
40
+ errors.add(:base, "if provided, attribute_to_property must be a hash")
41
+ end
42
+ end
43
+
44
+ def working?
45
+ received_event_without_error?
46
+ end
47
+
48
+ def check
49
+ handle interpolated['payload'].presence || {}
50
+ end
51
+
52
+ def receive(incoming_events)
53
+ incoming_events.each do |event|
54
+ handle(event)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def handle(event)
61
+ endpoint = interpolated['endpoint']
62
+ site_code = interpolated['site_code']
63
+ password = interpolated['password']
64
+
65
+ auth = {
66
+ 'site_code' => site_code,
67
+ 'password' => password,
68
+ 'endpoint' => endpoint,
69
+ }
70
+ client = AcumenClient.new(faraday, auth)
71
+
72
+ ids = event.payload['ids']
73
+ products = get_products_by_ids(client, ids)
74
+ products = get_product_variants(client, products)
75
+ products = get_product_categories(client, products)
76
+ products = get_product_contributors(client, products)
77
+
78
+ # map attributes
79
+ products.map do |product|
80
+ map_attributes(product)
81
+
82
+ product['model'].each do |model|
83
+ map_attributes(model)
84
+ end
85
+
86
+ product
87
+ end
88
+
89
+ products.each do |product|
90
+ create_event payload: product
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def map_attributes(product)
97
+ attribute_to_property = interpolated['attribute_to_property']
98
+ attributes = product['acumenAttributes']
99
+
100
+ attributes.each do |key,val|
101
+ if attribute_to_property[key] && val
102
+ product['additionalProperty'] = [] if product['additionalProperty'].nil?
103
+ product['additionalProperty'].push({
104
+ '@type' => 'PropertyValue',
105
+ 'propertyID' => attribute_to_property[key],
106
+ 'value' => val,
107
+ })
108
+
109
+ attributes.delete(key)
110
+ end
111
+ end
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,370 @@
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 = parse_product_request(response)
14
+
15
+ response = acumen_client.get_products_marketing(ids)
16
+ marketing = parse_product_marketing_request(response)
17
+
18
+ merge_products_and_marketing(products, marketing)
19
+ end
20
+
21
+ def get_variants_for_ids(acumen_client, ids)
22
+ result = get_linked_products_by_ids(acumen_client, ids)
23
+ result.select { |link| link['alt_format'].to_s != 0.to_s }
24
+ end
25
+
26
+ def get_linked_products_by_ids(acumen_client, ids)
27
+ response = acumen_client.get_linked_products(ids)
28
+ process_linked_product_query(response)
29
+ end
30
+
31
+ def get_product_contributors(acumen_client, products)
32
+ ids = products.map {|product| product['acumenAttributes']['product_marketing_id']}
33
+ response = acumen_client.get_product_contributors(ids)
34
+ product_contributors = process_product_contributor_query(response)
35
+
36
+ products.each do |product|
37
+ id = product['acumenAttributes']['product_marketing_id']
38
+ product_contributor = product_contributors[id]
39
+
40
+ if product_contributor
41
+ product['contributor'] = product_contributor.map do |pc|
42
+ {
43
+ '@type' => 'Person',
44
+ 'identifier' => pc['contributor_id']
45
+ }
46
+ end
47
+ end
48
+ end
49
+ products
50
+ end
51
+
52
+ def get_product_variants(acumen_client, products)
53
+ ids = products.map { |product| product['identifier'] }
54
+ # fetch product/variant relationships
55
+ variant_links = get_variants_for_ids(acumen_client, ids)
56
+ variant_ids = variant_links.map { |link| link['to_id'] }
57
+
58
+ variant_ids = variant_links.map { |link| link['to_id'] }
59
+
60
+ # fetch product variants
61
+ variants = get_products_by_ids(acumen_client, variant_ids)
62
+
63
+ # merge variants and products together
64
+ process_products_and_variants(products, variants, variant_links)
65
+ end
66
+
67
+ def get_product_categories(acumen_client, products)
68
+ # fetch categories
69
+ skus = products.map { |product| product['sku'] }
70
+ response = acumen_client.get_product_categories(skus)
71
+ categories = process_product_categories_query(response)
72
+
73
+ # map categories to products
74
+ products.each do |product|
75
+ sku = product['sku']
76
+ if categories[sku]
77
+ active = categories[sku].select { |c| c['inactive'] != 0 }
78
+ product['categories'] = active.map do |category|
79
+ {
80
+ '@type' => 'Thing',
81
+ 'identifier' => category['category_id']
82
+ }
83
+ end
84
+ end
85
+ end
86
+
87
+ products
88
+ end
89
+
90
+ def parse_product_request(products)
91
+ products.map do |p|
92
+ variant = response_mapper(p, {
93
+ 'Inv_Product.ID' => 'identifier',
94
+ 'Inv_Product.ProdCode' => 'sku',
95
+ 'Inv_Product.SubTitle' => 'disambiguatingDescription',
96
+ 'Inv_Product.ISBN_UPC' => 'isbn',
97
+ 'Inv_Product.Pub_Date' => 'datePublished',
98
+ })
99
+ variant['@type'] = 'ProductModel'
100
+ variant['isDefault'] = field_value(p, 'Inv_Product.OnWeb_LinkOnly') == '0'
101
+ variant['isTaxable'] = field_value(p, 'Inv_Product.Taxable') == '1'
102
+
103
+ variant['offers'] = [{
104
+ '@type' => 'Offer',
105
+ 'price' => field_value(p, 'Inv_Product.Price_1'),
106
+ }]
107
+ if field_value(p, 'Inv_Product.Price_2')
108
+ variant['offers'].push({
109
+ '@type' => 'Offer',
110
+ 'price' => field_value(p, 'Inv_Product.Price_2'),
111
+ })
112
+ end
113
+
114
+ weight = field_value(p, 'Inv_Product.Weight')
115
+ variant['weight'] = quantitative_value(weight, 'oz.')
116
+
117
+ product = {
118
+ '@type' => 'Product',
119
+ 'identifier' => variant['identifier'],
120
+ 'sku' => variant['sku'],
121
+ 'name' => field_value(p, 'Inv_Product.Full_Title'),
122
+ 'model' => [
123
+ variant
124
+ ],
125
+ 'additionalProperty' => [],
126
+ 'acumenAttributes' => {
127
+ 'info_alpha_1' => field_value(p, 'Inv_Product.Info_Alpha_1'),
128
+ 'info_boolean_1' => field_value(p, 'Inv_Product.Info_Boolean_1'),
129
+ },
130
+ }
131
+
132
+ category = field_value(p, 'Inv_Product.Category')
133
+ if category
134
+
135
+ if variant['acumenAttributes']
136
+ variant['acumenAttributes']['category'] = category
137
+ else
138
+ variant['acumenAttributes'] = { 'category' => category }
139
+ end
140
+
141
+ if category == 'Paperback'
142
+ product['additionalType'] = variant['additionalType'] = 'Book'
143
+ variant['bookFormat'] = "http://schema.org/Paperback"
144
+ variant['accessMode'] = "textual"
145
+ variant['isDigital'] = false
146
+ elsif category == 'Hardcover'
147
+ product['additionalType'] = variant['additionalType'] = 'Book'
148
+ variant['bookFormat'] = "http://schema.org/Hardcover"
149
+ variant['accessMode'] = "textual"
150
+ variant['isDigital'] = false
151
+ elsif category == 'eBook'
152
+ product['additionalType'] = variant['additionalType'] = 'Book'
153
+ variant['bookFormat'] = "http://schema.org/EBook"
154
+ variant['accessMode'] = "textual"
155
+ variant['isDigital'] = true
156
+ elsif category == 'CD'
157
+ product['additionalType'] = variant['additionalType'] = 'CreativeWork'
158
+ variant['accessMode'] = "auditory"
159
+ variant['isDigital'] = false
160
+ else
161
+ variant['isDigital'] = false
162
+ end
163
+ end
164
+
165
+ product
166
+ end
167
+ end
168
+
169
+ def process_linked_product_query(links)
170
+ links.map do |link|
171
+ response_mapper(link, {
172
+ 'Product_Link.Link_From_ID' => 'from_id',
173
+ 'Product_Link.Link_To_ID' => 'to_id',
174
+ 'Product_Link.Alt_Format' => 'alt_format',
175
+ })
176
+ end
177
+ end
178
+
179
+ def parse_product_marketing_request(products)
180
+ results = {}
181
+ products.each do |product|
182
+ mapped = response_mapper(product, {
183
+ 'ProdMkt.Product_ID' => 'product_id',
184
+ 'ProdMkt.Product_Code' => 'sku',
185
+ 'ProdMkt.ID' => 'id',
186
+ 'ProdMkt.Pages' => 'pages',
187
+ 'ProdMkt.Publisher' => 'publisher',
188
+ 'ProdMkt.Description_Short' => 'description_short',
189
+ 'ProdMkt.Description_Long' => 'description_long',
190
+ 'ProdMkt.Height' => 'height',
191
+ 'ProdMkt.Width' => 'width',
192
+ 'ProdMkt.Thickness' => 'depth',
193
+ 'ProdMkt.Meta_Keywords' => 'meta_keywords',
194
+ 'ProdMkt.Meta_Description' => 'meta_description',
195
+ 'ProdMkt.Extent_Unit' => 'extent_unit',
196
+ 'ProdMkt.Extent_Value' => 'extent_value',
197
+ 'ProdMkt.Age_Highest' => 'age_highest',
198
+ 'ProdMkt.Age_Lowest' => 'age_lowest',
199
+ 'ProdMkt.Awards' => 'awards',
200
+ 'ProdMkt.Dimensions_Unit_Measure' => 'dimensions_unit_measure',
201
+ 'ProdMkt.Excerpt' => 'excerpt',
202
+ 'ProdMkt.Grade_Highest' => 'grade_highest',
203
+ 'ProdMkt.Grade_Lowest' => 'grade_lowest',
204
+ 'ProdMkt.Status' => 'status',
205
+ 'ProdMkt.UPC' => 'upc',
206
+ 'ProdMkt.Weight_Unit_Measure' => 'weight_unit_measure',
207
+ 'ProdMkt.Weight' => 'weight',
208
+ 'ProdMkt.Info_Text_01' => 'info_text_01',
209
+ 'ProdMkt.Info_Text_02' => 'info_text_02',
210
+ 'ProdMkt.Religious_Text_Identifier' => 'religious_text_identifier',
211
+ })
212
+
213
+ results[mapped['product_id']] = mapped
214
+ end
215
+
216
+ results
217
+ end
218
+
219
+ def merge_products_and_marketing(products, product_marketing)
220
+ products.each do |product|
221
+ marketing = product_marketing[product['identifier']]
222
+ if marketing
223
+ product['acumenAttributes']['product_marketing_id'] = marketing['id']
224
+
225
+ product['publisher'] = {
226
+ '@type': 'Organization',
227
+ 'name' => marketing['publisher']
228
+ };
229
+ product['description'] = marketing['description_long']
230
+ product['abstract'] = marketing['description_short']
231
+ product['keywords'] = marketing['meta_keywords']
232
+ product['text'] = marketing['excerpt']
233
+
234
+ if marketing['age_lowest'] || marketing['age_highest']
235
+ product['typicalAgeRange'] = "#{marketing['age_lowest']}-#{marketing['age_highest']}"
236
+ end
237
+
238
+ # properties for product pages
239
+ if marketing['grade_lowest'] || marketing['grade_highest']
240
+ # educationalUse? educationalAlignment?
241
+ product['additionalProperty'].push({
242
+ '@type' => 'PropertyValue',
243
+ 'name' => 'Grade',
244
+ 'propertyID' => 'grade_range',
245
+ 'minValue' => marketing['grade_lowest'],
246
+ 'maxValue' => marketing['grade_highest'],
247
+ 'value' => "#{marketing['grade_lowest']}-#{marketing['grade_highest']}",
248
+ })
249
+ end
250
+ if marketing['awards']
251
+ product['additionalProperty'].push({
252
+ '@type' => 'PropertyValue',
253
+ 'propertyID' => 'awards',
254
+ 'name' => 'Awards',
255
+ 'value' => marketing['awards'],
256
+ })
257
+ end
258
+
259
+ # acumen specific properties
260
+ product['acumenAttributes']['extent_unit'] = marketing['extent_unit']
261
+ product['acumenAttributes']['extent_value'] = marketing['extent_value']
262
+ product['acumenAttributes']['info_text_01'] = marketing['info_text_01']
263
+ product['acumenAttributes']['info_text_02'] = marketing['info_text_02']
264
+ product['acumenAttributes']['meta_description'] = marketing['meta_description']
265
+ product['acumenAttributes']['religious_text_identifier'] = marketing['religious_text_identifier']
266
+ product['acumenAttributes']['status'] = marketing['status']
267
+
268
+ variant = product['model'][0]
269
+ variant['gtin12'] = marketing['upc']
270
+ variant['numberOfPages'] = marketing['pages']
271
+
272
+ variant['height'] = quantitative_value(
273
+ marketing['height'], marketing['dimensions_unit_measure']
274
+ )
275
+ variant['width'] = quantitative_value(
276
+ marketing['width'], marketing['dimensions_unit_measure']
277
+ )
278
+ variant['depth'] = quantitative_value(
279
+ marketing['thickness'], marketing['dimensions_unit_measure']
280
+ )
281
+ variant['weight'] = quantitative_value(
282
+ marketing['weight'], marketing['weight_unit_measure']
283
+ )
284
+ end
285
+ end
286
+
287
+ products
288
+ end
289
+
290
+ def process_product_categories_query(categories)
291
+ results = {}
292
+ categories.each do |category|
293
+ mapped = response_mapper(category, {
294
+ 'ProdMkt_WPC.ProdCode' => 'sku',
295
+ 'ProdMkt_WPC.WPC_ID' => 'category_id',
296
+ 'ProdMkt_WPC.Inactive' => 'inactive',
297
+ })
298
+
299
+ if results[mapped['sku']]
300
+ results[mapped['sku']].push(mapped)
301
+ else
302
+ results[mapped['sku']] = [mapped]
303
+ end
304
+ end
305
+
306
+ results
307
+ end
308
+
309
+ def process_product_contributor_query(contributors)
310
+ results = {}
311
+ contributors.each do |contributor|
312
+ mapped = response_mapper(contributor, {
313
+ 'ProdMkt_Contrib_Link.ProdMkt_Contrib_ID' => 'contributor_id',
314
+ 'ProdMkt_Contrib_Link.ProdMkt_ID' => 'product_marketing_id',
315
+ 'ProdMkt_Contrib_Link.Inactive' => 'inactive',
316
+ })
317
+
318
+ if results[mapped['product_marketing_id']]
319
+ results[mapped['product_marketing_id']].push(mapped)
320
+ else
321
+ results[mapped['product_marketing_id']] = [mapped]
322
+ end
323
+ end
324
+ results
325
+ end
326
+
327
+ def process_products_and_variants(products, variants, links)
328
+ products_map = {}
329
+ products.each { |product| products_map[product['identifier']] = product }
330
+
331
+ variants_map = {}
332
+ variants.each { |variant| variants_map[variant['identifier']] = variant }
333
+
334
+ links.each do |link|
335
+ from_id = link['from_id']
336
+ to_id = link['to_id']
337
+ variant = variants_map[to_id]
338
+ variant['isDefault'] = false
339
+ products_map[from_id]['model'].push(*variant['model'])
340
+ end
341
+
342
+ result = []
343
+ products_map.each_value { |p| result.push(p) }
344
+ result
345
+ end
346
+
347
+ private
348
+
349
+ def response_mapper(data, map)
350
+ result = {}
351
+ map.each do |key,val|
352
+ result[val] = field_value(data, key)
353
+ end
354
+
355
+ result
356
+ end
357
+
358
+ def field_value(field, key)
359
+ field[key]['__content__'] if field[key]
360
+ end
361
+
362
+ def quantitative_value(value, unit)
363
+ {
364
+ '@type' => 'QuantitativeValue',
365
+ 'value' => value,
366
+ 'unitText' => unit,
367
+ 'unitCode' => (UNIT_MAP[unit] if unit),
368
+ } if value
369
+ end
370
+ end
@@ -0,0 +1,61 @@
1
+ require 'rails_helper'
2
+ require 'huginn_agent/spec_helper'
3
+ require 'yaml'
4
+
5
+ require_relative '../lib/huginn_acumen_product_agent/acumen_client'
6
+
7
+ spec_folder = File.expand_path(File.dirname(__FILE__))
8
+ mock_data = YAML.load(File.read(spec_folder + "/acumen_product_agent_spec.yml"))
9
+
10
+ def mock_response(ns, ids)
11
+ records = ids.map {|id| mock_data[ns][id]}
12
+ response = <<~TEXT
13
+ <?xml version="1.0" encoding="UTF-8"?>
14
+ <SOAP-ENV:Envelope
15
+ SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
16
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
17
+ xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
18
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
19
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
20
+ <SOAP-ENV:Body>
21
+ <acusoapResponse>
22
+ <result_set.#{ns}>#{records.join("\n")}</result_set.#{ns}>
23
+ </acusoapResponse>
24
+ </SOAP-ENV:Body>
25
+ </SOAP-ENV:Envelope>
26
+ TEXT
27
+ response = ::MultiXml.parse(response, {})
28
+ AcumenClient::get_results(response, ns)
29
+ end
30
+
31
+ allow(AcumenClient).to receive(:get_products) do |ids|
32
+ mock_response('Inv_Product', ids)
33
+ end
34
+
35
+ allow(AcumenClient).to receive(:get_products_marketing) do |ids|
36
+ mock_response('ProdMkt', ids)
37
+ end
38
+
39
+ allow(AcumenClient).to receive(:get_linked_products) do |ids|
40
+ mock_response('Product_Link', ids)
41
+ end
42
+
43
+ allow(AcumenClient).to receive(:get_product_contributors) do |ids|
44
+ mock_response('ProdMkt_Contrib_Link', ids)
45
+ end
46
+
47
+ allow(AcumenClient).to receive(:get_product_categories) do |ids|
48
+ mock_response('ProdMkt_WPC', ids)
49
+ end
50
+
51
+
52
+ describe Agents::AcumenProductAgent do
53
+ before(:each) do
54
+ @valid_options = Agents::AcumenProductAgent.new.default_options
55
+ @checker = Agents::AcumenProductAgent.new(:name => "AcumenProductAgent", :options => @valid_options)
56
+ @checker.user = users(:bob)
57
+ @checker.save!
58
+ end
59
+
60
+ pending "add specs here"
61
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: huginn_acumen_product_agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jacob Spizziri
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: huginn_agent
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: The Huginn ACUMEN Product Agent takes in an array of ACUMEN product ID's,
56
+ queries the relevant ACUMEN tables, and emits a set of events with a sane data interface
57
+ for each those events.
58
+ email:
59
+ - jacob.spizziri@gmail.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE.txt
65
+ - lib/huginn_acumen_product_agent.rb
66
+ - lib/huginn_acumen_product_agent/acumen_client.rb
67
+ - lib/huginn_acumen_product_agent/acumen_product_agent.rb
68
+ - lib/huginn_acumen_product_agent/concerns/acumen_product_query_concern.rb
69
+ - spec/acumen_product_agent_spec.rb
70
+ homepage: https://github.com/5-Stones/huginn_acumen_product_agent
71
+ licenses:
72
+ - MIT
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.0.3
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Huginn agent for sane ACUMEN product data.
93
+ test_files:
94
+ - spec/acumen_product_agent_spec.rb