huginn_acumen_product_agent 2.0.0 → 2.1.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 +4 -4
- data/lib/huginn_acumen_product_agent.rb +1 -0
- data/lib/huginn_acumen_product_agent/acumen_agent_error.rb +2 -1
- data/lib/huginn_acumen_product_agent/acumen_client.rb +30 -0
- data/lib/huginn_acumen_product_agent/acumen_product_agent.rb +38 -8
- data/lib/huginn_acumen_product_agent/concerns/alternate_products_query_concern.rb +13 -2
- data/lib/huginn_acumen_product_agent/concerns/inv_product_query_concern.rb +34 -2
- data/lib/huginn_acumen_product_agent/concerns/inv_status_query_concern.rb +76 -0
- data/lib/huginn_acumen_product_agent/concerns/prod_mkt_query_concern.rb +1 -1
- data/lib/huginn_acumen_product_agent/concerns/product_categories_query_concern.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0c47e2991b9e5c568b8b7762fbd9286b818401cc8d9f6cbc6b4e9af4d93c3a2
|
4
|
+
data.tar.gz: 994118245c433230a1b1ea15043bbb9e7d5f83af241c0c2bfae3362d833acb46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2349ea217eb714518d1c995703a9167527b8589106c38e13559835d621eadb155a90430d6c63927115d3d062369f52005f57b62f5d18a61c0bd2c501dae47401
|
7
|
+
data.tar.gz: 03f03158f7e226fd518c49d04867f1ae00424992f60aa71b31fc9ab9f7a5bd1aff366a49598580e404775f871ff74f57c22fb432449b946e5395e3778b25621a
|
@@ -3,6 +3,7 @@ require 'huginn_agent'
|
|
3
3
|
HuginnAgent.load 'huginn_acumen_product_agent/concerns/acumen_query_concern'
|
4
4
|
HuginnAgent.load 'huginn_acumen_product_agent/concerns/alternate_products_query_concern'
|
5
5
|
HuginnAgent.load 'huginn_acumen_product_agent/concerns/inv_product_query_concern'
|
6
|
+
HuginnAgent.load 'huginn_acumen_product_agent/concerns/inv_status_query_concern'
|
6
7
|
HuginnAgent.load 'huginn_acumen_product_agent/concerns/prod_mkt_query_concern'
|
7
8
|
HuginnAgent.load 'huginn_acumen_product_agent/concerns/product_categories_query_concern'
|
8
9
|
HuginnAgent.load 'huginn_acumen_product_agent/concerns/product_contributors_query_concern'
|
@@ -19,6 +19,12 @@ class AcumenClient
|
|
19
19
|
get_results(response, 'ProdMkt')
|
20
20
|
end
|
21
21
|
|
22
|
+
def get_inv_status(skus)
|
23
|
+
body = build_inv_status_query(skus)
|
24
|
+
response = execute_in_list_query(body, {})
|
25
|
+
get_results(response, 'Inv_Status')
|
26
|
+
end
|
27
|
+
|
22
28
|
def get_linked_products(ids)
|
23
29
|
body = build_linked_product_query(ids)
|
24
30
|
response = execute_in_list_query(body, {})
|
@@ -97,6 +103,8 @@ class AcumenClient
|
|
97
103
|
<column_name>Inv_Product.BO_Reason</column_name>
|
98
104
|
<column_name>Inv_Product.Not_On_Website</column_name>
|
99
105
|
<column_name>Inv_Product.Not_Active</column_name>
|
106
|
+
<column_name>Inv_Product.Disable_Web_Purchase</column_name>
|
107
|
+
<column_name>Inv_Product.No_Backorder_Fill</column_name>
|
100
108
|
</requested_output>
|
101
109
|
</acusoapRequest>
|
102
110
|
XML
|
@@ -151,6 +159,28 @@ class AcumenClient
|
|
151
159
|
XML
|
152
160
|
end
|
153
161
|
|
162
|
+
def build_inv_status_query(skus)
|
163
|
+
<<~XML
|
164
|
+
<acusoapRequest>
|
165
|
+
#{build_acumen_query_auth()}
|
166
|
+
<query>
|
167
|
+
<statement>
|
168
|
+
<column_name>Inv_Status.ProdCode</column_name>
|
169
|
+
<comparator>in</comparator>
|
170
|
+
<value>#{skus.join(',')}</value>
|
171
|
+
</statement>
|
172
|
+
</query>
|
173
|
+
<requested_output>
|
174
|
+
<view_owner_table_name>Inv_Status</view_owner_table_name>
|
175
|
+
<view_name>Inv_StatusAllRead</view_name>
|
176
|
+
<column_name>Inv_Status.Warehouse</column_name>
|
177
|
+
<column_name>Inv_Status.ProdCode</column_name>
|
178
|
+
<column_name>Inv_Status.Available</column_name>
|
179
|
+
</requested_output>
|
180
|
+
</acusoapRequest>
|
181
|
+
XML
|
182
|
+
end
|
183
|
+
|
154
184
|
def build_product_ids_since_request(since)
|
155
185
|
<<~XML
|
156
186
|
<acusoapRequest>
|
@@ -184,34 +184,46 @@ module Agents
|
|
184
184
|
def fetch_products(acumen_client, product_ids, digital_format_list)
|
185
185
|
products = fetch_inv_product_data(acumen_client, product_ids, digital_format_list)
|
186
186
|
products = fetch_product_marketing(acumen_client, products)
|
187
|
+
products = fetch_inv_status(acumen_client, products)
|
187
188
|
products = fetch_product_contributors(acumen_client, products)
|
188
189
|
products = fetch_product_categories(acumen_client, products)
|
189
190
|
|
190
191
|
products.each do |product|
|
191
192
|
map_attributes(product)
|
193
|
+
update_availability(product)
|
192
194
|
end
|
193
195
|
|
194
196
|
return products
|
195
197
|
end
|
196
198
|
|
197
|
-
#
|
198
|
-
#
|
199
|
-
# format of a given title.
|
199
|
+
# Loads product bundles for the provided `product_ids` array and emits
|
200
|
+
# a unique event payload for each bundle. Emitted events will contain an
|
201
|
+
# array of all the product definitions for each format of a given title.
|
200
202
|
#
|
201
203
|
# NOTE: The generated bundles will contain both active and inactive products
|
202
204
|
# to facilitate product deletion in external systems.
|
203
205
|
def fetch_product_bundles(acumen_client, product_ids, digital_format_list, ignored_skus)
|
204
206
|
|
205
207
|
begin
|
206
|
-
|
208
|
+
data = fetch_alternate_format_ids(acumen_client, product_ids)
|
209
|
+
full_id_set = data[:id_set]
|
210
|
+
alternate_ids_map = data[:alternate_ids_map]
|
211
|
+
product_data = fetch_products(acumen_client, full_id_set, digital_format_list)
|
207
212
|
|
208
213
|
bundles = product_ids.map do |id|
|
209
214
|
bundle_ids = alternate_ids_map[id]
|
210
215
|
bundle_ids.append(id) unless bundle_ids.include?(id)
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
216
|
+
bundle_ids.sort()
|
217
|
+
|
218
|
+
bundle = []
|
219
|
+
bundle_ids.each() do |b_id|
|
220
|
+
# Filter out any products that are explicitly ignored by SKU
|
221
|
+
product = product_data.find { |p| p['identifier'] == b_id.to_s }
|
222
|
+
bundle << product unless product.nil? || ignored_skus.include?(product['sku'])
|
223
|
+
# NOTE: The product.nil? check is designed to handle cases where a product link
|
224
|
+
# points to a non existent product. Conventionally this shouldn't happen, but
|
225
|
+
# we've seen it, and need to account for it.
|
226
|
+
end
|
215
227
|
|
216
228
|
create_event payload: { products: bundle, status: 200 }
|
217
229
|
end
|
@@ -242,5 +254,23 @@ module Agents
|
|
242
254
|
end
|
243
255
|
end
|
244
256
|
end
|
257
|
+
|
258
|
+
def update_availability(product)
|
259
|
+
stock_quantity = product['acumenAttributes']['stock_quantity']
|
260
|
+
publication_date = product['datePublished']
|
261
|
+
no_backorder_fill = product['noBackorderFill']
|
262
|
+
stock_quantity = stock_quantity.present? ? stock_quantity.to_i : 0
|
263
|
+
|
264
|
+
if (!product['isDigital'] && product['productAvailability'] == 'available')
|
265
|
+
if ((publication_date && publication_date.to_datetime > DateTime.current().end_of_day) || (!no_backorder_fill && stock_quantity < 1))
|
266
|
+
product['productAvailability'] = 'preorder'
|
267
|
+
end
|
268
|
+
|
269
|
+
if (no_backorder_fill && stock_quantity < 1)
|
270
|
+
product['productAvailability'] = 'not available'
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
245
275
|
end
|
246
276
|
end
|
@@ -12,14 +12,25 @@
|
|
12
12
|
module AlternateProductsQueryConcern
|
13
13
|
extend AcumenQueryConcern
|
14
14
|
|
15
|
-
#
|
15
|
+
# This function returns two data elements in a hash object.
|
16
|
+
# The `id_set` is the full collection of IDs to be fetched including the input
|
17
|
+
# product_ids and all of their alternate format ids. The goal of this value
|
18
|
+
# is to reduce the resource requirements of running each "bundle" individually
|
19
|
+
#
|
20
|
+
# The alternate_ids_map contains arrays of product IDs mapped to their master
|
21
|
+
# product id. This map will be used to assemble fetched product data into bundles
|
16
22
|
def fetch_alternate_format_ids(acumen_client, product_ids)
|
17
23
|
begin
|
18
24
|
link_data = acumen_client.get_linked_products(product_ids)
|
19
25
|
|
20
26
|
links = process_alternate_format_response(link_data)
|
21
27
|
|
22
|
-
|
28
|
+
mapped_ids = map_alternate_format_links(links, product_ids)
|
29
|
+
|
30
|
+
id_set = [] + product_ids
|
31
|
+
mapped_ids.each_value { |bundle| id_set += bundle }
|
32
|
+
|
33
|
+
return {id_set: id_set, alternate_ids_map: mapped_ids }
|
23
34
|
rescue => error
|
24
35
|
issue_error(AcumenAgentError.new(
|
25
36
|
'fetch_alternate_format_ids',
|
@@ -33,9 +33,19 @@ module InvProductQueryConcern
|
|
33
33
|
'Inv_Product.Next_Release' => 'releaseDate',
|
34
34
|
})
|
35
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
|
+
|
36
45
|
product['@type'] = 'Product'
|
37
46
|
product['isTaxable'] = get_field_value(p, 'Inv_Product.Taxable') == '1'
|
38
|
-
product['
|
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'
|
39
49
|
product['acumenAttributes'] = {
|
40
50
|
'info_alpha_1' => get_field_value(p, 'Inv_Product.Info_Alpha_1'),
|
41
51
|
'info_boolean_1' => get_field_value(p, 'Inv_Product.Info_Boolean_1'), # is_available_on_formed
|
@@ -45,6 +55,12 @@ module InvProductQueryConcern
|
|
45
55
|
'@type' => 'PropertyValue',
|
46
56
|
'propertyID' => 'is_master',
|
47
57
|
'value' => get_field_value(p, 'Inv_Product.OnWeb_LinkOnly') == '0',
|
58
|
+
},
|
59
|
+
{
|
60
|
+
# NOTE: This is different than isAvailableForPurchase. This
|
61
|
+
'@type' => 'PropertyValue',
|
62
|
+
'propertyID' => 'disable_web_purchase',
|
63
|
+
'value' => get_field_value(p, 'Inv_Product.Disable_Web_Purchase'),
|
48
64
|
}
|
49
65
|
]
|
50
66
|
|
@@ -62,6 +78,22 @@ module InvProductQueryConcern
|
|
62
78
|
})
|
63
79
|
end
|
64
80
|
|
81
|
+
not_on_website = get_field_value(p, 'Inv_Product.Not_On_Website')
|
82
|
+
disable_web_purchase = get_field_value(p, 'Inv_Product.Disable_Web_Purchase')
|
83
|
+
|
84
|
+
product_availability = 'available'
|
85
|
+
|
86
|
+
|
87
|
+
if (disable_web_purchase == '1')
|
88
|
+
product_availability = 'disabled'
|
89
|
+
end
|
90
|
+
|
91
|
+
if (not_on_website == '1')
|
92
|
+
product_availability = 'not available'
|
93
|
+
end
|
94
|
+
|
95
|
+
product['productAvailability'] = product_availability
|
96
|
+
|
65
97
|
weight = get_field_value(p, 'Inv_Product.Weight')
|
66
98
|
product['weight'] = get_quantitative_value(weight, 'oz.')
|
67
99
|
|
@@ -97,7 +129,7 @@ module InvProductQueryConcern
|
|
97
129
|
issue_error(AcumenAgentError.new(
|
98
130
|
'process_inv_product_response',
|
99
131
|
'Failed to load Inventory Product Records',
|
100
|
-
{
|
132
|
+
{ raw_data: raw_data },
|
101
133
|
error,
|
102
134
|
))
|
103
135
|
|
@@ -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
|
+
inv_status,
|
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
|
+
{ product: product, inventory_data: inventory_data },
|
67
|
+
error,
|
68
|
+
))
|
69
|
+
end
|
70
|
+
|
71
|
+
product
|
72
|
+
end
|
73
|
+
|
74
|
+
return products
|
75
|
+
end
|
76
|
+
end
|
@@ -34,7 +34,7 @@ module ProdMktQueryConcern
|
|
34
34
|
'ProdMkt.Description_Long' => 'description_long',
|
35
35
|
'ProdMkt.Height' => 'height',
|
36
36
|
'ProdMkt.Width' => 'width',
|
37
|
-
'ProdMkt.Thickness' => '
|
37
|
+
'ProdMkt.Thickness' => 'thickness',
|
38
38
|
'ProdMkt.Meta_Keywords' => 'meta_keywords',
|
39
39
|
'ProdMkt.Meta_Description' => 'meta_description',
|
40
40
|
'ProdMkt.Extent_Unit' => 'extent_unit',
|
@@ -41,7 +41,7 @@ module ProductCategoriesQueryConcern
|
|
41
41
|
results[product_sku] = [mapped] if mapped['inactive'] == '0'
|
42
42
|
end
|
43
43
|
rescue => error
|
44
|
-
|
44
|
+
issue_error(AcumenAgentError.new(
|
45
45
|
'process_product_category_response',
|
46
46
|
'Failed while processing category data',
|
47
47
|
product_category,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: huginn_acumen_product_agent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jacob Spizziri
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/huginn_acumen_product_agent/concerns/agent_error_concern.rb
|
71
71
|
- lib/huginn_acumen_product_agent/concerns/alternate_products_query_concern.rb
|
72
72
|
- lib/huginn_acumen_product_agent/concerns/inv_product_query_concern.rb
|
73
|
+
- lib/huginn_acumen_product_agent/concerns/inv_status_query_concern.rb
|
73
74
|
- lib/huginn_acumen_product_agent/concerns/prod_mkt_query_concern.rb
|
74
75
|
- lib/huginn_acumen_product_agent/concerns/product_categories_query_concern.rb
|
75
76
|
- lib/huginn_acumen_product_agent/concerns/product_contributors_query_concern.rb
|