@pagefront/lint-commerce 0.8.0 → 0.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/README.md +29 -2
- package/dist/catalogue.js +9 -2
- package/dist/organization-catalogue.js +1 -1
- package/dist/product-schema.js +46 -15
- package/dist/rules/configurator-pricing.d.ts +21 -2
- package/dist/rules/configurator-pricing.js +41 -5
- package/dist/rules/pdp-e-008.js +1 -1
- package/dist/rules/pdp-e-009.d.ts +1 -1
- package/dist/rules/pdp-e-009.js +3 -3
- package/dist/rules/pdp-e-013.d.ts +9 -0
- package/dist/rules/pdp-e-013.js +26 -0
- package/dist/rules/pdp-e-014.d.ts +11 -0
- package/dist/rules/pdp-e-014.js +34 -0
- package/dist/rules/pdp-e-015.d.ts +2 -0
- package/dist/rules/pdp-e-015.js +91 -0
- package/dist/rules/pdp-w-032.d.ts +3 -2
- package/dist/rules/pdp-w-032.js +20 -12
- package/dist/rules/pdp-w-036.d.ts +2 -0
- package/dist/rules/pdp-w-036.js +31 -0
- package/dist/rules/pdp-w-037.d.ts +2 -0
- package/dist/rules/pdp-w-037.js +37 -0
- package/dist/rules/pdp-w-038.d.ts +2 -0
- package/dist/rules/pdp-w-038.js +30 -0
- package/dist/rules/pdp-w-039.d.ts +10 -0
- package/dist/rules/pdp-w-039.js +29 -0
- package/dist/schema-validators.js +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,8 +26,8 @@ lint with their own catalogue, dispatched by sheet type.
|
|
|
26
26
|
|
|
27
27
|
## Catalogue
|
|
28
28
|
|
|
29
|
-
Catalogue 0.
|
|
30
|
-
`PDP-E-
|
|
29
|
+
Catalogue 0.14.0, targeting format 0.9: 55 product rules (`PDP-E-001` to
|
|
30
|
+
`PDP-E-015`, `PDP-W-001` to `PDP-W-039`, `PDP-I-001`) and 7 organization
|
|
31
31
|
rules (`ORG-E-001`, `ORG-W-001` to `ORG-W-006`), each defined in
|
|
32
32
|
[`rules.md`](https://github.com/themachineweb/pagefront/blob/main/commerce/spec/rules.md)
|
|
33
33
|
with its target, check, message and remediation. Every report carries the
|
|
@@ -93,6 +93,33 @@ configurator now fails Layer 1.
|
|
|
93
93
|
|
|
94
94
|
Amounts are summed and compared as exact decimals.
|
|
95
95
|
|
|
96
|
+
Added in 0.9.0 (catalogue 0.14.0), from the same section and from
|
|
97
|
+
["Size guidance"](https://themachineweb.org/pagefront/spec/commerce/product#size-guidance-pagefrontsizechart-and-pagefrontsizerecommendation):
|
|
98
|
+
option values are identifiers, free-text dimensions, order-time services
|
|
99
|
+
kept out of `options`, notes as facts, and size advice in the sizing
|
|
100
|
+
dimension's system. From 0.9.0 every amount is a JSON number or a plain
|
|
101
|
+
decimal string, one carrier per Sheet, and amounts are compared numerically
|
|
102
|
+
whatever the carrier; 0.8.0 validated pricing-block amounts as decimal
|
|
103
|
+
strings only. An Offer's `price`, a variant's `price` and a promotion's
|
|
104
|
+
`discountAmount` no longer accept a string that is not a plain decimal, and
|
|
105
|
+
prices, bounds and base prices no longer accept a negative amount.
|
|
106
|
+
|
|
107
|
+
- `PDP-E-013` — a dimension declares `inputType: "text"` and enumerates
|
|
108
|
+
options
|
|
109
|
+
- `PDP-W-036` — an option value looks like a display label (whitespace,
|
|
110
|
+
an uppercase letter, or a unit suffix); sizing dimensions are exempt
|
|
111
|
+
- `PDP-W-037` — an option in a sizing dimension (`system` of `UK`, `EU`,
|
|
112
|
+
`US` or `JP`) is not written as a size in that system
|
|
113
|
+
- `PDP-W-032`, `PDP-E-009` and the computed band of `PDP-E-011` now cover
|
|
114
|
+
the `"*"` modifier of a priced text dimension
|
|
115
|
+
- `PDP-E-014` — a size adjustment's `sizeSystem` is another system than
|
|
116
|
+
the configurator's sizing dimension
|
|
117
|
+
- `PDP-E-015` — the Sheet's amounts mix JSON numbers and strings
|
|
118
|
+
- `PDP-W-038` — a configurator dimension's `note` reads as page copy
|
|
119
|
+
(English Sheets: `you`, `your`, `select`, `order`, `contact`)
|
|
120
|
+
- `PDP-W-039` — size advice carries a `description` and no
|
|
121
|
+
`sizeAdjustment` beside a sizing dimension
|
|
122
|
+
|
|
96
123
|
## API
|
|
97
124
|
|
|
98
125
|
The public surface — everything importable from the package root, and
|
package/dist/catalogue.js
CHANGED
|
@@ -10,6 +10,9 @@ import { pdpE009 } from "./rules/pdp-e-009.js";
|
|
|
10
10
|
import { pdpE010 } from "./rules/pdp-e-010.js";
|
|
11
11
|
import { pdpE011 } from "./rules/pdp-e-011.js";
|
|
12
12
|
import { pdpE012 } from "./rules/pdp-e-012.js";
|
|
13
|
+
import { pdpE013 } from "./rules/pdp-e-013.js";
|
|
14
|
+
import { pdpE014 } from "./rules/pdp-e-014.js";
|
|
15
|
+
import { pdpE015 } from "./rules/pdp-e-015.js";
|
|
13
16
|
import { pdpW001 } from "./rules/pdp-w-001.js";
|
|
14
17
|
import { pdpW002 } from "./rules/pdp-w-002.js";
|
|
15
18
|
import { pdpW003 } from "./rules/pdp-w-003.js";
|
|
@@ -45,6 +48,10 @@ import { pdpW032 } from "./rules/pdp-w-032.js";
|
|
|
45
48
|
import { pdpW033 } from "./rules/pdp-w-033.js";
|
|
46
49
|
import { pdpW034 } from "./rules/pdp-w-034.js";
|
|
47
50
|
import { pdpW035 } from "./rules/pdp-w-035.js";
|
|
51
|
+
import { pdpW036 } from "./rules/pdp-w-036.js";
|
|
52
|
+
import { pdpW037 } from "./rules/pdp-w-037.js";
|
|
53
|
+
import { pdpW038 } from "./rules/pdp-w-038.js";
|
|
54
|
+
import { pdpW039 } from "./rules/pdp-w-039.js";
|
|
48
55
|
import { pdpI001 } from "./rules/pdp-i-001.js";
|
|
49
56
|
/**
|
|
50
57
|
* The commerce product-sheet rule catalogue — the executable form of
|
|
@@ -56,6 +63,6 @@ import { pdpI001 } from "./rules/pdp-i-001.js";
|
|
|
56
63
|
export const commerceCatalogue = {
|
|
57
64
|
vertical: "commerce",
|
|
58
65
|
formatVersion: "0.9",
|
|
59
|
-
catalogueVersion: "0.
|
|
60
|
-
rules: [pdpE001, pdpE002, pdpE003, pdpE004, pdpE005, pdpE006, pdpE007, pdpE008, pdpE009, pdpE010, pdpE011, pdpE012, pdpW001, pdpW002, pdpW003, pdpW004, pdpW005, pdpW006, pdpW007, pdpW008, pdpW009, pdpW010, pdpW011, pdpW012, pdpW013, pdpW014, pdpW015, pdpW016, pdpW017, pdpW018, pdpW019, pdpW020, pdpW021, pdpW022, pdpW023, pdpW024, pdpW025, pdpW026, pdpW027, pdpW028, pdpW029, pdpW030, pdpW031, pdpW032, pdpW033, pdpW034, pdpW035, pdpI001],
|
|
66
|
+
catalogueVersion: "0.14.0",
|
|
67
|
+
rules: [pdpE001, pdpE002, pdpE003, pdpE004, pdpE005, pdpE006, pdpE007, pdpE008, pdpE009, pdpE010, pdpE011, pdpE012, pdpE013, pdpE014, pdpE015, pdpW001, pdpW002, pdpW003, pdpW004, pdpW005, pdpW006, pdpW007, pdpW008, pdpW009, pdpW010, pdpW011, pdpW012, pdpW013, pdpW014, pdpW015, pdpW016, pdpW017, pdpW018, pdpW019, pdpW020, pdpW021, pdpW022, pdpW023, pdpW024, pdpW025, pdpW026, pdpW027, pdpW028, pdpW029, pdpW030, pdpW031, pdpW032, pdpW033, pdpW034, pdpW035, pdpW036, pdpW037, pdpW038, pdpW039, pdpI001],
|
|
61
68
|
};
|
|
@@ -17,6 +17,6 @@ import { orgW006 } from "./rules/org-w-006.js";
|
|
|
17
17
|
export const organizationCatalogue = {
|
|
18
18
|
vertical: "commerce",
|
|
19
19
|
formatVersion: "0.9",
|
|
20
|
-
catalogueVersion: "0.
|
|
20
|
+
catalogueVersion: "0.14.0",
|
|
21
21
|
rules: [orgE001, orgW001, orgW002, orgW003, orgW004, orgW005, orgW006],
|
|
22
22
|
};
|
package/dist/product-schema.js
CHANGED
|
@@ -10,7 +10,7 @@ export const PRODUCT_SCHEMA = {
|
|
|
10
10
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
11
11
|
"$id": "https://themachineweb.org/pagefront/spec/commerce/product/v0.9.json",
|
|
12
12
|
"title": "Pagefront Product Sheet v0.9",
|
|
13
|
-
"description": "Formal schema for the Pagefront product sheet. Revision of 2026-10-01 (breaking, within v0.9): configurator pricing moves from pagefront:configurator to the offer. The configurator loses basePrice, basePriceCurrency and priceFormula, its options lose priceModifier and currency, and it gains an optional @id and an exclusions[] array; configurator dimensions gain affectsPrice; Offer and AggregateOffer accept pagefront:configuratorPricing, which names the configurator it prices and carries baseConfiguration, basePrice, modifiers, optional adjustments[] and a pricing completeness flag. v0.9 sheets that carried prices inside the configurator no longer validate. v0.9 was otherwise an additive release on top of v0.8: the legalEntity entry's organization now also validates in reference-stub form (@type + @id, no name) so entries can reference a Commerce Organization Sheet; the inline form and all other constraints are unchanged, and v0.8 sheets remain valid once their format_version is updated. v0.8 is an additive release on top of v0.7: it adds a pagefront:featureGroup[] array for grouped, ordered technical specifications (groups of Schema.org PropertyValue entries, with an optional pagefront:presentationValue display variant per entry), a pagefront:productHighlights[] array of ordered publisher-authored selling-point strings, and a pagefront:legalEntity[] array for role-typed legal entities behind a product (closed role enum: brand_owner, licensee, authorized_representative, importer, manufacturer — EU GPSR responsible persons and similar). gtin and gtin13 now accept a string or an array of strings (regional packaging, re-releases); category now accepts the display string, a Schema.org CategoryCode object, or a mixed array of both. The pagefront:Document role enum gains repairability_index, recall_notice, and declaration_of_conformity; the pagefront:RelatedProduct relationship enum gains service. v0.8 also adds two GPSR-driven safety fields: a pagefront:safetyWarning[] array for typed, language-scoped safety warnings (text required; optional pictogram URL and open-vocabulary hazardCategory) and a pagefront:safetyNotice[] array for structured recall/safety notices (closed noticeType enum: recall, warning; closed remedy enum: repair, replacement, refund). v0.7 sheets remain valid under v0.8 once their format_version is updated. Validates the envelope and pagefront-namespaced extensions strictly; Schema.org-typed blocks permit additional unspecified properties.",
|
|
13
|
+
"description": "Formal schema for the Pagefront product sheet. Revision of 2026-10-01 (breaking, within v0.9): configurator pricing moves from pagefront:configurator to the offer. The configurator loses basePrice, basePriceCurrency and priceFormula, its options lose priceModifier and currency, and it gains an optional @id and an exclusions[] array; configurator dimensions gain affectsPrice, inputType (select or text, for free-text dimensions) and maxLength; Offer and AggregateOffer accept pagefront:configuratorPricing, which names the configurator it prices and carries baseConfiguration, basePrice, modifiers, optional adjustments[] and a pricing completeness flag; every amount field (Offer price, AggregateOffer lowPrice and highPrice, variant price hints, promotion discountAmount, and the pricing block's basePrice, modifiers and adjustment amounts) references a shared definition, a JSON number or a plain decimal string: nonNegativeAmount for prices, bounds, base prices, price hints and discount amounts, and the signed amount for modifiers and adjustment amounts. v0.9 sheets that carried prices inside the configurator no longer validate. v0.9 was otherwise an additive release on top of v0.8: the legalEntity entry's organization now also validates in reference-stub form (@type + @id, no name) so entries can reference a Commerce Organization Sheet; the inline form and all other constraints are unchanged, and v0.8 sheets remain valid once their format_version is updated. v0.8 is an additive release on top of v0.7: it adds a pagefront:featureGroup[] array for grouped, ordered technical specifications (groups of Schema.org PropertyValue entries, with an optional pagefront:presentationValue display variant per entry), a pagefront:productHighlights[] array of ordered publisher-authored selling-point strings, and a pagefront:legalEntity[] array for role-typed legal entities behind a product (closed role enum: brand_owner, licensee, authorized_representative, importer, manufacturer — EU GPSR responsible persons and similar). gtin and gtin13 now accept a string or an array of strings (regional packaging, re-releases); category now accepts the display string, a Schema.org CategoryCode object, or a mixed array of both. The pagefront:Document role enum gains repairability_index, recall_notice, and declaration_of_conformity; the pagefront:RelatedProduct relationship enum gains service. v0.8 also adds two GPSR-driven safety fields: a pagefront:safetyWarning[] array for typed, language-scoped safety warnings (text required; optional pictogram URL and open-vocabulary hazardCategory) and a pagefront:safetyNotice[] array for structured recall/safety notices (closed noticeType enum: recall, warning; closed remedy enum: repair, replacement, refund). v0.7 sheets remain valid under v0.8 once their format_version is updated. Validates the envelope and pagefront-namespaced extensions strictly; Schema.org-typed blocks permit additional unspecified properties.",
|
|
14
14
|
"type": "object",
|
|
15
15
|
"required": [
|
|
16
16
|
"format_version",
|
|
@@ -433,6 +433,31 @@ export const PRODUCT_SCHEMA = {
|
|
|
433
433
|
},
|
|
434
434
|
"additionalProperties": true
|
|
435
435
|
},
|
|
436
|
+
"amount": {
|
|
437
|
+
"description": "A currency amount: a JSON number, or a decimal string with a dot as decimal point, no thousands separators and no currency symbol. A Sheet uses one of the two carriers for all its amounts.",
|
|
438
|
+
"oneOf": [
|
|
439
|
+
{
|
|
440
|
+
"type": "number"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"type": "string",
|
|
444
|
+
"pattern": "^-?\\d+(\\.\\d+)?$"
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
},
|
|
448
|
+
"nonNegativeAmount": {
|
|
449
|
+
"description": "An amount that is zero or positive: the same two carriers as amount, with no leading minus. Used for prices, price bounds, base prices, price hints and discount amounts; only modifiers and adjustment amounts are signed.",
|
|
450
|
+
"oneOf": [
|
|
451
|
+
{
|
|
452
|
+
"type": "number",
|
|
453
|
+
"minimum": 0
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"type": "string",
|
|
457
|
+
"pattern": "^\\d+(\\.\\d+)?$"
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
},
|
|
436
461
|
"variantEntry": {
|
|
437
462
|
"type": "object",
|
|
438
463
|
"required": [
|
|
@@ -482,10 +507,7 @@ export const PRODUCT_SCHEMA = {
|
|
|
482
507
|
"type": "string"
|
|
483
508
|
},
|
|
484
509
|
"price": {
|
|
485
|
-
"
|
|
486
|
-
"string",
|
|
487
|
-
"number"
|
|
488
|
-
]
|
|
510
|
+
"$ref": "#/$defs/nonNegativeAmount"
|
|
489
511
|
},
|
|
490
512
|
"image": {
|
|
491
513
|
"type": "string",
|
|
@@ -1181,7 +1203,7 @@ export const PRODUCT_SCHEMA = {
|
|
|
1181
1203
|
"type": "object"
|
|
1182
1204
|
},
|
|
1183
1205
|
"price": {
|
|
1184
|
-
"
|
|
1206
|
+
"$ref": "#/$defs/nonNegativeAmount"
|
|
1185
1207
|
},
|
|
1186
1208
|
"priceCurrency": {
|
|
1187
1209
|
"type": "string",
|
|
@@ -1301,10 +1323,10 @@ export const PRODUCT_SCHEMA = {
|
|
|
1301
1323
|
"type": "string"
|
|
1302
1324
|
},
|
|
1303
1325
|
"lowPrice": {
|
|
1304
|
-
"
|
|
1326
|
+
"$ref": "#/$defs/nonNegativeAmount"
|
|
1305
1327
|
},
|
|
1306
1328
|
"highPrice": {
|
|
1307
|
-
"
|
|
1329
|
+
"$ref": "#/$defs/nonNegativeAmount"
|
|
1308
1330
|
},
|
|
1309
1331
|
"priceCurrency": {
|
|
1310
1332
|
"type": "string",
|
|
@@ -1734,6 +1756,18 @@ export const PRODUCT_SCHEMA = {
|
|
|
1734
1756
|
"type": "boolean",
|
|
1735
1757
|
"default": true
|
|
1736
1758
|
},
|
|
1759
|
+
"inputType": {
|
|
1760
|
+
"type": "string",
|
|
1761
|
+
"enum": [
|
|
1762
|
+
"select",
|
|
1763
|
+
"text"
|
|
1764
|
+
],
|
|
1765
|
+
"default": "select"
|
|
1766
|
+
},
|
|
1767
|
+
"maxLength": {
|
|
1768
|
+
"type": "integer",
|
|
1769
|
+
"minimum": 1
|
|
1770
|
+
},
|
|
1737
1771
|
"system": {
|
|
1738
1772
|
"type": "string"
|
|
1739
1773
|
},
|
|
@@ -1792,16 +1826,14 @@ export const PRODUCT_SCHEMA = {
|
|
|
1792
1826
|
"$ref": "#/$defs/configurationSelection"
|
|
1793
1827
|
},
|
|
1794
1828
|
"basePrice": {
|
|
1795
|
-
"
|
|
1796
|
-
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
|
1829
|
+
"$ref": "#/$defs/nonNegativeAmount"
|
|
1797
1830
|
},
|
|
1798
1831
|
"modifiers": {
|
|
1799
1832
|
"type": "object",
|
|
1800
1833
|
"additionalProperties": {
|
|
1801
1834
|
"type": "object",
|
|
1802
1835
|
"additionalProperties": {
|
|
1803
|
-
"
|
|
1804
|
-
"pattern": "^-?[0-9]+(\\.[0-9]+)?$"
|
|
1836
|
+
"$ref": "#/$defs/amount"
|
|
1805
1837
|
}
|
|
1806
1838
|
}
|
|
1807
1839
|
},
|
|
@@ -1820,8 +1852,7 @@ export const PRODUCT_SCHEMA = {
|
|
|
1820
1852
|
"minProperties": 1
|
|
1821
1853
|
},
|
|
1822
1854
|
"priceModifier": {
|
|
1823
|
-
"
|
|
1824
|
-
"pattern": "^-?[0-9]+(\\.[0-9]+)?$"
|
|
1855
|
+
"$ref": "#/$defs/amount"
|
|
1825
1856
|
}
|
|
1826
1857
|
},
|
|
1827
1858
|
"additionalProperties": false
|
|
@@ -1955,7 +1986,7 @@ export const PRODUCT_SCHEMA = {
|
|
|
1955
1986
|
"maximum": 100
|
|
1956
1987
|
},
|
|
1957
1988
|
"discountAmount": {
|
|
1958
|
-
"
|
|
1989
|
+
"$ref": "#/$defs/nonNegativeAmount"
|
|
1959
1990
|
},
|
|
1960
1991
|
"discountCurrency": {
|
|
1961
1992
|
"type": "string",
|
|
@@ -38,9 +38,23 @@ export interface Dimension {
|
|
|
38
38
|
name: string;
|
|
39
39
|
/** `affectsPrice` is not `false`. */
|
|
40
40
|
priced: boolean;
|
|
41
|
-
/**
|
|
41
|
+
/** `inputType` is `text` (Definitions: *text dimension*). */
|
|
42
|
+
text: boolean;
|
|
43
|
+
/** The declared sizing `system`, when the dimension is a sizing dimension. */
|
|
44
|
+
system: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Option keys in document order; null when the dimension enumerates
|
|
47
|
+
* none. Always null for a text dimension, which takes free input.
|
|
48
|
+
*/
|
|
42
49
|
optionKeys: string[] | null;
|
|
43
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* `system` → the `pagefront:SizeAdjustment.sizeSystem` naming the same
|
|
53
|
+
* system (Definitions: *corresponding size system*).
|
|
54
|
+
*/
|
|
55
|
+
export declare const CORRESPONDING_SIZE_SYSTEM: Record<string, string>;
|
|
56
|
+
/** The one modifier key of a text dimension: any non-empty input. */
|
|
57
|
+
export declare const TEXT_KEY = "*";
|
|
44
58
|
export interface Configurator {
|
|
45
59
|
id: string | undefined;
|
|
46
60
|
dimensions: Dimension[];
|
|
@@ -86,6 +100,9 @@ export declare function baseConfigurationProblems(configurator: Configurator, bl
|
|
|
86
100
|
export type MissingModifierProblem = {
|
|
87
101
|
kind: "no-options";
|
|
88
102
|
dimension: string;
|
|
103
|
+
} | {
|
|
104
|
+
kind: "no-text-modifier";
|
|
105
|
+
dimension: string;
|
|
89
106
|
} | {
|
|
90
107
|
kind: "missing";
|
|
91
108
|
dimension: string;
|
|
@@ -108,7 +125,9 @@ export interface Band {
|
|
|
108
125
|
* enumerated together. Every other priced dimension contributes its
|
|
109
126
|
* smallest modifier to the minimum and its largest to the maximum,
|
|
110
127
|
* independently of the rest, which yields the same band as the full
|
|
111
|
-
* enumeration.
|
|
128
|
+
* enumeration. A priced text dimension is empty or non-empty: its `*`
|
|
129
|
+
* modifier lowers the minimum when negative and raises the maximum
|
|
130
|
+
* when positive.
|
|
112
131
|
*/
|
|
113
132
|
export declare function computeBand(configurator: Configurator, block: Obj): Band | null;
|
|
114
133
|
export {};
|
|
@@ -83,6 +83,18 @@ export function optionKey(value) {
|
|
|
83
83
|
return JSON.stringify(value);
|
|
84
84
|
return undefined;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* `system` → the `pagefront:SizeAdjustment.sizeSystem` naming the same
|
|
88
|
+
* system (Definitions: *corresponding size system*).
|
|
89
|
+
*/
|
|
90
|
+
export const CORRESPONDING_SIZE_SYSTEM = {
|
|
91
|
+
UK: "WearableSizeSystemUK",
|
|
92
|
+
EU: "WearableSizeSystemEU",
|
|
93
|
+
US: "WearableSizeSystemUS",
|
|
94
|
+
JP: "WearableSizeSystemJP",
|
|
95
|
+
};
|
|
96
|
+
/** The one modifier key of a text dimension: any non-empty input. */
|
|
97
|
+
export const TEXT_KEY = "*";
|
|
86
98
|
function selection(value) {
|
|
87
99
|
const obj = asObject(value);
|
|
88
100
|
if (obj === null)
|
|
@@ -107,8 +119,9 @@ export function readConfigurator(sheet) {
|
|
|
107
119
|
const d = asObject(entry);
|
|
108
120
|
if (d === null || typeof d["name"] !== "string")
|
|
109
121
|
continue;
|
|
122
|
+
const text = d["inputType"] === "text";
|
|
110
123
|
let optionKeys = null;
|
|
111
|
-
if (Array.isArray(d["options"]) && d["options"].length > 0) {
|
|
124
|
+
if (!text && Array.isArray(d["options"]) && d["options"].length > 0) {
|
|
112
125
|
optionKeys = [];
|
|
113
126
|
for (const option of d["options"]) {
|
|
114
127
|
const key = optionKey(asObject(option)?.["value"]);
|
|
@@ -116,7 +129,13 @@ export function readConfigurator(sheet) {
|
|
|
116
129
|
optionKeys.push(key);
|
|
117
130
|
}
|
|
118
131
|
}
|
|
119
|
-
const dimension = {
|
|
132
|
+
const dimension = {
|
|
133
|
+
name: d["name"],
|
|
134
|
+
priced: d["affectsPrice"] !== false,
|
|
135
|
+
text,
|
|
136
|
+
system: typeof d["system"] === "string" ? d["system"] : undefined,
|
|
137
|
+
optionKeys,
|
|
138
|
+
};
|
|
120
139
|
dimensions.push(dimension);
|
|
121
140
|
if (!byName.has(dimension.name))
|
|
122
141
|
byName.set(dimension.name, dimension);
|
|
@@ -213,11 +232,17 @@ export function missingModifierProblems(configurator, block) {
|
|
|
213
232
|
for (const d of configurator.dimensions) {
|
|
214
233
|
if (!d.priced)
|
|
215
234
|
continue;
|
|
235
|
+
const entry = asObject(own(modifiers, d.name));
|
|
236
|
+
if (d.text) {
|
|
237
|
+
if (own(entry, TEXT_KEY) === undefined) {
|
|
238
|
+
problems.push({ kind: "no-text-modifier", dimension: d.name });
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
216
242
|
if (d.optionKeys === null) {
|
|
217
243
|
problems.push({ kind: "no-options", dimension: d.name });
|
|
218
244
|
continue;
|
|
219
245
|
}
|
|
220
|
-
const entry = asObject(own(modifiers, d.name));
|
|
221
246
|
for (const option of d.optionKeys) {
|
|
222
247
|
if (own(entry, option) === undefined) {
|
|
223
248
|
problems.push({ kind: "missing", dimension: d.name, option });
|
|
@@ -244,7 +269,9 @@ function matches(pairs, configuration) {
|
|
|
244
269
|
* enumerated together. Every other priced dimension contributes its
|
|
245
270
|
* smallest modifier to the minimum and its largest to the maximum,
|
|
246
271
|
* independently of the rest, which yields the same band as the full
|
|
247
|
-
* enumeration.
|
|
272
|
+
* enumeration. A priced text dimension is empty or non-empty: its `*`
|
|
273
|
+
* modifier lowers the minimum when negative and raises the maximum
|
|
274
|
+
* when positive.
|
|
248
275
|
*/
|
|
249
276
|
export function computeBand(configurator, block) {
|
|
250
277
|
const base = parseDecimal(block["basePrice"]);
|
|
@@ -279,8 +306,17 @@ export function computeBand(configurator, block) {
|
|
|
279
306
|
continue;
|
|
280
307
|
seen.add(d.name);
|
|
281
308
|
if (d.optionKeys === null) {
|
|
282
|
-
if (d.priced)
|
|
309
|
+
if (!d.priced)
|
|
310
|
+
continue;
|
|
311
|
+
if (!d.text)
|
|
312
|
+
return null;
|
|
313
|
+
const amount = modifierOf(d, TEXT_KEY);
|
|
314
|
+
if (amount === undefined)
|
|
283
315
|
return null;
|
|
316
|
+
if (amount.units < 0n)
|
|
317
|
+
min = addDecimal(min, amount);
|
|
318
|
+
else
|
|
319
|
+
max = addDecimal(max, amount);
|
|
284
320
|
continue;
|
|
285
321
|
}
|
|
286
322
|
if (coupledNames.has(d.name)) {
|
package/dist/rules/pdp-e-008.js
CHANGED
|
@@ -35,7 +35,7 @@ export const pdpE008 = {
|
|
|
35
35
|
return hits.length > 0 ? hits : null;
|
|
36
36
|
},
|
|
37
37
|
messageTemplate: "Modifier for base option `{option}` of `{dimension}` at `{path}` is `{modifier}`; the base option's modifier is `0`.",
|
|
38
|
-
remediation: "Set the base option's modifier to
|
|
38
|
+
remediation: "Set the base option's modifier to zero and express the other options of the dimension as differences from it. If the modifier is right, `basePrice` is the total of another configuration: correct `baseConfiguration` or `basePrice`.",
|
|
39
39
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
40
40
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
41
41
|
};
|
|
@@ -7,6 +7,6 @@ import type { Rule } from "@pagefront/lint-core";
|
|
|
7
7
|
* A `modifiers` key naming no dimension, or a dimension declared
|
|
8
8
|
* `affectsPrice: false`, fires once at that key; under a priced
|
|
9
9
|
* dimension, each option key that is not one of its options fires at
|
|
10
|
-
* its own path.
|
|
10
|
+
* its own path. Under a priced text dimension, `*` is the one valid key.
|
|
11
11
|
*/
|
|
12
12
|
export declare const pdpE009: Rule;
|
package/dist/rules/pdp-e-009.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asObject, resolveConfigurator, seg } from "./configurator-pricing.js";
|
|
1
|
+
import { asObject, resolveConfigurator, seg, TEXT_KEY } from "./configurator-pricing.js";
|
|
2
2
|
/**
|
|
3
3
|
* PDP-E-009 — Modifier for an unknown dimension or option.
|
|
4
4
|
* Transcribed from commerce/spec/rules.md (normative).
|
|
@@ -7,7 +7,7 @@ import { asObject, resolveConfigurator, seg } from "./configurator-pricing.js";
|
|
|
7
7
|
* A `modifiers` key naming no dimension, or a dimension declared
|
|
8
8
|
* `affectsPrice: false`, fires once at that key; under a priced
|
|
9
9
|
* dimension, each option key that is not one of its options fires at
|
|
10
|
-
* its own path.
|
|
10
|
+
* its own path. Under a priced text dimension, `*` is the one valid key.
|
|
11
11
|
*/
|
|
12
12
|
export const pdpE009 = {
|
|
13
13
|
id: "PDP-E-009",
|
|
@@ -41,7 +41,7 @@ export const pdpE009 = {
|
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
43
|
for (const option of Object.keys(asObject(entry) ?? {})) {
|
|
44
|
-
if (d.optionKeys !== null && d.optionKeys.includes(option))
|
|
44
|
+
if (d.text ? option === TEXT_KEY : d.optionKeys !== null && d.optionKeys.includes(option))
|
|
45
45
|
continue;
|
|
46
46
|
hits.push({
|
|
47
47
|
path: `${path}${seg(option)}`,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-013 — Text dimension with options.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires on a dimension whose `inputType` is `text` and whose `options`
|
|
7
|
+
* is an array with at least one entry.
|
|
8
|
+
*/
|
|
9
|
+
export declare const pdpE013: Rule;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { asObject } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-013 — Text dimension with options.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires on a dimension whose `inputType` is `text` and whose `options`
|
|
7
|
+
* is an array with at least one entry.
|
|
8
|
+
*/
|
|
9
|
+
export const pdpE013 = {
|
|
10
|
+
id: "PDP-E-013",
|
|
11
|
+
title: "Text dimension with options",
|
|
12
|
+
severity: "error",
|
|
13
|
+
target: "$.data['pagefront:configurator'].dimensions[*]",
|
|
14
|
+
check: (match) => {
|
|
15
|
+
const d = asObject(match.value);
|
|
16
|
+
if (d === null || d["inputType"] !== "text")
|
|
17
|
+
return null;
|
|
18
|
+
if (!Array.isArray(d["options"]) || d["options"].length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
return { values: { dimension: typeof d["name"] === "string" ? d["name"] : "(unnamed)" } };
|
|
21
|
+
},
|
|
22
|
+
messageTemplate: "Dimension `{dimension}` at `{path}` declares `inputType: \"text\"` and enumerates options.",
|
|
23
|
+
remediation: "Remove `options` from a free-text dimension, or remove `inputType: \"text\"` from a dimension the buyer picks a value in.",
|
|
24
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
25
|
+
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
26
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-014 — Size advice in another system than the sizing dimension.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the adjustment carries a `sizeSystem`, the configurator has
|
|
7
|
+
* a sizing dimension in `UK`, `EU`, `US` or `JP`, and the adjustment's
|
|
8
|
+
* system corresponds to none of them. The `https://schema.org/` prefix
|
|
9
|
+
* on `sizeSystem` is accepted.
|
|
10
|
+
*/
|
|
11
|
+
export declare const pdpE014: Rule;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { asObject, CORRESPONDING_SIZE_SYSTEM, readConfigurator } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-014 — Size advice in another system than the sizing dimension.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the adjustment carries a `sizeSystem`, the configurator has
|
|
7
|
+
* a sizing dimension in `UK`, `EU`, `US` or `JP`, and the adjustment's
|
|
8
|
+
* system corresponds to none of them. The `https://schema.org/` prefix
|
|
9
|
+
* on `sizeSystem` is accepted.
|
|
10
|
+
*/
|
|
11
|
+
export const pdpE014 = {
|
|
12
|
+
id: "PDP-E-014",
|
|
13
|
+
title: "Size advice in another system than the sizing dimension",
|
|
14
|
+
severity: "error",
|
|
15
|
+
target: "$.data['pagefront:sizeRecommendation'].sizeAdjustment",
|
|
16
|
+
check: (match, ctx) => {
|
|
17
|
+
const sizeSystem = asObject(match.value)?.["sizeSystem"];
|
|
18
|
+
if (typeof sizeSystem !== "string")
|
|
19
|
+
return null;
|
|
20
|
+
const systems = (readConfigurator(ctx.sheet)?.dimensions ?? [])
|
|
21
|
+
.map((d) => d.system)
|
|
22
|
+
.filter((s) => s !== undefined && Object.hasOwn(CORRESPONDING_SIZE_SYSTEM, s));
|
|
23
|
+
if (systems.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const bare = sizeSystem.replace(/^https?:\/\/schema\.org\//, "");
|
|
26
|
+
if (systems.some((s) => CORRESPONDING_SIZE_SYSTEM[s] === bare))
|
|
27
|
+
return null;
|
|
28
|
+
return { values: { sizeSystem, dimensionSystem: systems[0] } };
|
|
29
|
+
},
|
|
30
|
+
messageTemplate: "Size adjustment at `{path}` is in `{sizeSystem}` but the Sheet's sizing dimension is in `{dimensionSystem}`.",
|
|
31
|
+
remediation: "State the adjustment in the sizing dimension's system if the merchant states it there. If the merchant's advice is in another system, remove `sizeAdjustment` and keep the merchant's wording in `description`.",
|
|
32
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
33
|
+
specReference: "product.md#size-guidance-pagefrontsizechart-and-pagefrontsizerecommendation",
|
|
34
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { asObject, seg } from "./configurator-pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-015 — Amounts mix numbers and strings.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Walks `data` in document order over the catalogue's closed list of
|
|
7
|
+
* amount fields and fires once per Sheet when at least one amount is a
|
|
8
|
+
* JSON number and at least one is a string. `numberPath` and
|
|
9
|
+
* `stringPath` are the first amount of each carrier.
|
|
10
|
+
*/
|
|
11
|
+
const OFFER_FIELDS = ["price", "lowPrice", "highPrice"];
|
|
12
|
+
const SPECIFICATION_FIELDS = ["price", "minPrice", "maxPrice"];
|
|
13
|
+
const MONETARY_FIELDS = ["value", "minValue", "maxValue"];
|
|
14
|
+
const ROLE_OF_KEY = {
|
|
15
|
+
offers: "offer",
|
|
16
|
+
priceSpecification: "specification",
|
|
17
|
+
hasVariant: "variant",
|
|
18
|
+
"pagefront:promotions": "promotion",
|
|
19
|
+
"pagefront:configuratorPricing": "pricing",
|
|
20
|
+
};
|
|
21
|
+
function record(found, value, path) {
|
|
22
|
+
if (typeof value === "number")
|
|
23
|
+
found.numberPath ??= path;
|
|
24
|
+
else if (typeof value === "string")
|
|
25
|
+
found.stringPath ??= path;
|
|
26
|
+
}
|
|
27
|
+
function amountFields(node, role) {
|
|
28
|
+
const fields = [];
|
|
29
|
+
if (role === "offer")
|
|
30
|
+
fields.push(...OFFER_FIELDS);
|
|
31
|
+
if (role === "specification")
|
|
32
|
+
fields.push(...SPECIFICATION_FIELDS);
|
|
33
|
+
if (role === "variant")
|
|
34
|
+
fields.push("price");
|
|
35
|
+
if (role === "promotion")
|
|
36
|
+
fields.push("discountAmount");
|
|
37
|
+
if (role === "pricing")
|
|
38
|
+
fields.push("basePrice");
|
|
39
|
+
if (role === "adjustment")
|
|
40
|
+
fields.push("priceModifier");
|
|
41
|
+
if (node["@type"] === "MonetaryAmount")
|
|
42
|
+
fields.push(...MONETARY_FIELDS);
|
|
43
|
+
return fields;
|
|
44
|
+
}
|
|
45
|
+
function walk(node, path, role, found) {
|
|
46
|
+
if (Array.isArray(node)) {
|
|
47
|
+
node.forEach((item, index) => walk(item, `${path}[${index}]`, role, found));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const obj = asObject(node);
|
|
51
|
+
if (obj === null)
|
|
52
|
+
return;
|
|
53
|
+
const fields = amountFields(obj, role);
|
|
54
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
55
|
+
const childPath = `${path}${seg(key)}`;
|
|
56
|
+
if (fields.includes(key)) {
|
|
57
|
+
record(found, value, childPath);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (role === "modifiers") {
|
|
61
|
+
// one entry per dimension; every value under it is an amount
|
|
62
|
+
for (const [option, amount] of Object.entries(asObject(value) ?? {})) {
|
|
63
|
+
record(found, amount, `${childPath}${seg(option)}`);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
let childRole = ROLE_OF_KEY[key];
|
|
68
|
+
if (role === "pricing" && key === "modifiers")
|
|
69
|
+
childRole = "modifiers";
|
|
70
|
+
if (role === "pricing" && key === "adjustments")
|
|
71
|
+
childRole = "adjustment";
|
|
72
|
+
walk(value, childPath, childRole, found);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export const pdpE015 = {
|
|
76
|
+
id: "PDP-E-015",
|
|
77
|
+
title: "Amounts mix numbers and strings",
|
|
78
|
+
severity: "error",
|
|
79
|
+
target: "$.data",
|
|
80
|
+
check: (match) => {
|
|
81
|
+
const found = {};
|
|
82
|
+
walk(match.value, match.path, undefined, found);
|
|
83
|
+
if (found.numberPath === undefined || found.stringPath === undefined)
|
|
84
|
+
return null;
|
|
85
|
+
return { values: { numberPath: found.numberPath, stringPath: found.stringPath } };
|
|
86
|
+
},
|
|
87
|
+
messageTemplate: "Amounts mix numbers and strings; use one carrier throughout the Sheet.",
|
|
88
|
+
remediation: "Carry every amount the same way. Decimal strings are recommended when amounts have fractional parts.",
|
|
89
|
+
introduced: "v0.9 (2026-10-01 revision)",
|
|
90
|
+
specReference: "product.md#amounts",
|
|
91
|
+
};
|
|
@@ -4,7 +4,8 @@ import type { Rule } from "@pagefront/lint-core";
|
|
|
4
4
|
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
5
|
*
|
|
6
6
|
* Applies to a resolving pricing block whose `pricing` is absent or
|
|
7
|
-
* `complete`. One finding per priced option with no modifier,
|
|
8
|
-
*
|
|
7
|
+
* `complete`. One finding per priced option with no modifier, one per
|
|
8
|
+
* priced text dimension with no `*` modifier, and one per priced select
|
|
9
|
+
* dimension that enumerates no options, all at the block.
|
|
9
10
|
*/
|
|
10
11
|
export declare const pdpW032: Rule;
|