@pagefront/lint-commerce 0.3.0 → 0.5.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/dist/catalogue.js +5 -2
- package/dist/organization-catalogue.js +3 -2
- package/dist/rules/context-prefix.d.ts +16 -0
- package/dist/rules/context-prefix.js +44 -0
- package/dist/rules/offer-price.d.ts +28 -0
- package/dist/rules/offer-price.js +57 -0
- package/dist/rules/org-e-001.d.ts +7 -0
- package/dist/rules/org-e-001.js +17 -0
- package/dist/rules/pdp-e-003.d.ts +2 -6
- package/dist/rules/pdp-e-003.js +4 -42
- package/dist/rules/pdp-e-004.d.ts +2 -0
- package/dist/rules/pdp-e-004.js +47 -0
- package/dist/rules/pdp-w-028.d.ts +10 -0
- package/dist/rules/pdp-w-028.js +29 -0
- package/dist/rules/pdp-w-029.d.ts +10 -0
- package/dist/rules/pdp-w-029.js +28 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/catalogue.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pdpE001 } from "./rules/pdp-e-001.js";
|
|
2
2
|
import { pdpE002 } from "./rules/pdp-e-002.js";
|
|
3
3
|
import { pdpE003 } from "./rules/pdp-e-003.js";
|
|
4
|
+
import { pdpE004 } from "./rules/pdp-e-004.js";
|
|
4
5
|
import { pdpW001 } from "./rules/pdp-w-001.js";
|
|
5
6
|
import { pdpW002 } from "./rules/pdp-w-002.js";
|
|
6
7
|
import { pdpW003 } from "./rules/pdp-w-003.js";
|
|
@@ -28,6 +29,8 @@ import { pdpW024 } from "./rules/pdp-w-024.js";
|
|
|
28
29
|
import { pdpW025 } from "./rules/pdp-w-025.js";
|
|
29
30
|
import { pdpW026 } from "./rules/pdp-w-026.js";
|
|
30
31
|
import { pdpW027 } from "./rules/pdp-w-027.js";
|
|
32
|
+
import { pdpW028 } from "./rules/pdp-w-028.js";
|
|
33
|
+
import { pdpW029 } from "./rules/pdp-w-029.js";
|
|
31
34
|
import { pdpI001 } from "./rules/pdp-i-001.js";
|
|
32
35
|
/**
|
|
33
36
|
* The commerce product-sheet rule catalogue — the executable form of
|
|
@@ -39,6 +42,6 @@ import { pdpI001 } from "./rules/pdp-i-001.js";
|
|
|
39
42
|
export const commerceCatalogue = {
|
|
40
43
|
vertical: "commerce",
|
|
41
44
|
formatVersion: "0.9",
|
|
42
|
-
catalogueVersion: "0.
|
|
43
|
-
rules: [pdpE001, pdpE002, pdpE003, 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, pdpI001],
|
|
45
|
+
catalogueVersion: "0.8.0",
|
|
46
|
+
rules: [pdpE001, pdpE002, pdpE003, pdpE004, 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, pdpI001],
|
|
44
47
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { orgE001 } from "./rules/org-e-001.js";
|
|
1
2
|
import { orgW001 } from "./rules/org-w-001.js";
|
|
2
3
|
import { orgW002 } from "./rules/org-w-002.js";
|
|
3
4
|
import { orgW003 } from "./rules/org-w-003.js";
|
|
@@ -16,6 +17,6 @@ import { orgW006 } from "./rules/org-w-006.js";
|
|
|
16
17
|
export const organizationCatalogue = {
|
|
17
18
|
vertical: "commerce",
|
|
18
19
|
formatVersion: "0.9",
|
|
19
|
-
catalogueVersion: "0.
|
|
20
|
-
rules: [orgW001, orgW002, orgW003, orgW004, orgW005, orgW006],
|
|
20
|
+
catalogueVersion: "0.8.0",
|
|
21
|
+
rules: [orgE001, orgW001, orgW002, orgW003, orgW004, orgW005, orgW006],
|
|
21
22
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by PDP-E-003 and ORG-E-001: does the data block declare the
|
|
3
|
+
* `pagefront` prefix, and which `pagefront:` term does it use first?
|
|
4
|
+
* A term is a property name or an `@type` value beginning with
|
|
5
|
+
* `pagefront:`, anywhere in the block; the walk is depth-first in
|
|
6
|
+
* document order, so the named term is the first one a reader meets.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VOCABULARY_IRI = "https://themachineweb.org/pagefront/vocabulary/";
|
|
9
|
+
export declare const declaresPagefrontPrefix: (context: unknown) => boolean;
|
|
10
|
+
export declare const firstPagefrontTerm: (value: unknown) => string | undefined;
|
|
11
|
+
/** The check both rules share: null when the prefix is declared or no term is used. */
|
|
12
|
+
export declare const missingPrefixTerm: (data: unknown) => {
|
|
13
|
+
values: {
|
|
14
|
+
term: string;
|
|
15
|
+
};
|
|
16
|
+
} | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by PDP-E-003 and ORG-E-001: does the data block declare the
|
|
3
|
+
* `pagefront` prefix, and which `pagefront:` term does it use first?
|
|
4
|
+
* A term is a property name or an `@type` value beginning with
|
|
5
|
+
* `pagefront:`, anywhere in the block; the walk is depth-first in
|
|
6
|
+
* document order, so the named term is the first one a reader meets.
|
|
7
|
+
*/
|
|
8
|
+
export const VOCABULARY_IRI = "https://themachineweb.org/pagefront/vocabulary/";
|
|
9
|
+
export const declaresPagefrontPrefix = (context) => Array.isArray(context) &&
|
|
10
|
+
context.some((entry) => typeof entry === "object" &&
|
|
11
|
+
entry !== null &&
|
|
12
|
+
!Array.isArray(entry) &&
|
|
13
|
+
entry["pagefront"] === VOCABULARY_IRI);
|
|
14
|
+
export const firstPagefrontTerm = (value) => {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const item of value) {
|
|
17
|
+
const found = firstPagefrontTerm(item);
|
|
18
|
+
if (found !== undefined)
|
|
19
|
+
return found;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value !== "object" || value === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
for (const [key, child] of Object.entries(value)) {
|
|
26
|
+
if (key.startsWith("pagefront:"))
|
|
27
|
+
return key;
|
|
28
|
+
if (key === "@type" && typeof child === "string" && child.startsWith("pagefront:"))
|
|
29
|
+
return child;
|
|
30
|
+
const found = firstPagefrontTerm(child);
|
|
31
|
+
if (found !== undefined)
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
};
|
|
36
|
+
/** The check both rules share: null when the prefix is declared or no term is used. */
|
|
37
|
+
export const missingPrefixTerm = (data) => {
|
|
38
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
39
|
+
return null;
|
|
40
|
+
if (declaresPagefrontPrefix(data["@context"]))
|
|
41
|
+
return null;
|
|
42
|
+
const term = firstPagefrontTerm(data);
|
|
43
|
+
return term === undefined ? null : { values: { term } };
|
|
44
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the offer-price rules (PDP-E-004, PDP-W-028,
|
|
3
|
+
* PDP-W-029). The schema admits `priceSpecification` as one object or
|
|
4
|
+
* an array of objects; the rules treat an object as a single entry.
|
|
5
|
+
*/
|
|
6
|
+
export interface PriceSpecificationEntry {
|
|
7
|
+
entry: Record<string, unknown>;
|
|
8
|
+
/** Concrete JSONPath of the entry, in the engine's normalized style. */
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Every object entry of an offer's `priceSpecification`, with the path
|
|
13
|
+
* each would carry in a finding. A bare object yields one entry at the
|
|
14
|
+
* field's own path; an array yields one per object element.
|
|
15
|
+
*/
|
|
16
|
+
export declare function priceSpecificationEntries(offer: Record<string, unknown>, offerPath: string): PriceSpecificationEntry[];
|
|
17
|
+
/**
|
|
18
|
+
* True when the offer states a price at all: a `price` field, or a
|
|
19
|
+
* `priceSpecification` with at least one member. Offers declaring
|
|
20
|
+
* `pagefront:priceOnRequest` are outside the pricing rules entirely.
|
|
21
|
+
*/
|
|
22
|
+
export declare function statesPrice(offer: Record<string, unknown>): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* A numeric reading of a Schema.org price value: a number, or a string
|
|
25
|
+
* holding a plain decimal. Anything else (empty, localized, with a
|
|
26
|
+
* currency symbol) is not comparable and yields undefined.
|
|
27
|
+
*/
|
|
28
|
+
export declare function numericPrice(value: unknown): number | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the offer-price rules (PDP-E-004, PDP-W-028,
|
|
3
|
+
* PDP-W-029). The schema admits `priceSpecification` as one object or
|
|
4
|
+
* an array of objects; the rules treat an object as a single entry.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Every object entry of an offer's `priceSpecification`, with the path
|
|
8
|
+
* each would carry in a finding. A bare object yields one entry at the
|
|
9
|
+
* field's own path; an array yields one per object element.
|
|
10
|
+
*/
|
|
11
|
+
export function priceSpecificationEntries(offer, offerPath) {
|
|
12
|
+
const spec = offer["priceSpecification"];
|
|
13
|
+
const fieldPath = `${offerPath}['priceSpecification']`;
|
|
14
|
+
if (Array.isArray(spec)) {
|
|
15
|
+
const out = [];
|
|
16
|
+
spec.forEach((item, index) => {
|
|
17
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
18
|
+
out.push({ entry: item, path: `${fieldPath}[${index}]` });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
if (typeof spec === "object" && spec !== null) {
|
|
24
|
+
return [{ entry: spec, path: fieldPath }];
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True when the offer states a price at all: a `price` field, or a
|
|
30
|
+
* `priceSpecification` with at least one member. Offers declaring
|
|
31
|
+
* `pagefront:priceOnRequest` are outside the pricing rules entirely.
|
|
32
|
+
*/
|
|
33
|
+
export function statesPrice(offer) {
|
|
34
|
+
if (offer["pagefront:priceOnRequest"] !== undefined)
|
|
35
|
+
return false;
|
|
36
|
+
if (offer["price"] !== undefined)
|
|
37
|
+
return true;
|
|
38
|
+
const spec = offer["priceSpecification"];
|
|
39
|
+
if (Array.isArray(spec))
|
|
40
|
+
return spec.length > 0;
|
|
41
|
+
if (typeof spec === "object" && spec !== null)
|
|
42
|
+
return Object.keys(spec).length > 0;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A numeric reading of a Schema.org price value: a number, or a string
|
|
47
|
+
* holding a plain decimal. Anything else (empty, localized, with a
|
|
48
|
+
* currency symbol) is not comparable and yields undefined.
|
|
49
|
+
*/
|
|
50
|
+
export function numericPrice(value) {
|
|
51
|
+
if (typeof value === "number")
|
|
52
|
+
return Number.isFinite(value) ? value : undefined;
|
|
53
|
+
if (typeof value === "string" && /^\s*-?\d+(\.\d+)?\s*$/.test(value)) {
|
|
54
|
+
return Number(value);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-E-001 — `pagefront:` term without the vocabulary prefix in `@context`.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative). The organization
|
|
5
|
+
* counterpart of PDP-E-003; the check is shared (context-prefix.ts).
|
|
6
|
+
*/
|
|
7
|
+
export declare const orgE001: Rule;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { missingPrefixTerm } from "./context-prefix.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-E-001 — `pagefront:` term without the vocabulary prefix in `@context`.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative). The organization
|
|
5
|
+
* counterpart of PDP-E-003; the check is shared (context-prefix.ts).
|
|
6
|
+
*/
|
|
7
|
+
export const orgE001 = {
|
|
8
|
+
id: "ORG-E-001",
|
|
9
|
+
title: "pagefront: term without the vocabulary prefix",
|
|
10
|
+
severity: "error",
|
|
11
|
+
target: "$.data",
|
|
12
|
+
check: (match) => missingPrefixTerm(match.value),
|
|
13
|
+
messageTemplate: "The Sheet uses `{term}` but `@context` at `{path}` does not declare the `pagefront` prefix, so the term does not resolve to the Pagefront vocabulary.",
|
|
14
|
+
remediation: 'Declare the prefix: `"@context": ["https://schema.org", {"pagefront": "https://themachineweb.org/pagefront/vocabulary/"}]`.',
|
|
15
|
+
introduced: "v0.9",
|
|
16
|
+
specReference: "vocabulary.md#how-to-use-this-vocabulary",
|
|
17
|
+
};
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { Rule } from "@pagefront/lint-core";
|
|
2
2
|
/**
|
|
3
3
|
* PDP-E-003 — `pagefront:` term without the vocabulary prefix in `@context`.
|
|
4
|
-
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
-
*
|
|
6
|
-
* A term is a property name or an `@type` value beginning with
|
|
7
|
-
* `pagefront:`, anywhere in the data block. The walk is depth-first in
|
|
8
|
-
* document order, so the named term is the first one a reader meets.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative). The check is
|
|
5
|
+
* shared with ORG-E-001 (context-prefix.ts).
|
|
9
6
|
*/
|
|
10
|
-
export declare const VOCABULARY_IRI = "https://themachineweb.org/pagefront/vocabulary/";
|
|
11
7
|
export declare const pdpE003: Rule;
|
package/dist/rules/pdp-e-003.js
CHANGED
|
@@ -1,53 +1,15 @@
|
|
|
1
|
+
import { missingPrefixTerm } from "./context-prefix.js";
|
|
1
2
|
/**
|
|
2
3
|
* PDP-E-003 — `pagefront:` term without the vocabulary prefix in `@context`.
|
|
3
|
-
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
-
*
|
|
5
|
-
* A term is a property name or an `@type` value beginning with
|
|
6
|
-
* `pagefront:`, anywhere in the data block. The walk is depth-first in
|
|
7
|
-
* document order, so the named term is the first one a reader meets.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative). The check is
|
|
5
|
+
* shared with ORG-E-001 (context-prefix.ts).
|
|
8
6
|
*/
|
|
9
|
-
export const VOCABULARY_IRI = "https://themachineweb.org/pagefront/vocabulary/";
|
|
10
|
-
const declaresPrefix = (context) => Array.isArray(context) &&
|
|
11
|
-
context.some((entry) => typeof entry === "object" &&
|
|
12
|
-
entry !== null &&
|
|
13
|
-
!Array.isArray(entry) &&
|
|
14
|
-
entry["pagefront"] === VOCABULARY_IRI);
|
|
15
|
-
const firstTerm = (value) => {
|
|
16
|
-
if (Array.isArray(value)) {
|
|
17
|
-
for (const item of value) {
|
|
18
|
-
const found = firstTerm(item);
|
|
19
|
-
if (found !== undefined)
|
|
20
|
-
return found;
|
|
21
|
-
}
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
if (typeof value !== "object" || value === null)
|
|
25
|
-
return undefined;
|
|
26
|
-
for (const [key, child] of Object.entries(value)) {
|
|
27
|
-
if (key.startsWith("pagefront:"))
|
|
28
|
-
return key;
|
|
29
|
-
if (key === "@type" && typeof child === "string" && child.startsWith("pagefront:"))
|
|
30
|
-
return child;
|
|
31
|
-
const found = firstTerm(child);
|
|
32
|
-
if (found !== undefined)
|
|
33
|
-
return found;
|
|
34
|
-
}
|
|
35
|
-
return undefined;
|
|
36
|
-
};
|
|
37
7
|
export const pdpE003 = {
|
|
38
8
|
id: "PDP-E-003",
|
|
39
9
|
title: "pagefront: term without the vocabulary prefix",
|
|
40
10
|
severity: "error",
|
|
41
11
|
target: "$.data",
|
|
42
|
-
check: (match) =>
|
|
43
|
-
const data = match.value;
|
|
44
|
-
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
45
|
-
return null;
|
|
46
|
-
if (declaresPrefix(data["@context"]))
|
|
47
|
-
return null;
|
|
48
|
-
const term = firstTerm(data);
|
|
49
|
-
return term === undefined ? null : { values: { term } };
|
|
50
|
-
},
|
|
12
|
+
check: (match) => missingPrefixTerm(match.value),
|
|
51
13
|
messageTemplate: "The Sheet uses `{term}` but `@context` at `{path}` does not declare the `pagefront` prefix, so the term does not resolve to the Pagefront vocabulary.",
|
|
52
14
|
remediation: 'Declare the prefix: `"@context": ["https://schema.org", {"pagefront": "https://themachineweb.org/pagefront/vocabulary/"}]`.',
|
|
53
15
|
introduced: "v0.9",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { numericPrice, priceSpecificationEntries } from "./offer-price.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-004 — Offer price disagrees with its mirroring price specification.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A mirroring entry carries a numeric `price` in the Offer's own
|
|
7
|
+
* currency and none of the fields that legitimately make an entry's
|
|
8
|
+
* amount differ from `price` (`priceType` for sale pricing,
|
|
9
|
+
* `eligibleQuantity` for tiers, `validFrom`/`validThrough` for
|
|
10
|
+
* scheduled prices). One finding per disagreeing entry, at the entry.
|
|
11
|
+
*/
|
|
12
|
+
const DIFFERENTIATING_FIELDS = ["priceType", "eligibleQuantity", "validFrom", "validThrough"];
|
|
13
|
+
export const pdpE004 = {
|
|
14
|
+
id: "PDP-E-004",
|
|
15
|
+
title: "Offer price disagrees with its mirroring price specification",
|
|
16
|
+
severity: "error",
|
|
17
|
+
target: "$.data.offers[*]",
|
|
18
|
+
check: (match) => {
|
|
19
|
+
const offer = match.value;
|
|
20
|
+
if (typeof offer !== "object" || offer === null)
|
|
21
|
+
return null;
|
|
22
|
+
const o = offer;
|
|
23
|
+
const price = numericPrice(o["price"]);
|
|
24
|
+
const currency = o["priceCurrency"];
|
|
25
|
+
if (price === undefined || typeof currency !== "string")
|
|
26
|
+
return null;
|
|
27
|
+
const hits = [];
|
|
28
|
+
for (const { entry, path } of priceSpecificationEntries(o, match.path)) {
|
|
29
|
+
if (entry["priceCurrency"] !== currency)
|
|
30
|
+
continue;
|
|
31
|
+
if (DIFFERENTIATING_FIELDS.some((field) => entry[field] !== undefined))
|
|
32
|
+
continue;
|
|
33
|
+
const specPrice = numericPrice(entry["price"]);
|
|
34
|
+
if (specPrice === undefined || specPrice === price)
|
|
35
|
+
continue;
|
|
36
|
+
hits.push({
|
|
37
|
+
path,
|
|
38
|
+
values: { price: String(o["price"]), specPrice: String(entry["price"]) },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return hits.length > 0 ? hits : null;
|
|
42
|
+
},
|
|
43
|
+
messageTemplate: "Price specification at `{path}` states `{specPrice}` but its Offer's `price` is `{price}`.",
|
|
44
|
+
remediation: "Make the two amounts equal, or remove the duplicated amount from the specification entry and keep `valueAddedTaxIncluded` (and `priceCurrency`) on it.",
|
|
45
|
+
introduced: "v0.9",
|
|
46
|
+
specReference: "product.md#tax-inclusion",
|
|
47
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-028 — Offer price without a tax-inclusion statement.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the offer states a price (`price`, or a non-empty
|
|
7
|
+
* `priceSpecification`) and no `priceSpecification` entry carries a
|
|
8
|
+
* boolean `valueAddedTaxIncluded`. Price-on-request offers are exempt.
|
|
9
|
+
*/
|
|
10
|
+
export declare const pdpW028: Rule;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { priceSpecificationEntries, statesPrice } from "./offer-price.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-028 — Offer price without a tax-inclusion statement.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the offer states a price (`price`, or a non-empty
|
|
7
|
+
* `priceSpecification`) and no `priceSpecification` entry carries a
|
|
8
|
+
* boolean `valueAddedTaxIncluded`. Price-on-request offers are exempt.
|
|
9
|
+
*/
|
|
10
|
+
export const pdpW028 = {
|
|
11
|
+
id: "PDP-W-028",
|
|
12
|
+
title: "Offer price without a tax-inclusion statement",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
target: "$.data.offers[*]",
|
|
15
|
+
check: (match) => {
|
|
16
|
+
const offer = match.value;
|
|
17
|
+
if (typeof offer !== "object" || offer === null)
|
|
18
|
+
return null;
|
|
19
|
+
const o = offer;
|
|
20
|
+
if (!statesPrice(o))
|
|
21
|
+
return null;
|
|
22
|
+
const stated = priceSpecificationEntries(o, match.path).some(({ entry }) => typeof entry["valueAddedTaxIncluded"] === "boolean");
|
|
23
|
+
return stated ? null : {};
|
|
24
|
+
},
|
|
25
|
+
messageTemplate: "Offer at `{path}` states a price but not whether value-added tax is included.",
|
|
26
|
+
remediation: "Add a `UnitPriceSpecification` entry mirroring `price` with `valueAddedTaxIncluded` set to `true` or `false`; on an Offer that already uses `priceSpecification`, set the flag on each entry.",
|
|
27
|
+
introduced: "v0.9",
|
|
28
|
+
specReference: "product.md#tax-inclusion",
|
|
29
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-029 — Offer price without a declared region.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the offer states a price and carries neither `areaServed`
|
|
7
|
+
* nor `eligibleRegion`. `ineligibleRegion` alone does not satisfy the
|
|
8
|
+
* check. Price-on-request offers are exempt.
|
|
9
|
+
*/
|
|
10
|
+
export declare const pdpW029: Rule;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { statesPrice } from "./offer-price.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-029 — Offer price without a declared region.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Fires when the offer states a price and carries neither `areaServed`
|
|
7
|
+
* nor `eligibleRegion`. `ineligibleRegion` alone does not satisfy the
|
|
8
|
+
* check. Price-on-request offers are exempt.
|
|
9
|
+
*/
|
|
10
|
+
export const pdpW029 = {
|
|
11
|
+
id: "PDP-W-029",
|
|
12
|
+
title: "Offer price without a declared region",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
target: "$.data.offers[*]",
|
|
15
|
+
check: (match) => {
|
|
16
|
+
const offer = match.value;
|
|
17
|
+
if (typeof offer !== "object" || offer === null)
|
|
18
|
+
return null;
|
|
19
|
+
const o = offer;
|
|
20
|
+
if (!statesPrice(o))
|
|
21
|
+
return null;
|
|
22
|
+
return o["areaServed"] === undefined && o["eligibleRegion"] === undefined ? {} : null;
|
|
23
|
+
},
|
|
24
|
+
messageTemplate: "Offer at `{path}` states a price but no region it applies to.",
|
|
25
|
+
remediation: "Declare `areaServed` (or `eligibleRegion`) for the market whose displayed price this is. Where the market cannot be determined from the storefront, stating the tax basis via `valueAddedTaxIncluded` is the higher priority; see \"Tax inclusion.\"",
|
|
26
|
+
introduced: "v0.9",
|
|
27
|
+
specReference: "product.md#tax-inclusion",
|
|
28
|
+
};
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED