@pagefront/lint-commerce 0.7.0 → 0.8.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/README.md +30 -2
- package/dist/catalogue.js +12 -2
- package/dist/organization-catalogue.js +1 -1
- package/dist/product-schema.js +125 -17
- package/dist/rules/configurator-pricing.d.ts +114 -0
- package/dist/rules/configurator-pricing.js +349 -0
- package/dist/rules/pdp-e-001.js +12 -3
- package/dist/rules/pdp-e-007.d.ts +12 -0
- package/dist/rules/pdp-e-007.js +53 -0
- package/dist/rules/pdp-e-008.d.ts +10 -0
- package/dist/rules/pdp-e-008.js +41 -0
- package/dist/rules/pdp-e-009.d.ts +12 -0
- package/dist/rules/pdp-e-009.js +62 -0
- package/dist/rules/pdp-e-010.d.ts +10 -0
- package/dist/rules/pdp-e-010.js +37 -0
- package/dist/rules/pdp-e-011.d.ts +11 -0
- package/dist/rules/pdp-e-011.js +53 -0
- package/dist/rules/pdp-e-012.d.ts +2 -0
- package/dist/rules/pdp-e-012.js +32 -0
- package/dist/rules/pdp-w-032.d.ts +10 -0
- package/dist/rules/pdp-w-032.js +42 -0
- package/dist/rules/pdp-w-033.d.ts +10 -0
- package/dist/rules/pdp-w-033.js +30 -0
- package/dist/rules/pdp-w-034.d.ts +10 -0
- package/dist/rules/pdp-w-034.js +28 -0
- package/dist/rules/pdp-w-035.d.ts +11 -0
- package/dist/rules/pdp-w-035.js +41 -0
- package/dist/schema-validators.js +8 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { asObject, baseConfigurationProblems, compareDecimal, computeBand, formatDecimal, isComplete, missingModifierProblems, parseDecimal, pricingBlock, resolveConfigurator, } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-011 — AggregateOffer band disagrees with computed band.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Applies to an `AggregateOffer` whose pricing block is complete. Silent
|
|
7
|
+
* when the band cannot be computed: the reference does not resolve, the
|
|
8
|
+
* block fires PDP-E-007 or PDP-W-032, or every configuration is
|
|
9
|
+
* excluded. One finding per disagreeing bound, at the bound.
|
|
10
|
+
*/
|
|
11
|
+
export const pdpE011 = {
|
|
12
|
+
id: "PDP-E-011",
|
|
13
|
+
title: "AggregateOffer band disagrees with computed band",
|
|
14
|
+
severity: "error",
|
|
15
|
+
target: "$.data.offers[*]",
|
|
16
|
+
check: (match, ctx) => {
|
|
17
|
+
const offer = asObject(match.value);
|
|
18
|
+
if (offer === null || offer["@type"] !== "AggregateOffer")
|
|
19
|
+
return null;
|
|
20
|
+
const block = pricingBlock(offer);
|
|
21
|
+
if (block === null || !isComplete(block))
|
|
22
|
+
return null;
|
|
23
|
+
const configurator = resolveConfigurator(block, ctx.sheet);
|
|
24
|
+
if (configurator === null)
|
|
25
|
+
return null;
|
|
26
|
+
if (baseConfigurationProblems(configurator, block).length > 0)
|
|
27
|
+
return null;
|
|
28
|
+
if (missingModifierProblems(configurator, block).length > 0)
|
|
29
|
+
return null;
|
|
30
|
+
const band = computeBand(configurator, block);
|
|
31
|
+
if (band === null)
|
|
32
|
+
return null;
|
|
33
|
+
const hits = [];
|
|
34
|
+
const bounds = [
|
|
35
|
+
{ bound: "lowPrice", extreme: "cheapest", computed: band.min },
|
|
36
|
+
{ bound: "highPrice", extreme: "dearest", computed: band.max },
|
|
37
|
+
];
|
|
38
|
+
for (const { bound, extreme, computed } of bounds) {
|
|
39
|
+
const stated = parseDecimal(offer[bound]);
|
|
40
|
+
if (stated === undefined || compareDecimal(stated, computed) === 0)
|
|
41
|
+
continue;
|
|
42
|
+
hits.push({
|
|
43
|
+
path: `${match.path}['${bound}']`,
|
|
44
|
+
values: { bound, extreme, stated: String(offer[bound]), computed: formatDecimal(computed) },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return hits.length > 0 ? hits : null;
|
|
48
|
+
},
|
|
49
|
+
messageTemplate: "`{bound}` at `{path}` is `{stated}` but the {extreme} non-excluded configuration computed from the pricing block totals `{computed}`.",
|
|
50
|
+
remediation: "Correct the bound, or correct the price list: a missing adjustment, a missing exclusion and a wrong modifier each move the computed band. If the price list is known to have gaps, declare `pricing: \"partial\"`.",
|
|
51
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
52
|
+
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
53
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { pricedConfiguratorId, pricingBlock } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-012 — Configurator priced by more than one offer.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires on each `offers` entry whose pricing block names a configurator
|
|
7
|
+
* that an earlier entry already prices (exact string match on
|
|
8
|
+
* `configurator.@id`); `{firstPath}` is the first such entry.
|
|
9
|
+
*/
|
|
10
|
+
function pricedId(offer) {
|
|
11
|
+
const block = pricingBlock(offer);
|
|
12
|
+
return block === null ? undefined : pricedConfiguratorId(block);
|
|
13
|
+
}
|
|
14
|
+
export const pdpE012 = {
|
|
15
|
+
id: "PDP-E-012",
|
|
16
|
+
title: "Configurator priced by more than one offer",
|
|
17
|
+
severity: "error",
|
|
18
|
+
target: "$.data.offers[*]",
|
|
19
|
+
check: (match, ctx) => {
|
|
20
|
+
const value = pricedId(match.value);
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
const first = ctx.allMatches.find((m) => pricedId(m.value) === value);
|
|
24
|
+
if (first === undefined || first.path === match.path)
|
|
25
|
+
return null;
|
|
26
|
+
return { values: { value, firstPath: first.path } };
|
|
27
|
+
},
|
|
28
|
+
messageTemplate: "Offer at `{path}` prices configurator `{value}`, which the offer at `{firstPath}` already prices.",
|
|
29
|
+
remediation: "Keep one pricing block per configurator. Publish each further market's price list in that market's own Sheet.",
|
|
30
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
31
|
+
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
32
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-032 — Priced option without a modifier.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Applies to a resolving pricing block whose `pricing` is absent or
|
|
7
|
+
* `complete`. One finding per priced option with no modifier, and one
|
|
8
|
+
* per priced dimension that enumerates no options, all at the block.
|
|
9
|
+
*/
|
|
10
|
+
export declare const pdpW032: Rule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { asObject, isComplete, missingModifierProblems, resolveConfigurator, } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-032 — Priced option without a modifier.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Applies to a resolving pricing block whose `pricing` is absent or
|
|
7
|
+
* `complete`. One finding per priced option with no modifier, and one
|
|
8
|
+
* per priced dimension that enumerates no options, all at the block.
|
|
9
|
+
*/
|
|
10
|
+
export const pdpW032 = {
|
|
11
|
+
id: "PDP-W-032",
|
|
12
|
+
title: "Priced option without a modifier",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
target: "$.data.offers[*]['pagefront:configuratorPricing']",
|
|
15
|
+
check: (match, ctx) => {
|
|
16
|
+
const block = asObject(match.value);
|
|
17
|
+
if (block === null || !isComplete(block))
|
|
18
|
+
return null;
|
|
19
|
+
const configurator = resolveConfigurator(block, ctx.sheet);
|
|
20
|
+
if (configurator === null)
|
|
21
|
+
return null;
|
|
22
|
+
const hits = missingModifierProblems(configurator, block).map((p) => p.kind === "no-options"
|
|
23
|
+
? {
|
|
24
|
+
values: {
|
|
25
|
+
dimension: p.dimension,
|
|
26
|
+
problem: `cannot state modifiers for \`${p.dimension}\`, a priced dimension that enumerates no options`,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
values: {
|
|
31
|
+
dimension: p.dimension,
|
|
32
|
+
option: p.option,
|
|
33
|
+
problem: `states no modifier for option \`${p.option}\` of \`${p.dimension}\``,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return hits.length > 0 ? hits : null;
|
|
37
|
+
},
|
|
38
|
+
messageTemplate: "Pricing block at `{path}` {problem}, and `pricing` is not `partial`.",
|
|
39
|
+
remediation: "State the modifier, `\"0\"` included. If the dimension never changes the price, declare `affectsPrice: false` on it. If the price is not known, declare `pricing: \"partial\"`.",
|
|
40
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
41
|
+
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
42
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-033 — Priced configurator with no offer pricing it.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the configurator has a priced dimension, no `offers` entry
|
|
7
|
+
* carries a pricing block, and no `offers` entry states a price. The
|
|
8
|
+
* last case is PDP-W-034's; the two never fire on the same Sheet.
|
|
9
|
+
*/
|
|
10
|
+
export declare const pdpW033: Rule;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { entryStatesPrice, hasPricedDimension, offerEntries, pricingBlock, readConfigurator, } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-033 — Priced configurator with no offer pricing it.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the configurator has a priced dimension, no `offers` entry
|
|
7
|
+
* carries a pricing block, and no `offers` entry states a price. The
|
|
8
|
+
* last case is PDP-W-034's; the two never fire on the same Sheet.
|
|
9
|
+
*/
|
|
10
|
+
export const pdpW033 = {
|
|
11
|
+
id: "PDP-W-033",
|
|
12
|
+
title: "Priced configurator with no offer pricing it",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
target: "$.data['pagefront:configurator']",
|
|
15
|
+
check: (_match, ctx) => {
|
|
16
|
+
const configurator = readConfigurator(ctx.sheet);
|
|
17
|
+
if (configurator === null || !hasPricedDimension(configurator))
|
|
18
|
+
return null;
|
|
19
|
+
const offers = offerEntries(ctx.sheet);
|
|
20
|
+
if (offers.some((o) => pricingBlock(o) !== null))
|
|
21
|
+
return null;
|
|
22
|
+
if (offers.some(entryStatesPrice))
|
|
23
|
+
return null;
|
|
24
|
+
return {};
|
|
25
|
+
},
|
|
26
|
+
messageTemplate: "Configurator at `{path}` has priced dimensions but no offer prices it.",
|
|
27
|
+
remediation: "Add an offer carrying `pagefront:configuratorPricing` for this configurator. If no option changes the price, declare `affectsPrice: false` on each dimension.",
|
|
28
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
29
|
+
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
30
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-034 — Offer beside a configurator with no pricing block.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires once per Sheet, at the first `offers` entry that states a
|
|
7
|
+
* price, when the Sheet carries a configurator with a priced dimension
|
|
8
|
+
* and no `offers` entry carries a pricing block.
|
|
9
|
+
*/
|
|
10
|
+
export declare const pdpW034: Rule;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { entryStatesPrice, hasPricedDimension, pricingBlock, readConfigurator, } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-034 — Offer beside a configurator with no pricing block.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires once per Sheet, at the first `offers` entry that states a
|
|
7
|
+
* price, when the Sheet carries a configurator with a priced dimension
|
|
8
|
+
* and no `offers` entry carries a pricing block.
|
|
9
|
+
*/
|
|
10
|
+
export const pdpW034 = {
|
|
11
|
+
id: "PDP-W-034",
|
|
12
|
+
title: "Offer beside a configurator with no pricing block",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
target: "$.data.offers[*]",
|
|
15
|
+
check: (match, ctx) => {
|
|
16
|
+
const configurator = readConfigurator(ctx.sheet);
|
|
17
|
+
if (configurator === null || !hasPricedDimension(configurator))
|
|
18
|
+
return null;
|
|
19
|
+
if (ctx.allMatches.some((m) => pricingBlock(m.value) !== null))
|
|
20
|
+
return null;
|
|
21
|
+
const first = ctx.allMatches.find((m) => entryStatesPrice(m.value));
|
|
22
|
+
return first !== undefined && first.path === match.path ? {} : null;
|
|
23
|
+
},
|
|
24
|
+
messageTemplate: "Offer price is not linked to the configurator; state which configuration it prices.",
|
|
25
|
+
remediation: "Add `pagefront:configuratorPricing` to the offer: give the configurator an `@id`, reference it, and name the configuration the price is the total of. When only that one total is known, declare `pricing: \"partial\"`.",
|
|
26
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
27
|
+
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
28
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-035 — `basePrice` may be a component price.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Heuristic, one finding per pricing block: (a) `basePrice` equals the
|
|
7
|
+
* largest modifier of one dimension's `modifiers` entry, or (b)
|
|
8
|
+
* `basePrice` is below every non-zero modifier of one entry that has at
|
|
9
|
+
* least one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const pdpW035: Rule;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { asObject, compareDecimal, isZero, parseDecimal, } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-035 — `basePrice` may be a component price.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Heuristic, one finding per pricing block: (a) `basePrice` equals the
|
|
7
|
+
* largest modifier of one dimension's `modifiers` entry, or (b)
|
|
8
|
+
* `basePrice` is below every non-zero modifier of one entry that has at
|
|
9
|
+
* least one.
|
|
10
|
+
*/
|
|
11
|
+
export const pdpW035 = {
|
|
12
|
+
id: "PDP-W-035",
|
|
13
|
+
title: "`basePrice` may be a component price",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
target: "$.data.offers[*]['pagefront:configuratorPricing']",
|
|
16
|
+
check: (match) => {
|
|
17
|
+
const block = asObject(match.value);
|
|
18
|
+
const modifiers = asObject(block?.["modifiers"]);
|
|
19
|
+
const basePrice = parseDecimal(block?.["basePrice"]);
|
|
20
|
+
if (modifiers === null || basePrice === undefined)
|
|
21
|
+
return null;
|
|
22
|
+
for (const entry of Object.values(modifiers)) {
|
|
23
|
+
const amounts = Object.values(asObject(entry) ?? {})
|
|
24
|
+
.map(parseDecimal)
|
|
25
|
+
.filter((d) => d !== undefined);
|
|
26
|
+
if (amounts.length === 0)
|
|
27
|
+
continue;
|
|
28
|
+
const largest = amounts.reduce((a, b) => (compareDecimal(b, a) > 0 ? b : a));
|
|
29
|
+
if (compareDecimal(basePrice, largest) === 0)
|
|
30
|
+
return {};
|
|
31
|
+
const nonZero = amounts.filter((d) => !isZero(d));
|
|
32
|
+
if (nonZero.length > 0 && nonZero.every((d) => compareDecimal(basePrice, d) < 0))
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
messageTemplate: "basePrice may be a component price, not a configured total.",
|
|
38
|
+
remediation: "Confirm that `basePrice` is what a buyer pays for the whole of `baseConfiguration`. If it is the price of one part, replace it with the configured total and recompute the modifiers as differences from that total.",
|
|
39
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
40
|
+
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
41
|
+
};
|