@dylandepass/helix-product-pipeline 2.8.2 → 2.9.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.
- package/CHANGELOG.md +14 -0
- package/package.json +2 -2
- package/src/global.d.ts +2 -0
- package/src/product-html-pipe.js +6 -0
- package/src/product-index-pipe.js +7 -4
- package/src/product-json-pipe.js +5 -0
- package/src/product-sitemap-pipe.js +2 -4
- package/src/steps/apply-price-rules.js +223 -0
- package/src/steps/fetch-price-rules.js +80 -0
- package/src/steps/set-cache-headers.js +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [2.9.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.8.3...v2.9.0) (2026-05-08)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add price-rules overrides ([#83](https://github.com/adobe-rnd/helix-product-pipeline/issues/83)) ([cbf9bd6](https://github.com/adobe-rnd/helix-product-pipeline/commit/cbf9bd613155176121f6148f1788d0b922192962))
|
|
7
|
+
|
|
8
|
+
## [2.8.3](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.8.2...v2.8.3) (2026-04-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add cache key headers to index and sitemap responses ([#82](https://github.com/adobe-rnd/helix-product-pipeline/issues/82)) ([bce6e51](https://github.com/adobe-rnd/helix-product-pipeline/commit/bce6e510c1fd55b1c3d7f57b91edd1111e7d8f22))
|
|
14
|
+
|
|
1
15
|
## [2.8.2](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.8.1...v2.8.2) (2026-03-30)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dylandepass/helix-product-pipeline",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Helix Product Pipeline",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@adobe/helix-html-pipeline": "6.27.9",
|
|
49
49
|
"@adobe/helix-shared-utils": "3.0.2",
|
|
50
|
-
"@dylandepass/helix-product-shared": "1.
|
|
50
|
+
"@dylandepass/helix-product-shared": "1.8.4",
|
|
51
51
|
"dayjs": "1.11.19",
|
|
52
52
|
"github-slugger": "2.0.0",
|
|
53
53
|
"hast-util-from-html": "^2.0.3",
|
package/src/global.d.ts
CHANGED
package/src/product-html-pipe.js
CHANGED
|
@@ -28,6 +28,8 @@ import fetch404 from './steps/fetch-404.js';
|
|
|
28
28
|
import { setProductCacheHeaders } from './steps/set-cache-headers.js';
|
|
29
29
|
import { getUnauthorizedBody } from './utils/http-response.js';
|
|
30
30
|
import extractAuthoredMetadata from './steps/extract-authored-metadata.js';
|
|
31
|
+
import { fetchCatalogPriceRules } from './steps/fetch-price-rules.js';
|
|
32
|
+
import { applyProductPriceRule } from './steps/apply-price-rules.js';
|
|
31
33
|
|
|
32
34
|
export async function productHTMLPipe(state, req) {
|
|
33
35
|
const { log } = state;
|
|
@@ -101,6 +103,10 @@ export async function productHTMLPipe(state, req) {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
transformImages(state);
|
|
106
|
+
|
|
107
|
+
await fetchCatalogPriceRules(state, req);
|
|
108
|
+
applyProductPriceRule(state, res);
|
|
109
|
+
|
|
104
110
|
state.timer?.update('render');
|
|
105
111
|
await html(state);
|
|
106
112
|
await extractAuthoredMetadata(state);
|
|
@@ -16,8 +16,10 @@ import { validatePathInfo } from './utils/path.js';
|
|
|
16
16
|
import initConfig from './steps/init-config.js';
|
|
17
17
|
import fetchProductBusContent from './steps/fetch-productbus.js';
|
|
18
18
|
import { setLastModified } from './utils/last-modified.js';
|
|
19
|
-
import { set404CacheHeaders } from './steps/set-cache-headers.js';
|
|
19
|
+
import { set404CacheHeaders, setIndexCacheHeaders } from './steps/set-cache-headers.js';
|
|
20
20
|
import { getIncludes, getPaginationParams } from './steps/utils.js';
|
|
21
|
+
import { fetchCatalogPriceRules } from './steps/fetch-price-rules.js';
|
|
22
|
+
import { applyCatalogPriceRules } from './steps/apply-price-rules.js';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Returns index as spreadsheet with pagination support.
|
|
@@ -147,9 +149,10 @@ export async function productIndexPipe(state, req) {
|
|
|
147
149
|
throw new PipelineStatusError(res.status, res.error);
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
await setIndexCacheHeaders(state, req, res);
|
|
153
|
+
|
|
154
|
+
await fetchCatalogPriceRules(state, req);
|
|
155
|
+
applyCatalogPriceRules(state, res);
|
|
153
156
|
|
|
154
157
|
setLastModified(state, res);
|
|
155
158
|
|
package/src/product-json-pipe.js
CHANGED
|
@@ -18,6 +18,8 @@ import fetchProductBusContent from './steps/fetch-productbus.js';
|
|
|
18
18
|
import transformImages from './steps/transform-images.js';
|
|
19
19
|
import { setLastModified } from './utils/last-modified.js';
|
|
20
20
|
import { set404CacheHeaders, setProductCacheHeaders } from './steps/set-cache-headers.js';
|
|
21
|
+
import { fetchCatalogPriceRules } from './steps/fetch-price-rules.js';
|
|
22
|
+
import { applyProductPriceRule } from './steps/apply-price-rules.js';
|
|
21
23
|
|
|
22
24
|
export async function productJSONPipe(state, req) {
|
|
23
25
|
const { log, info } = state;
|
|
@@ -63,6 +65,9 @@ export async function productJSONPipe(state, req) {
|
|
|
63
65
|
|
|
64
66
|
transformImages(state);
|
|
65
67
|
|
|
68
|
+
await fetchCatalogPriceRules(state, req);
|
|
69
|
+
applyProductPriceRule(state, res);
|
|
70
|
+
|
|
66
71
|
// set surrogate keys
|
|
67
72
|
await setProductCacheHeaders(state, req, res);
|
|
68
73
|
|
|
@@ -18,7 +18,7 @@ import { validatePathInfo } from './utils/path.js';
|
|
|
18
18
|
import initConfig from './steps/init-config.js';
|
|
19
19
|
import fetchProductBusContent from './steps/fetch-productbus.js';
|
|
20
20
|
import { setLastModified } from './utils/last-modified.js';
|
|
21
|
-
import { set404CacheHeaders } from './steps/set-cache-headers.js';
|
|
21
|
+
import { set404CacheHeaders, setSitemapCacheHeaders } from './steps/set-cache-headers.js';
|
|
22
22
|
|
|
23
23
|
dayjs.extend(utc);
|
|
24
24
|
|
|
@@ -151,9 +151,7 @@ export async function productSitemapPipe(state, req) {
|
|
|
151
151
|
throw new PipelineStatusError(res.status, res.error);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
// const keys = await computeJSONSurrogateKeys(state);
|
|
156
|
-
// setCachingHeaders(state, req, res, keys);
|
|
154
|
+
await setSitemapCacheHeaders(state, req, res);
|
|
157
155
|
|
|
158
156
|
setLastModified(state, res);
|
|
159
157
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* eslint-disable no-continue */
|
|
14
|
+
|
|
15
|
+
import { recordLastModified } from '../utils/last-modified.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {{ start?: string, end?: string }} rule
|
|
19
|
+
* @param {number} now
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function isActive(rule, now) {
|
|
23
|
+
if (rule.start && new Date(rule.start).getTime() > now) return false;
|
|
24
|
+
if (rule.end && new Date(rule.end).getTime() < now) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Apply a catalog price rule to a product object.
|
|
30
|
+
* In product mode, mutates price.final. In index mode, mutates the flat price field.
|
|
31
|
+
* Variant rules are only applied in product mode (index entries are flat).
|
|
32
|
+
* In non-index mode, the product price is only written if the rule price is lower
|
|
33
|
+
* (a rule may be selected purely for variant benefit without improving the product price).
|
|
34
|
+
* @param {object} product
|
|
35
|
+
* @param {SharedTypes.CatalogPriceRule} rule
|
|
36
|
+
* @param {number} now
|
|
37
|
+
* @param {boolean} isIndex
|
|
38
|
+
*/
|
|
39
|
+
function applyRuleToProduct(product, rule, now, isIndex = false) {
|
|
40
|
+
if (rule.price != null) {
|
|
41
|
+
if (isIndex) {
|
|
42
|
+
product.price = rule.price;
|
|
43
|
+
} else if (product.price && parseFloat(rule.price) < parseFloat(product.price.final)) {
|
|
44
|
+
product.price.final = rule.price;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Variants may be an array (product JSON) or an object keyed by SKU (stored index)
|
|
49
|
+
/** @type {SharedTypes.ProductBusVariant[]} */
|
|
50
|
+
let variantList;
|
|
51
|
+
if (Array.isArray(product.variants)) {
|
|
52
|
+
variantList = product.variants;
|
|
53
|
+
} else if (product.variants) {
|
|
54
|
+
variantList = Object.values(product.variants);
|
|
55
|
+
} else {
|
|
56
|
+
variantList = [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const variant of variantList) {
|
|
60
|
+
const currentVariantPrice = isIndex ? variant.price : variant.price?.final;
|
|
61
|
+
const variantRule = rule.variants?.[variant.sku];
|
|
62
|
+
if (variantRule && isActive(variantRule, now)) {
|
|
63
|
+
if (variantRule.price != null
|
|
64
|
+
&& parseFloat(variantRule.price) < parseFloat(currentVariantPrice)) {
|
|
65
|
+
if (isIndex) {
|
|
66
|
+
variant.price = variantRule.price;
|
|
67
|
+
} else if (variant.price) {
|
|
68
|
+
variant.price.final = variantRule.price;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else if (rule.price != null && parseFloat(rule.price) < parseFloat(currentVariantPrice)) {
|
|
72
|
+
// inherit parent product price only if lower than variant's current price
|
|
73
|
+
if (isIndex) {
|
|
74
|
+
variant.price = rule.price;
|
|
75
|
+
} else if (variant.price) {
|
|
76
|
+
variant.price.final = rule.price;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find the best active promotion rule for a product across all promotions.
|
|
84
|
+
* A rule qualifies if its price is lower than the product's current price OR if any of
|
|
85
|
+
* its active variant-specific prices are lower than the corresponding variant's current price.
|
|
86
|
+
* Among qualifying rules, the one with the lowest product-level price wins.
|
|
87
|
+
* @param {SharedTypes.CatalogPriceRules} catalogPriceRules
|
|
88
|
+
* @param {string} productPath
|
|
89
|
+
* @param {number} now
|
|
90
|
+
* @param {object} product - the product object (used to read current price and variant prices)
|
|
91
|
+
* @returns {SharedTypes.CatalogPriceRule | null}
|
|
92
|
+
*/
|
|
93
|
+
function findBestRule(catalogPriceRules, productPath, now, product) {
|
|
94
|
+
const currentPrice = parseFloat(product.price?.final ?? 'Infinity');
|
|
95
|
+
|
|
96
|
+
const variantCurrentPrices = new Map();
|
|
97
|
+
const variantList = Array.isArray(product.variants)
|
|
98
|
+
? product.variants
|
|
99
|
+
: Object.values(product.variants ?? {});
|
|
100
|
+
for (const v of variantList) {
|
|
101
|
+
if (v.sku && v.price?.final != null) {
|
|
102
|
+
variantCurrentPrices.set(v.sku, parseFloat(v.price.final));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let bestRule = null;
|
|
107
|
+
let bestRuleProductPrice = Infinity;
|
|
108
|
+
|
|
109
|
+
for (const promotion of catalogPriceRules.promotions) {
|
|
110
|
+
for (const rule of promotion.rules) {
|
|
111
|
+
if (rule.path !== productPath) continue;
|
|
112
|
+
if (!isActive(rule, now)) continue;
|
|
113
|
+
|
|
114
|
+
const p = parseFloat(rule.price);
|
|
115
|
+
const lowersProductPrice = !Number.isNaN(p) && p < currentPrice;
|
|
116
|
+
const lowersVariantPrice = Object.entries(rule.variants ?? {}).some(([sku, vr]) => {
|
|
117
|
+
if (!isActive(vr, now) || vr.price == null) return false;
|
|
118
|
+
const currentVPrice = variantCurrentPrices.get(sku);
|
|
119
|
+
return currentVPrice !== undefined && parseFloat(vr.price) < currentVPrice;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!lowersProductPrice && !lowersVariantPrice) continue;
|
|
123
|
+
|
|
124
|
+
const ruleProductPrice = Number.isNaN(p) ? Infinity : p;
|
|
125
|
+
if (!bestRule || ruleProductPrice < bestRuleProductPrice) {
|
|
126
|
+
bestRuleProductPrice = ruleProductPrice;
|
|
127
|
+
bestRule = rule;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return bestRule;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Apply catalog price rules to state.content.data (single product request).
|
|
137
|
+
* Finds the lowest active promotion price for the product path and applies it only
|
|
138
|
+
* if it is less than the product's current price. Also records the most recently
|
|
139
|
+
* started active rule's start time as a last-modified source.
|
|
140
|
+
* @param {PipelineState} state
|
|
141
|
+
* @param {PipelineResponse} [res]
|
|
142
|
+
*/
|
|
143
|
+
export function applyProductPriceRule(state, res) {
|
|
144
|
+
const { catalogPriceRules, content, info } = state;
|
|
145
|
+
if (!catalogPriceRules?.promotions?.length || !content?.data) return;
|
|
146
|
+
|
|
147
|
+
const productPath = info.path.replace(/\.(json|html)$/, '');
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const rule = findBestRule(catalogPriceRules, productPath, now, content.data);
|
|
150
|
+
if (rule) applyRuleToProduct(content.data, rule, now, false);
|
|
151
|
+
|
|
152
|
+
if (res) {
|
|
153
|
+
let newestStartMs = 0;
|
|
154
|
+
for (const promotion of catalogPriceRules.promotions) {
|
|
155
|
+
for (const r of promotion.rules) {
|
|
156
|
+
if (r.path !== productPath || !isActive(r, now) || !r.start) continue;
|
|
157
|
+
const ms = new Date(r.start).getTime();
|
|
158
|
+
if (ms > newestStartMs) newestStartMs = ms;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (newestStartMs) {
|
|
162
|
+
recordLastModified(state, res, 'price-rules', new Date(newestStartMs).toUTCString());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Apply catalog price rules to the stored index (index request).
|
|
169
|
+
* For each product, finds the lowest active promotion price and applies it only
|
|
170
|
+
* if it is less than the product's current price. Also records the most recently
|
|
171
|
+
* started active rule's start time (across all paths in the index) as a last-modified source.
|
|
172
|
+
* @param {PipelineState} state
|
|
173
|
+
* @param {PipelineResponse} [res]
|
|
174
|
+
*/
|
|
175
|
+
export function applyCatalogPriceRules(state, res) {
|
|
176
|
+
const { catalogPriceRules, content } = state;
|
|
177
|
+
if (!catalogPriceRules?.promotions?.length || !content?.data) return;
|
|
178
|
+
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
|
|
181
|
+
// Build a path → best rule map and a path → newest start map across all promotions
|
|
182
|
+
/** @type {Map<string, SharedTypes.CatalogPriceRule>} */
|
|
183
|
+
const bestRuleByPath = new Map();
|
|
184
|
+
/** @type {Map<string, number>} */
|
|
185
|
+
const newestStartMsByPath = new Map();
|
|
186
|
+
for (const promotion of catalogPriceRules.promotions) {
|
|
187
|
+
for (const rule of promotion.rules) {
|
|
188
|
+
if (!isActive(rule, now)) continue;
|
|
189
|
+
const price = parseFloat(rule.price);
|
|
190
|
+
if (Number.isNaN(price)) continue;
|
|
191
|
+
const current = bestRuleByPath.get(rule.path);
|
|
192
|
+
if (!current || price < parseFloat(current.price)) {
|
|
193
|
+
bestRuleByPath.set(rule.path, rule);
|
|
194
|
+
}
|
|
195
|
+
if (rule.start) {
|
|
196
|
+
const startMs = new Date(rule.start).getTime();
|
|
197
|
+
if (startMs > (newestStartMsByPath.get(rule.path) ?? 0)) {
|
|
198
|
+
newestStartMsByPath.set(rule.path, startMs);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let newestStartMs = 0;
|
|
205
|
+
for (const entry of Object.values(content.data)) {
|
|
206
|
+
const product = entry?.data;
|
|
207
|
+
// eslint-disable-next-line no-continue
|
|
208
|
+
if (!product?.path) continue;
|
|
209
|
+
const rule = bestRuleByPath.get(product.path);
|
|
210
|
+
if (!rule) continue;
|
|
211
|
+
const rulePrice = parseFloat(rule.price);
|
|
212
|
+
const productPrice = parseFloat(product.price);
|
|
213
|
+
if (!Number.isNaN(productPrice) && rulePrice < productPrice) {
|
|
214
|
+
applyRuleToProduct(product, rule, now, true);
|
|
215
|
+
}
|
|
216
|
+
const startMs = newestStartMsByPath.get(product.path) ?? 0;
|
|
217
|
+
if (startMs > newestStartMs) newestStartMs = startMs;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (newestStartMs && res) {
|
|
221
|
+
recordLastModified(state, res, 'price-rules', new Date(newestStartMs).toUTCString());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PRICING_BUCKET_ID = 'helix-commerce-pricing';
|
|
14
|
+
const PRICING_BUCKET_DEV_ID = 'helix-commerce-pricing-dev';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} org
|
|
18
|
+
* @param {string} site
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function catalogRulesKey(org, site) {
|
|
22
|
+
return `${org}/${site}/prices/catalog/rules.json`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @returns {SharedTypes.CatalogPriceRules} */
|
|
26
|
+
function emptyRules() {
|
|
27
|
+
return { promotions: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch all catalog price rules. Sets state.catalogPriceRules.
|
|
32
|
+
* When the request carries `x-env: stage`, fetches from the dev pricing bucket and sets
|
|
33
|
+
* state.stagePricing = true so cache headers can be suppressed downstream.
|
|
34
|
+
* Used for both single-product and index routes.
|
|
35
|
+
* @param {PipelineState} state
|
|
36
|
+
* @param {PipelineRequest} [req]
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchCatalogPriceRules(state, req) {
|
|
39
|
+
const { org, site, s3Loader } = state;
|
|
40
|
+
const useStagePricing = req?.headers?.get('x-env') === 'stage';
|
|
41
|
+
const bucketId = useStagePricing
|
|
42
|
+
? PRICING_BUCKET_DEV_ID
|
|
43
|
+
: PRICING_BUCKET_ID;
|
|
44
|
+
if (useStagePricing) {
|
|
45
|
+
state.log.info('fetching catalog price rules from dev bucket');
|
|
46
|
+
state.stagePricing = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @type {PipelineResponse} */
|
|
50
|
+
let res;
|
|
51
|
+
try {
|
|
52
|
+
res = await s3Loader.getObject(bucketId, catalogRulesKey(org, site));
|
|
53
|
+
if (res.status !== 200) {
|
|
54
|
+
state.catalogPriceRules = emptyRules();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @type {SharedTypes.CatalogPriceRules} */
|
|
59
|
+
const rules = JSON.parse(res.body);
|
|
60
|
+
if (!rules || !Array.isArray(rules.promotions)) {
|
|
61
|
+
state.catalogPriceRules = emptyRules();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
|
|
67
|
+
// Pre-filter rules whose end is already past; remove promotions with no remaining rules
|
|
68
|
+
state.catalogPriceRules = {
|
|
69
|
+
promotions: rules.promotions
|
|
70
|
+
.map((promotion) => ({
|
|
71
|
+
...promotion,
|
|
72
|
+
rules: promotion.rules.filter((rule) => !rule.end || new Date(rule.end).getTime() > now),
|
|
73
|
+
}))
|
|
74
|
+
.filter((promotion) => promotion.rules.length > 0),
|
|
75
|
+
};
|
|
76
|
+
} catch (e) {
|
|
77
|
+
state.log.warn(`failed to fetch catalog price rules: ${res?.status ?? 'unknown error'}`, e);
|
|
78
|
+
state.catalogPriceRules = emptyRules();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -16,8 +16,11 @@ import {
|
|
|
16
16
|
computeProductPathKey,
|
|
17
17
|
computeSiteKey,
|
|
18
18
|
compute404Key,
|
|
19
|
+
computePriceRulesKey,
|
|
19
20
|
computeAuthoredContentKey,
|
|
20
21
|
computeProductKeys,
|
|
22
|
+
computeIndexKeys,
|
|
23
|
+
computeSitemapKeys,
|
|
21
24
|
} from '@dylandepass/helix-product-shared';
|
|
22
25
|
import { applyByoCdnHeaders } from '../utils/cache-headers.js';
|
|
23
26
|
|
|
@@ -105,4 +108,35 @@ export async function setProductCacheHeaders(state, req, resp) {
|
|
|
105
108
|
|
|
106
109
|
const productKeys = await computeProductKeys(org, site, info.path, contentBusId);
|
|
107
110
|
setCachingHeaders(req, resp, productKeys);
|
|
111
|
+
if (state.stagePricing) resp.headers.set('cache-control', 'no-store');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sets the cache headers for an index.json resource
|
|
116
|
+
* @param {PipelineState} state
|
|
117
|
+
* @param {PipelineRequest} req
|
|
118
|
+
* @param {PipelineResponse} resp
|
|
119
|
+
* @returns {Promise<void>}
|
|
120
|
+
*/
|
|
121
|
+
export async function setIndexCacheHeaders(state, req, resp) {
|
|
122
|
+
const { org, site, info } = state;
|
|
123
|
+
const keys = await computeIndexKeys(org, site, info.path);
|
|
124
|
+
keys.push(computePriceRulesKey(org, site));
|
|
125
|
+
setCachingHeaders(req, resp, keys);
|
|
126
|
+
if (state.stagePricing) resp.headers.set('cache-control', 'no-store');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sets the cache headers for a sitemap.xml resource
|
|
131
|
+
* @param {PipelineState} state
|
|
132
|
+
* @param {PipelineRequest} req
|
|
133
|
+
* @param {PipelineResponse} resp
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
export async function setSitemapCacheHeaders(state, req, resp) {
|
|
137
|
+
const { org, site, info } = state;
|
|
138
|
+
const keys = await computeSitemapKeys(org, site, info.path);
|
|
139
|
+
keys.push(computePriceRulesKey(org, site));
|
|
140
|
+
setCachingHeaders(req, resp, keys);
|
|
141
|
+
if (state.stagePricing) resp.headers.set('cache-control', 'no-store');
|
|
108
142
|
}
|