@pagefront/lint-commerce 0.4.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 +1 -1
- package/dist/rules/offer-price.d.ts +28 -0
- package/dist/rules/offer-price.js +57 -0
- 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
|
};
|
|
@@ -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.8.0",
|
|
21
21
|
rules: [orgE001, orgW001, orgW002, orgW003, orgW004, orgW005, orgW006],
|
|
22
22
|
};
|
|
@@ -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,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