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 +7 -0
- data/LICENSE.txt +7 -0
- data/lib/huginn_acumen_product_agent.rb +6 -0
- data/lib/huginn_acumen_product_agent/acumen_client.rb +257 -0
- data/lib/huginn_acumen_product_agent/acumen_product_agent.rb +115 -0
- data/lib/huginn_acumen_product_agent/concerns/acumen_product_query_concern.rb +370 -0
- data/spec/acumen_product_agent_spec.rb +61 -0
- metadata +94 -0
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,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
|