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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30c918a4763fbc8b84145f83215cf46d9b162adeb32d025d206d7f7fa51ded41
4
- data.tar.gz: 77b5d58d8de8f15e3908b920b04882fb0706974009c9fa1ea1fe93bb66b7f1d6
3
+ metadata.gz: a0c47e2991b9e5c568b8b7762fbd9286b818401cc8d9f6cbc6b4e9af4d93c3a2
4
+ data.tar.gz: 994118245c433230a1b1ea15043bbb9e7d5f83af241c0c2bfae3362d833acb46
5
5
  SHA512:
6
- metadata.gz: e2fe59ce492e910b0ca2c063938141a9f0ef6b64639fff28f34c3e01d577e5388d6007e1a135a51f01579677e60f2ee0e79e3aa2a449bfffba0e44242258953a
7
- data.tar.gz: 92563dd015ec11f237a6a91d740d3ff682ddcb73e6598f1768c3dfa51c2c4e75cb8355c3ee7228a5f63a0f1470a483d21adb5bd324283f4efa45b7c77ccada50
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'
@@ -1,5 +1,6 @@
1
1
  class AcumenAgentError < StandardError
2
- attr_reader :thing
2
+ attr_reader :scope, :data, :original_error
3
+
3
4
  def initialize(scope, message, data, original_error)
4
5
  @scope = scope
5
6
  @data = data
@@ -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
- # Returns an array of product bundles for the provided `products` array.
198
- # Each bundle will contain an array of all the product definitions for each
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
- alternate_ids_map = fetch_alternate_format_ids(acumen_client, product_ids)
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
- bundle = fetch_products(acumen_client, bundle_ids.sort, digital_format_list)
212
-
213
- # Filter out any products that are explicitly ignored by SKU
214
- bundle.select { |p| !ignored_skus.include?(p['sku']) }
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
- # Fetches the Inv_Product.ID for alternate formats of the provided product_ids
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
- return map_alternate_format_links(links, product_ids)
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['isAvailableForPurchase'] = get_field_value(p, 'Inv_Product.Not_On_Website') == '0'
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
- { product_ids: product_ids },
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' => 'depth',
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
- issue_errpr(AcumenAgentError.new(
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.0.0
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-01-14 00:00:00.000000000 Z
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