huginn_acumen_product_agent 1.7.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 +9 -1
- data/lib/huginn_acumen_product_agent/acumen_agent_error.rb +10 -0
- data/lib/huginn_acumen_product_agent/acumen_client.rb +30 -0
- data/lib/huginn_acumen_product_agent/acumen_product_agent.rb +163 -43
- data/lib/huginn_acumen_product_agent/concerns/acumen_query_concern.rb +58 -0
- data/lib/huginn_acumen_product_agent/concerns/agent_error_concern.rb +19 -0
- data/lib/huginn_acumen_product_agent/concerns/alternate_products_query_concern.rb +98 -0
- data/lib/huginn_acumen_product_agent/concerns/inv_product_query_concern.rb +140 -0
- 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 +163 -0
- data/lib/huginn_acumen_product_agent/concerns/product_categories_query_concern.rb +74 -0
- data/lib/huginn_acumen_product_agent/concerns/product_contributors_query_concern.rb +116 -0
- metadata +11 -3
- data/lib/huginn_acumen_product_agent/concerns/acumen_product_query_concern.rb +0 -478
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module is responsible for reading/processing data from the Product_Link
|
4
|
+
# table. This table defines the link between product records.
|
5
|
+
#
|
6
|
+
# It's important to note that this table defines links between related products
|
7
|
+
# as well as product formats. The `Alt_Format` field determines whether the linked
|
8
|
+
# product is an alternate format for a given title.
|
9
|
+
#
|
10
|
+
# For the purposes of this concern, we only care about Product_Link records where
|
11
|
+
# the `Alt_Format` field is `true`
|
12
|
+
module AlternateProductsQueryConcern
|
13
|
+
extend AcumenQueryConcern
|
14
|
+
|
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
|
22
|
+
def fetch_alternate_format_ids(acumen_client, product_ids)
|
23
|
+
begin
|
24
|
+
link_data = acumen_client.get_linked_products(product_ids)
|
25
|
+
|
26
|
+
links = process_alternate_format_response(link_data)
|
27
|
+
|
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 }
|
34
|
+
rescue => error
|
35
|
+
issue_error(AcumenAgentError.new(
|
36
|
+
'fetch_alternate_format_ids',
|
37
|
+
'Failed attempting to lookup alternate products',
|
38
|
+
product_ids,
|
39
|
+
error
|
40
|
+
))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# This function parses the raw data returned from the Product_Link table
|
45
|
+
# The resulting array contains the set alternate format IDs associated with a
|
46
|
+
# single product
|
47
|
+
def process_alternate_format_response(raw_data)
|
48
|
+
results = []
|
49
|
+
raw_data.map do |link|
|
50
|
+
|
51
|
+
begin
|
52
|
+
mapped = response_mapper(link, {
|
53
|
+
'Product_Link.Link_From_ID' => 'from_id',
|
54
|
+
'Product_Link.Link_To_ID' => 'to_id',
|
55
|
+
'Product_Link.Alt_Format' => 'alt_format',
|
56
|
+
})
|
57
|
+
|
58
|
+
if mapped['alt_format'].to_s != '0' && !mapped.in?(results)
|
59
|
+
results.push(mapped)
|
60
|
+
end
|
61
|
+
|
62
|
+
rescue => error
|
63
|
+
issue_error(AcumenAgentError.new(
|
64
|
+
'process_alternate_format_response',
|
65
|
+
'Failed while processing alternate format links',
|
66
|
+
raw_data,
|
67
|
+
error
|
68
|
+
))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
return results
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns a map that ties each provided `product_id` to an array of IDs for its
|
76
|
+
# other formats
|
77
|
+
def map_alternate_format_links(links, product_ids)
|
78
|
+
results = {}
|
79
|
+
|
80
|
+
product_ids.each do |id|
|
81
|
+
|
82
|
+
begin
|
83
|
+
alternates = links.select { |l| l['from_id'] == id }
|
84
|
+
results[id] = alternates.map { |l| l['to_id'] }
|
85
|
+
|
86
|
+
rescue => error
|
87
|
+
issue_error(AcumenAgentError.new(
|
88
|
+
'map_alternate_format_links',
|
89
|
+
'Failed while mapping alternate format links',
|
90
|
+
{ id: id, links: links },
|
91
|
+
error
|
92
|
+
))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
return results
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module is responsible for reading/processing data recrods from the Inv_Product
|
4
|
+
# table in Acumen. This table contains the baseline product information -- name, sku,
|
5
|
+
# format, etc.
|
6
|
+
module InvProductQueryConcern
|
7
|
+
extend AcumenQueryConcern
|
8
|
+
|
9
|
+
# Fetch/Process the Acumen data,
|
10
|
+
def fetch_inv_product_data(acumen_client, product_ids, digital_format_list)
|
11
|
+
product_data = acumen_client.get_products(product_ids)
|
12
|
+
|
13
|
+
return process_inv_product_response(product_data, digital_format_list)
|
14
|
+
end
|
15
|
+
|
16
|
+
# This function returns an array of Acumen products mapped to Schema.org/Product
|
17
|
+
# objects. We've added additional fields of:
|
18
|
+
#
|
19
|
+
# * `isAvailableForPurchase` -- used to control product deletion in external systems
|
20
|
+
# * `acumenAttributes` -- Additional acumen data that doesn't have a direct 1:1 field
|
21
|
+
# on the Product, but may be useful in other platforms
|
22
|
+
def process_inv_product_response(raw_data, digital_format_list)
|
23
|
+
raw_data.map do |p|
|
24
|
+
|
25
|
+
begin
|
26
|
+
product = response_mapper(p, {
|
27
|
+
'Inv_Product.ID' => 'identifier',
|
28
|
+
'Inv_Product.ProdCode' => 'sku',
|
29
|
+
'Inv_Product.Full_Title' => 'name',
|
30
|
+
'Inv_Product.SubTitle' => 'disambiguatingDescription',
|
31
|
+
'Inv_Product.ISBN_UPC' => 'isbn',
|
32
|
+
'Inv_Product.Pub_Date' => 'datePublished',
|
33
|
+
'Inv_Product.Next_Release' => 'releaseDate',
|
34
|
+
})
|
35
|
+
|
36
|
+
# Nullify blank dates
|
37
|
+
if product['datePublished'] == '0000-00-00T00:00:00'
|
38
|
+
product['datePublished'] = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
if product['releaseDate'] == '0000-00-00T00:00:00'
|
42
|
+
product['releaseDate'] = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
product['@type'] = 'Product'
|
46
|
+
product['isTaxable'] = get_field_value(p, 'Inv_Product.Taxable') == '1'
|
47
|
+
product['productAvailability'] = get_field_value(p, 'Inv_Product.Not_On_Website') == '0'
|
48
|
+
product['noBackorderFill'] = get_field_value(p, 'Inv_Product.No_Backorder_Fill') == '1'
|
49
|
+
product['acumenAttributes'] = {
|
50
|
+
'info_alpha_1' => get_field_value(p, 'Inv_Product.Info_Alpha_1'),
|
51
|
+
'info_boolean_1' => get_field_value(p, 'Inv_Product.Info_Boolean_1'), # is_available_on_formed
|
52
|
+
}
|
53
|
+
product['additionalProperty'] = [
|
54
|
+
{
|
55
|
+
'@type' => 'PropertyValue',
|
56
|
+
'propertyID' => 'is_master',
|
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'),
|
64
|
+
}
|
65
|
+
]
|
66
|
+
|
67
|
+
product['offers'] = [{
|
68
|
+
'@type' => 'Offer',
|
69
|
+
'price' => get_field_value(p, 'Inv_Product.Price_1'),
|
70
|
+
'availability' => get_field_value(p, 'Inv_Product.BO_Reason')
|
71
|
+
}]
|
72
|
+
|
73
|
+
if get_field_value(p, 'Inv_Product.Price_2')
|
74
|
+
product['offers'].push({
|
75
|
+
'@type' => 'Offer',
|
76
|
+
'price' => get_field_value(p, 'Inv_Product.Price_2'),
|
77
|
+
'availability' => get_field_value(p, 'Inv_Product.BO_Reason')
|
78
|
+
})
|
79
|
+
end
|
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
|
+
|
97
|
+
weight = get_field_value(p, 'Inv_Product.Weight')
|
98
|
+
product['weight'] = get_quantitative_value(weight, 'oz.')
|
99
|
+
|
100
|
+
# The category used here is the Acumen Product category. Functionally, this
|
101
|
+
# serves as the product's _format_. This field is different from Web Categories
|
102
|
+
# which behave more like traditional category taxonomies.
|
103
|
+
category = get_field_value(p, 'Inv_Product.Category')
|
104
|
+
if category
|
105
|
+
product['acumenAttributes']['category'] = category
|
106
|
+
product['isDigital'] = digital_format_list.find { |f| f == category } ? true : false
|
107
|
+
|
108
|
+
if category == 'Paperback'
|
109
|
+
product['additionalType'] = 'Book'
|
110
|
+
product['bookFormat'] = "http://schema.org/Paperback"
|
111
|
+
product['accessMode'] = "textual"
|
112
|
+
elsif category == 'Hardcover'
|
113
|
+
product['additionalType'] = 'Book'
|
114
|
+
product['bookFormat'] = "http://schema.org/Hardcover"
|
115
|
+
product['accessMode'] = "textual"
|
116
|
+
elsif category == 'eBook'
|
117
|
+
product['additionalType'] = 'Book'
|
118
|
+
product['bookFormat'] = "http://schema.org/EBook"
|
119
|
+
product['accessMode'] = "textual"
|
120
|
+
elsif category == 'CD'
|
121
|
+
product['additionalType'] = 'CreativeWork'
|
122
|
+
product['accessMode'] = "auditory"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
product
|
127
|
+
|
128
|
+
rescue => error
|
129
|
+
issue_error(AcumenAgentError.new(
|
130
|
+
'process_inv_product_response',
|
131
|
+
'Failed to load Inventory Product Records',
|
132
|
+
{ raw_data: raw_data },
|
133
|
+
error,
|
134
|
+
))
|
135
|
+
|
136
|
+
return
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module is responsible for reading/processing the Inv_Status table. This table
|
4
|
+
# contains stock information for the product and is tied to product availability.
|
5
|
+
module ProdMktQueryConcern
|
6
|
+
extend AcumenQueryConcern
|
7
|
+
|
8
|
+
# Update the provided products with their associated marketing data
|
9
|
+
# NOTE: The `products` here are Shema.org/Product records mapped from Inv_Product
|
10
|
+
# data
|
11
|
+
def fetch_inv_status(acumen_client, products)
|
12
|
+
|
13
|
+
product_skus = products.map { |p| p['sku'] }
|
14
|
+
inventory_data = acumen_client.get_inv_status(product_skus)
|
15
|
+
inventory_data = process_inv_status_response(inventory_data)
|
16
|
+
|
17
|
+
return map_inv_status_data(products, inventory_data)
|
18
|
+
end
|
19
|
+
|
20
|
+
# This function parses the raw data returned from the Prod_Mkt table
|
21
|
+
def process_inv_status_response(raw_data)
|
22
|
+
results = []
|
23
|
+
raw_data.map do |inv_status|
|
24
|
+
|
25
|
+
begin
|
26
|
+
mapped = response_mapper(inv_status, {
|
27
|
+
'Inv_Status.Warehouse' => 'warehouse',
|
28
|
+
'Inv_Status.ProdCode' => 'sku',
|
29
|
+
'Inv_Status.Available' => 'quantity',
|
30
|
+
})
|
31
|
+
|
32
|
+
results << mapped
|
33
|
+
rescue => error
|
34
|
+
issue_error(AcumenAgentError.new(
|
35
|
+
'process_inv_status_response',
|
36
|
+
'Failed while processing Prod_Mkt record',
|
37
|
+
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
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module is responsible for reading/processing the ProdMkt table. This table
|
4
|
+
# contains more detailed product meta information: Publisher, page counts, age range,
|
5
|
+
# description, etc.
|
6
|
+
module ProdMktQueryConcern
|
7
|
+
extend AcumenQueryConcern
|
8
|
+
|
9
|
+
# Update the provided products with their associated marketing data
|
10
|
+
# NOTE: The `products` here are Shema.org/Product records mapped from Inv_Product
|
11
|
+
# data
|
12
|
+
def fetch_product_marketing(acumen_client, products)
|
13
|
+
|
14
|
+
product_ids = products.map { |p| p['identifier'] }
|
15
|
+
marketing_data = acumen_client.get_products_marketing(product_ids)
|
16
|
+
marketing_data = process_prod_mkt_response(marketing_data)
|
17
|
+
|
18
|
+
return map_marketing_data(products, marketing_data)
|
19
|
+
end
|
20
|
+
|
21
|
+
# This function parses the raw data returned from the Prod_Mkt table
|
22
|
+
def process_prod_mkt_response(raw_data)
|
23
|
+
results = {}
|
24
|
+
raw_data.map do |product_marketing|
|
25
|
+
|
26
|
+
begin
|
27
|
+
mapped = response_mapper(product_marketing, {
|
28
|
+
'ProdMkt.Product_ID' => 'product_id',
|
29
|
+
'ProdMkt.Product_Code' => 'sku',
|
30
|
+
'ProdMkt.ID' => 'id',
|
31
|
+
'ProdMkt.Pages' => 'pages',
|
32
|
+
'ProdMkt.Publisher' => 'publisher',
|
33
|
+
'ProdMkt.Description_Short' => 'description_short',
|
34
|
+
'ProdMkt.Description_Long' => 'description_long',
|
35
|
+
'ProdMkt.Height' => 'height',
|
36
|
+
'ProdMkt.Width' => 'width',
|
37
|
+
'ProdMkt.Thickness' => 'thickness',
|
38
|
+
'ProdMkt.Meta_Keywords' => 'meta_keywords',
|
39
|
+
'ProdMkt.Meta_Description' => 'meta_description',
|
40
|
+
'ProdMkt.Extent_Unit' => 'extent_unit',
|
41
|
+
'ProdMkt.Extent_Value' => 'extent_value',
|
42
|
+
'ProdMkt.Age_Highest' => 'age_highest',
|
43
|
+
'ProdMkt.Age_Lowest' => 'age_lowest',
|
44
|
+
'ProdMkt.Awards' => 'awards',
|
45
|
+
'ProdMkt.Dimensions_Unit_Measure' => 'dimensions_unit_measure',
|
46
|
+
'ProdMkt.Excerpt' => 'excerpt',
|
47
|
+
'ProdMkt.Grade_Highest' => 'grade_highest',
|
48
|
+
'ProdMkt.Grade_Lowest' => 'grade_lowest',
|
49
|
+
'ProdMkt.Status' => 'status',
|
50
|
+
'ProdMkt.UPC' => 'upc',
|
51
|
+
'ProdMkt.Weight_Unit_Measure' => 'weight_unit_measure',
|
52
|
+
'ProdMkt.Weight' => 'weight',
|
53
|
+
'ProdMkt.Info_Text_01' => 'info_text_01',
|
54
|
+
'ProdMkt.Info_Text_02' => 'info_text_02',
|
55
|
+
'ProdMkt.Religious_Text_Identifier' => 'religious_text_identifier',
|
56
|
+
'ProdMkt.Info_Alpha_07' => 'info_alpha_07',
|
57
|
+
})
|
58
|
+
|
59
|
+
results[mapped['product_id']] = mapped
|
60
|
+
# NOTE: In this case, product_id matches the Inv_Product.ID fields
|
61
|
+
rescue => error
|
62
|
+
issue_error(AcumenAgentError.new(
|
63
|
+
'process_prod_mkt_response',
|
64
|
+
'Failed while processing Prod_Mkt record',
|
65
|
+
product_marketing,
|
66
|
+
error,
|
67
|
+
))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
results
|
72
|
+
end
|
73
|
+
|
74
|
+
# This function maps parsed Prod_Mkt records to their matching product record
|
75
|
+
# and updates the product object with the additional data
|
76
|
+
def map_marketing_data(products, marketing_data)
|
77
|
+
products.map do |product|
|
78
|
+
marketing = marketing_data[product['identifier']]
|
79
|
+
|
80
|
+
begin
|
81
|
+
|
82
|
+
if marketing
|
83
|
+
product['acumenAttributes']['product_marketing_id'] = marketing['id']
|
84
|
+
|
85
|
+
product['publisher'] = {
|
86
|
+
'@type': 'Organization',
|
87
|
+
'name' => marketing['publisher']
|
88
|
+
};
|
89
|
+
product['description'] = marketing['description_long']
|
90
|
+
product['abstract'] = marketing['description_short']
|
91
|
+
product['keywords'] = marketing['meta_keywords']
|
92
|
+
product['text'] = marketing['excerpt']
|
93
|
+
|
94
|
+
if marketing['age_lowest'] && marketing['age_highest']
|
95
|
+
product['typicalAgeRange'] = "#{marketing['age_lowest']}-#{marketing['age_highest']}"
|
96
|
+
end
|
97
|
+
|
98
|
+
#---------- Product Page Attributes ----------#
|
99
|
+
if marketing['grade_lowest'] || marketing['grade_highest']
|
100
|
+
# educationalUse? educationalAlignment?
|
101
|
+
product['additionalProperty'].push({
|
102
|
+
'@type' => 'PropertyValue',
|
103
|
+
'name' => 'Grade',
|
104
|
+
'propertyID' => 'grade_range',
|
105
|
+
'minValue' => marketing['grade_lowest'],
|
106
|
+
'maxValue' => marketing['grade_highest'],
|
107
|
+
'value' => "#{marketing['grade_lowest']}-#{marketing['grade_highest']}",
|
108
|
+
})
|
109
|
+
end
|
110
|
+
|
111
|
+
if marketing['awards']
|
112
|
+
product['additionalProperty'].push({
|
113
|
+
'@type' => 'PropertyValue',
|
114
|
+
'propertyID' => 'awards',
|
115
|
+
'name' => 'Awards',
|
116
|
+
'value' => marketing['awards'],
|
117
|
+
})
|
118
|
+
end
|
119
|
+
|
120
|
+
#---------- Acumen Specific Properties ----------#
|
121
|
+
product['acumenAttributes']['extent_unit'] = marketing['extent_unit']
|
122
|
+
product['acumenAttributes']['extent_value'] = marketing['extent_value']
|
123
|
+
product['acumenAttributes']['info_text_01'] = marketing['info_text_01'] # editorial_reviews
|
124
|
+
product['acumenAttributes']['info_text_02'] = marketing['info_text_02'] # product_samples
|
125
|
+
product['acumenAttributes']['info_alpha_07'] = marketing['info_alpha_07'] # video_urls
|
126
|
+
product['acumenAttributes']['meta_description'] = marketing['meta_description']
|
127
|
+
product['acumenAttributes']['religious_text_identifier'] = marketing['religious_text_identifier']
|
128
|
+
product['acumenAttributes']['status'] = marketing['status']
|
129
|
+
|
130
|
+
product['gtin12'] = marketing['upc']
|
131
|
+
product['numberOfPages'] = marketing['pages']
|
132
|
+
|
133
|
+
product['height'] = get_quantitative_value(
|
134
|
+
marketing['height'], marketing['dimensions_unit_measure']
|
135
|
+
)
|
136
|
+
product['width'] = get_quantitative_value(
|
137
|
+
marketing['width'], marketing['dimensions_unit_measure']
|
138
|
+
)
|
139
|
+
product['depth'] = get_quantitative_value(
|
140
|
+
marketing['thickness'], marketing['dimensions_unit_measure']
|
141
|
+
)
|
142
|
+
if product['weight']['value'] == '0'
|
143
|
+
product['weight'] = get_quantitative_value(
|
144
|
+
marketing['weight'], marketing['weight_unit_measure']
|
145
|
+
)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
rescue => error
|
150
|
+
issue_error(AcumenAgentError.new(
|
151
|
+
'map_marketing_data',
|
152
|
+
'Failed to map marketing data for product',
|
153
|
+
{ product: product, marketing: marketing },
|
154
|
+
error,
|
155
|
+
))
|
156
|
+
end
|
157
|
+
|
158
|
+
product
|
159
|
+
end
|
160
|
+
|
161
|
+
return products
|
162
|
+
end
|
163
|
+
end
|