@sadrmanesh/commerce-agent-skills 0.0.0-stage → 0.3.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.
Files changed (201) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/bin/commerce-agent-skills.mjs +368 -0
  5. package/package.json +52 -4
  6. package/rules/b2b-pricing.md +11 -0
  7. package/rules/catalog-import-and-migration.md +11 -0
  8. package/rules/checkout-ux-and-conversion.md +12 -0
  9. package/rules/conversion-event-tracking.md +11 -0
  10. package/rules/ecommerce-seo-structured-data.md +11 -0
  11. package/rules/fraud-and-risk-basics.md +11 -0
  12. package/rules/google-merchant-product-feeds.md +11 -0
  13. package/rules/headless-nextjs-storefront.md +10 -0
  14. package/rules/inventory-and-multi-warehouse.md +10 -0
  15. package/rules/iranian-payment-gateways.md +10 -0
  16. package/rules/marketplace-search-and-ranking.md +10 -0
  17. package/rules/mcp-store-tooling.md +11 -0
  18. package/rules/multi-vendor-payouts.md +11 -0
  19. package/rules/order-and-webhook-reliability.md +12 -0
  20. package/rules/payment-gateway-integration.md +12 -0
  21. package/rules/product-catalog-modeling.md +11 -0
  22. package/rules/product-description-guardrails.md +10 -0
  23. package/rules/returns-and-refunds.md +10 -0
  24. package/rules/rtl-and-persian-localization.md +11 -0
  25. package/rules/shopify-app-development.md +10 -0
  26. package/rules/storefront-accessibility.md +11 -0
  27. package/rules/storefront-performance.md +10 -0
  28. package/rules/stripe-payments.md +10 -0
  29. package/rules/subscription-billing.md +10 -0
  30. package/rules/tax-and-shipping.md +10 -0
  31. package/rules/vector-product-search.md +10 -0
  32. package/rules/woocommerce-plugin-development.md +10 -0
  33. package/rules/woocommerce-rest-api.md +10 -0
  34. package/rules/wordpress-plugin-security.md +10 -0
  35. package/scripts/lib/frontmatter.mjs +218 -0
  36. package/skills/b2b-pricing/SKILL.md +55 -0
  37. package/skills/b2b-pricing/evals/examples/net-terms.md +3 -0
  38. package/skills/b2b-pricing/evals/examples/tiered-wholesale.md +3 -0
  39. package/skills/b2b-pricing/evals/tasks.json +19 -0
  40. package/skills/b2b-pricing/references/price-lists.md +46 -0
  41. package/skills/b2b-pricing/scripts/resolve-price.mjs +93 -0
  42. package/skills/catalog-import-and-migration/SKILL.md +58 -0
  43. package/skills/catalog-import-and-migration/evals/examples/platform-migration-urls.md +3 -0
  44. package/skills/catalog-import-and-migration/evals/examples/rerunnable-import.md +3 -0
  45. package/skills/catalog-import-and-migration/evals/tasks.json +20 -0
  46. package/skills/catalog-import-and-migration/references/field-mapping.md +43 -0
  47. package/skills/catalog-import-and-migration/scripts/check-import-rows.mjs +75 -0
  48. package/skills/catalog-order.txt +29 -0
  49. package/skills/checkout-ux-and-conversion/SKILL.md +86 -0
  50. package/skills/checkout-ux-and-conversion/evals/examples/guest-total.md +3 -0
  51. package/skills/checkout-ux-and-conversion/evals/examples/single-submit.md +3 -0
  52. package/skills/checkout-ux-and-conversion/evals/tasks.json +30 -0
  53. package/skills/checkout-ux-and-conversion/references/checkout-review.md +41 -0
  54. package/skills/conversion-event-tracking/SKILL.md +64 -0
  55. package/skills/conversion-event-tracking/evals/examples/purchase-once.md +3 -0
  56. package/skills/conversion-event-tracking/evals/examples/variant-item-id.md +3 -0
  57. package/skills/conversion-event-tracking/evals/tasks.json +30 -0
  58. package/skills/conversion-event-tracking/references/events.md +36 -0
  59. package/skills/conversion-event-tracking/scripts/check-event.mjs +79 -0
  60. package/skills/ecommerce-seo-structured-data/SKILL.md +84 -0
  61. package/skills/ecommerce-seo-structured-data/assets/example-product.jsonld +15 -0
  62. package/skills/ecommerce-seo-structured-data/evals/examples/extend-woocommerce-schema.md +8 -0
  63. package/skills/ecommerce-seo-structured-data/evals/examples/variant-offer.md +7 -0
  64. package/skills/ecommerce-seo-structured-data/evals/tasks.json +29 -0
  65. package/skills/ecommerce-seo-structured-data/references/product-jsonld.md +83 -0
  66. package/skills/ecommerce-seo-structured-data/scripts/check-product-jsonld.mjs +211 -0
  67. package/skills/fraud-and-risk-basics/SKILL.md +66 -0
  68. package/skills/fraud-and-risk-basics/evals/examples/review-queue.md +3 -0
  69. package/skills/fraud-and-risk-basics/evals/examples/wait-for-3ds.md +3 -0
  70. package/skills/fraud-and-risk-basics/evals/tasks.json +30 -0
  71. package/skills/google-merchant-product-feeds/SKILL.md +70 -0
  72. package/skills/google-merchant-product-feeds/assets/example-item.json +11 -0
  73. package/skills/google-merchant-product-feeds/evals/examples/generated-title.md +3 -0
  74. package/skills/google-merchant-product-feeds/evals/examples/stable-variant-id.md +3 -0
  75. package/skills/google-merchant-product-feeds/evals/tasks.json +30 -0
  76. package/skills/google-merchant-product-feeds/references/attributes.md +39 -0
  77. package/skills/google-merchant-product-feeds/scripts/check-feed-item.mjs +92 -0
  78. package/skills/headless-nextjs-storefront/SKILL.md +69 -0
  79. package/skills/headless-nextjs-storefront/evals/examples/revalidate-stock.md +7 -0
  80. package/skills/headless-nextjs-storefront/evals/examples/server-price.md +3 -0
  81. package/skills/headless-nextjs-storefront/evals/tasks.json +30 -0
  82. package/skills/headless-nextjs-storefront/references/cache-and-cart.md +39 -0
  83. package/skills/inventory-and-multi-warehouse/SKILL.md +61 -0
  84. package/skills/inventory-and-multi-warehouse/evals/examples/reserve-last-unit.md +3 -0
  85. package/skills/inventory-and-multi-warehouse/evals/examples/split-order.md +3 -0
  86. package/skills/inventory-and-multi-warehouse/evals/tasks.json +20 -0
  87. package/skills/inventory-and-multi-warehouse/references/platforms.md +40 -0
  88. package/skills/inventory-and-multi-warehouse/scripts/allocate-stock.mjs +72 -0
  89. package/skills/iranian-payment-gateways/SKILL.md +64 -0
  90. package/skills/iranian-payment-gateways/evals/examples/toman-store.md +3 -0
  91. package/skills/iranian-payment-gateways/evals/examples/zarinpal-callback.md +3 -0
  92. package/skills/iranian-payment-gateways/evals/tasks.json +20 -0
  93. package/skills/iranian-payment-gateways/references/zarinpal-v4.md +44 -0
  94. package/skills/iranian-payment-gateways/scripts/check-verify.mjs +62 -0
  95. package/skills/marketplace-search-and-ranking/SKILL.md +93 -0
  96. package/skills/marketplace-search-and-ranking/evals/examples/index-variant.md +3 -0
  97. package/skills/marketplace-search-and-ranking/evals/examples/normalize-query.md +7 -0
  98. package/skills/marketplace-search-and-ranking/evals/tasks.json +29 -0
  99. package/skills/marketplace-search-and-ranking/references/document-and-ranking.md +56 -0
  100. package/skills/marketplace-search-and-ranking/scripts/normalize-search-text.mjs +40 -0
  101. package/skills/mcp-store-tooling/SKILL.md +49 -0
  102. package/skills/mcp-store-tooling/evals/examples/refund-tool-design.md +3 -0
  103. package/skills/mcp-store-tooling/evals/examples/remote-server-auth.md +3 -0
  104. package/skills/mcp-store-tooling/evals/tasks.json +20 -0
  105. package/skills/mcp-store-tooling/references/mcp-notes.md +67 -0
  106. package/skills/mcp-store-tooling/scripts/check-tool-definitions.mjs +69 -0
  107. package/skills/multi-vendor-payouts/SKILL.md +71 -0
  108. package/skills/multi-vendor-payouts/evals/examples/retry-payout.md +3 -0
  109. package/skills/multi-vendor-payouts/evals/examples/split-paid-order.md +3 -0
  110. package/skills/multi-vendor-payouts/evals/tasks.json +30 -0
  111. package/skills/multi-vendor-payouts/references/ledger.md +35 -0
  112. package/skills/order-and-webhook-reliability/SKILL.md +99 -0
  113. package/skills/order-and-webhook-reliability/evals/examples/dedupe-resource-version.md +3 -0
  114. package/skills/order-and-webhook-reliability/evals/examples/verify-raw-body.md +3 -0
  115. package/skills/order-and-webhook-reliability/evals/tasks.json +29 -0
  116. package/skills/order-and-webhook-reliability/references/woocommerce-webhooks.md +58 -0
  117. package/skills/order-and-webhook-reliability/scripts/verify-hmac-signature.mjs +78 -0
  118. package/skills/payment-gateway-integration/SKILL.md +90 -0
  119. package/skills/payment-gateway-integration/evals/examples/confirmed-capture.md +9 -0
  120. package/skills/payment-gateway-integration/evals/examples/replayed-webhook.md +3 -0
  121. package/skills/payment-gateway-integration/evals/tasks.json +30 -0
  122. package/skills/payment-gateway-integration/references/woocommerce-gateway.md +67 -0
  123. package/skills/product-catalog-modeling/SKILL.md +91 -0
  124. package/skills/product-catalog-modeling/assets/example-catalog.json +45 -0
  125. package/skills/product-catalog-modeling/evals/examples/create-variation.md +10 -0
  126. package/skills/product-catalog-modeling/evals/examples/sync-parent.md +7 -0
  127. package/skills/product-catalog-modeling/evals/tasks.json +29 -0
  128. package/skills/product-catalog-modeling/references/variation-model.md +71 -0
  129. package/skills/product-catalog-modeling/scripts/validate-catalog.mjs +234 -0
  130. package/skills/product-description-guardrails/SKILL.md +84 -0
  131. package/skills/product-description-guardrails/evals/examples/facts-only.md +3 -0
  132. package/skills/product-description-guardrails/evals/examples/label-generated.md +3 -0
  133. package/skills/product-description-guardrails/evals/tasks.json +30 -0
  134. package/skills/product-description-guardrails/references/claims.md +24 -0
  135. package/skills/product-description-guardrails/scripts/check-description.mjs +61 -0
  136. package/skills/returns-and-refunds/SKILL.md +55 -0
  137. package/skills/returns-and-refunds/evals/examples/refund-after-inspection.md +3 -0
  138. package/skills/returns-and-refunds/evals/examples/woocommerce-partial-refund.md +3 -0
  139. package/skills/returns-and-refunds/evals/tasks.json +20 -0
  140. package/skills/returns-and-refunds/references/rma-states.md +37 -0
  141. package/skills/rtl-and-persian-localization/SKILL.md +81 -0
  142. package/skills/rtl-and-persian-localization/evals/examples/document-direction.md +7 -0
  143. package/skills/rtl-and-persian-localization/evals/examples/rial-display.md +7 -0
  144. package/skills/rtl-and-persian-localization/evals/tasks.json +30 -0
  145. package/skills/rtl-and-persian-localization/references/persian-text.md +51 -0
  146. package/skills/rtl-and-persian-localization/scripts/normalize-persian.mjs +62 -0
  147. package/skills/shopify-app-development/SKILL.md +73 -0
  148. package/skills/shopify-app-development/evals/examples/dedupe-delivery.md +3 -0
  149. package/skills/shopify-app-development/evals/examples/verify-webhook.md +3 -0
  150. package/skills/shopify-app-development/evals/tasks.json +29 -0
  151. package/skills/shopify-app-development/references/auth-and-webhooks.md +44 -0
  152. package/skills/storefront-accessibility/SKILL.md +59 -0
  153. package/skills/storefront-accessibility/evals/examples/add-to-cart-announcement.md +3 -0
  154. package/skills/storefront-accessibility/evals/examples/checkout-errors.md +3 -0
  155. package/skills/storefront-accessibility/evals/tasks.json +20 -0
  156. package/skills/storefront-accessibility/references/wcag-checklist.md +41 -0
  157. package/skills/storefront-performance/SKILL.md +59 -0
  158. package/skills/storefront-performance/evals/examples/cache-without-leaking-carts.md +3 -0
  159. package/skills/storefront-performance/evals/examples/slow-product-lcp.md +3 -0
  160. package/skills/storefront-performance/evals/tasks.json +20 -0
  161. package/skills/storefront-performance/references/web-vitals.md +55 -0
  162. package/skills/storefront-performance/scripts/check-web-vitals.mjs +49 -0
  163. package/skills/stripe-payments/SKILL.md +72 -0
  164. package/skills/stripe-payments/evals/examples/checkout-session-delayed.md +3 -0
  165. package/skills/stripe-payments/evals/examples/mark-paid-from-webhook.md +5 -0
  166. package/skills/stripe-payments/evals/examples/partial-refund.md +3 -0
  167. package/skills/stripe-payments/evals/tasks.json +27 -0
  168. package/skills/stripe-payments/references/stripe-objects.md +54 -0
  169. package/skills/stripe-payments/scripts/verify-stripe-signature.mjs +62 -0
  170. package/skills/subscription-billing/SKILL.md +76 -0
  171. package/skills/subscription-billing/evals/examples/cancel-at-period-end.md +3 -0
  172. package/skills/subscription-billing/evals/examples/one-renewal.md +3 -0
  173. package/skills/subscription-billing/evals/tasks.json +30 -0
  174. package/skills/subscription-billing/references/states.md +27 -0
  175. package/skills/tax-and-shipping/SKILL.md +61 -0
  176. package/skills/tax-and-shipping/evals/examples/store-tax-lines.md +3 -0
  177. package/skills/tax-and-shipping/evals/examples/unknown-zone.md +3 -0
  178. package/skills/tax-and-shipping/evals/tasks.json +30 -0
  179. package/skills/tax-and-shipping/references/woocommerce.md +22 -0
  180. package/skills/vector-product-search/SKILL.md +66 -0
  181. package/skills/vector-product-search/evals/examples/reembed-on-title.md +3 -0
  182. package/skills/vector-product-search/evals/examples/sku-before-vector.md +3 -0
  183. package/skills/vector-product-search/evals/tasks.json +30 -0
  184. package/skills/vector-product-search/references/index.md +40 -0
  185. package/skills/woocommerce-plugin-development/SKILL.md +84 -0
  186. package/skills/woocommerce-plugin-development/assets/main-plugin-file.php +28 -0
  187. package/skills/woocommerce-plugin-development/evals/examples/declare-hpos.md +9 -0
  188. package/skills/woocommerce-plugin-development/evals/examples/save-order-meta.md +9 -0
  189. package/skills/woocommerce-plugin-development/evals/tasks.json +32 -0
  190. package/skills/woocommerce-plugin-development/references/hooks.md +46 -0
  191. package/skills/woocommerce-plugin-development/references/hpos.md +84 -0
  192. package/skills/woocommerce-rest-api/SKILL.md +90 -0
  193. package/skills/woocommerce-rest-api/evals/examples/https-basic-auth.md +8 -0
  194. package/skills/woocommerce-rest-api/evals/examples/idempotent-order-create.md +7 -0
  195. package/skills/woocommerce-rest-api/evals/tasks.json +30 -0
  196. package/skills/woocommerce-rest-api/references/endpoints.md +66 -0
  197. package/skills/wordpress-plugin-security/SKILL.md +121 -0
  198. package/skills/wordpress-plugin-security/evals/examples/admin-form-nonce.md +9 -0
  199. package/skills/wordpress-plugin-security/evals/examples/private-rest-route.md +9 -0
  200. package/skills/wordpress-plugin-security/evals/tasks.json +31 -0
  201. package/skills/wordpress-plugin-security/references/review-checklist.md +31 -0
@@ -0,0 +1,30 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "purchase-once",
5
+ "task": "Emit a purchase event after payment succeeds, with the order number as the transaction id and value excluding tax.",
6
+ "expects": {
7
+ "must_include": [
8
+ "transaction_id",
9
+ "value"
10
+ ],
11
+ "must_not_include": [
12
+ "transaction_id: \"\""
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "variant-item-id",
18
+ "task": "Track add_to_cart for the blue shirt variant with that variant's SKU, not only the parent product id.",
19
+ "expects": {
20
+ "must_include": [
21
+ "add_to_cart",
22
+ "SHIRT-BLUE"
23
+ ],
24
+ "must_not_include": [
25
+ "button click purchase"
26
+ ]
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,36 @@
1
+ # Commerce events
2
+
3
+ Names follow GA4 recommended ecommerce events: `view_item`, `add_to_cart`, `begin_checkout`, `add_shipping_info`, `add_payment_info`, `purchase`, `refund`.
4
+
5
+ ## Purchase parameters
6
+
7
+ | Parameter | Rule |
8
+ | --- | --- |
9
+ | `transaction_id` | Required, non-empty, unique per order. Not an email and not an empty string. |
10
+ | `currency` | ISO 4217. Required when `value` is set. |
11
+ | `value` | Sum of `price * quantity` for `items`. Exclude `tax` and `shipping`. |
12
+ | `tax` | Optional number. Not included in `value`. |
13
+ | `shipping` | Optional number. Not included in `value`. |
14
+ | `items` | At least one item with `item_id` or `item_name`. |
15
+
16
+ GA4's event reference marks `value` and `currency` with conditions. This project requires them on `purchase` so revenue is not an unlabeled number. `view_item` may omit `value`. If it includes `value`, it includes `currency`.
17
+
18
+ ## Checker
19
+
20
+ `scripts/check-event.mjs` reads:
21
+
22
+ ```json
23
+ {
24
+ "name": "purchase",
25
+ "params": {
26
+ "transaction_id": "1042",
27
+ "currency": "USD",
28
+ "value": 19.0,
29
+ "tax": 0,
30
+ "shipping": 0,
31
+ "items": [{ "item_id": "SHIRT-BLUE", "item_name": "Cotton shirt", "price": 19.0, "quantity": 1 }]
32
+ }
33
+ }
34
+ ```
35
+
36
+ It checks the name, the items, the purchase id, and the value sum when every item has a numeric price and quantity. It does not call an analytics API.
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ const NAMES = new Set([
5
+ "view_item",
6
+ "add_to_cart",
7
+ "begin_checkout",
8
+ "add_shipping_info",
9
+ "add_payment_info",
10
+ "purchase",
11
+ "refund",
12
+ ]);
13
+
14
+ export function checkCommerceEvent(event) {
15
+ const errors = [];
16
+ if (!event || typeof event !== "object") {
17
+ return ["Event must be an object."];
18
+ }
19
+ if (!NAMES.has(event.name)) {
20
+ errors.push(`Unsupported event name: ${String(event.name)}`);
21
+ return errors;
22
+ }
23
+ const params = event.params;
24
+ if (!params || typeof params !== "object") {
25
+ return ["Event params are required."];
26
+ }
27
+ if (!Array.isArray(params.items) || params.items.length === 0) {
28
+ errors.push("items must contain at least one item");
29
+ } else {
30
+ params.items.forEach((item, index) => {
31
+ if (!item || (typeof item.item_id !== "string" && typeof item.item_name !== "string")) {
32
+ errors.push(`items[${index}] needs item_id or item_name`);
33
+ }
34
+ });
35
+ }
36
+ if (params.value !== undefined && (typeof params.currency !== "string" || !/^[A-Z]{3}$/.test(params.currency))) {
37
+ errors.push("currency is required when value is set and must be an ISO 4217 code");
38
+ }
39
+ if (event.name === "purchase" || event.name === "refund") {
40
+ if (typeof params.transaction_id !== "string" || params.transaction_id.trim() === "") {
41
+ errors.push(`${event.name} requires a non-empty transaction_id`);
42
+ }
43
+ if (typeof params.value !== "number") {
44
+ errors.push(`${event.name} requires a numeric value`);
45
+ }
46
+ }
47
+ if (event.name === "purchase" && Array.isArray(params.items)) {
48
+ const priced = params.items.every((item) => typeof item?.price === "number" && typeof item?.quantity === "number");
49
+ if (priced && typeof params.value === "number") {
50
+ const sum = params.items.reduce((total, item) => total + item.price * item.quantity, 0);
51
+ if (Math.abs(sum - params.value) > 0.001) {
52
+ errors.push("value must equal the sum of price times quantity and must exclude tax and shipping");
53
+ }
54
+ }
55
+ }
56
+ return errors;
57
+ }
58
+
59
+ function readOption(name) {
60
+ const index = process.argv.indexOf(name);
61
+ return index === -1 ? undefined : process.argv[index + 1];
62
+ }
63
+
64
+ const isCli = process.argv[1] && process.argv[1].endsWith("check-event.mjs");
65
+ if (isCli) {
66
+ const file = readOption("--file");
67
+ if (!file) {
68
+ console.error("Usage: node check-event.mjs --file event.json");
69
+ process.exit(2);
70
+ }
71
+ const errors = checkCommerceEvent(JSON.parse(await readFile(file, "utf8")));
72
+ if (errors.length > 0) {
73
+ for (const error of errors) {
74
+ console.error(`ERROR ${error}`);
75
+ }
76
+ process.exit(1);
77
+ }
78
+ console.log("Event passed the local checks.");
79
+ }
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: ecommerce-seo-structured-data
3
+ description: "Add product JSON-LD that matches the visible offer, including price, currency, availability, and images, without a second conflicting Product node. Use when editing product-page SEO, schema.org markup, or merchant listing data."
4
+ license: MIT
5
+ compatibility: "The JSON-LD checker requires Node.js 20 or newer. No network access."
6
+ metadata:
7
+ author: Naser Sadrmanesh
8
+ version: "0.3.0"
9
+ ---
10
+
11
+ # Ecommerce SEO structured data
12
+
13
+ Use this skill when a product page needs schema.org data that matches what the shopper sees. Google's merchant listing and product snippet features have different required fields. Check a document with [`scripts/check-product-jsonld.mjs`](scripts/check-product-jsonld.mjs). The field list and the WooCommerce hook are in [the JSON-LD notes](references/product-jsonld.md). A passing merchant example is [`assets/example-product.jsonld`](assets/example-product.jsonld).
14
+
15
+ Requirements below follow Google's Search Central product structured data documentation as reviewed for this skill. Rich result rules change. Re-check that documentation when a page is rejected, and do not promise a rich result.
16
+
17
+ ## Procedure
18
+
19
+ 1. Find existing Product markup before adding any. WooCommerce core prints Product data through `WC_Structured_Data` on the single product template. A second Product node for the same item conflicts with the first.
20
+ 2. Change WooCommerce output with the `woocommerce_structured_data_product` filter. Add a new JSON-LD script only on a headless or custom storefront that does not already emit one.
21
+ 3. Mark up the offer the page actually sells. A variant page uses that variant's price, currency, SKU, and availability. A parent page that spans several prices uses an `AggregateOffer` only for a product snippet, not as the sole offer for a merchant listing.
22
+ 4. Put JSON-LD in the server-rendered HTML. Google documents that Product markup built only in the browser can make shopping crawls less reliable for price and availability.
23
+ 5. Run the checker in the profile you are targeting:
24
+
25
+ ```bash
26
+ node scripts/check-product-jsonld.mjs --file product.json --profile merchant
27
+ node scripts/check-product-jsonld.mjs --file product.json --profile snippet
28
+ ```
29
+
30
+ ## Merchant listing profile
31
+
32
+ The checker requires, for each Product:
33
+
34
+ - `name`
35
+ - `image` as a crawlable URL, an `ImageObject` with `url`, or a list of those
36
+ - `offers` as one `Offer`, not an `AggregateOffer` and not an array
37
+ - a numeric `price` greater than zero, or the same under `priceSpecification` when `offers.price` is absent
38
+ - `priceCurrency` as a three-letter ISO 4217 code on the same object that carries the price
39
+
40
+ If both `offers.price` and `offers.priceSpecification.price` are present, Google uses `offers.price`. The checker flags a non-positive `offers.price` in that case.
41
+
42
+ `availability` is recommended. Use one schema.org value such as `https://schema.org/InStock` or `https://schema.org/OutOfStock`. Do not mark an out-of-stock variant as `InStock`.
43
+
44
+ ## Product snippet profile
45
+
46
+ The checker requires `name` and at least one of `review`, `aggregateRating`, or `offers`.
47
+
48
+ - An `Offer` needs a numeric price and `priceCurrency`.
49
+ - An `AggregateOffer` needs numeric `lowPrice` and `highPrice`, `priceCurrency`, and a positive `offerCount`.
50
+ - `aggregateRating`, when present, needs numeric `ratingValue` and either `reviewCount` or `ratingCount`.
51
+
52
+ A snippet may describe a price range. A merchant listing, in the documentation this skill follows, needs a specific offer with a positive price.
53
+
54
+ ## Visible agreement
55
+
56
+ - The price in JSON-LD is the price on the page, in the currency the shopper would be charged, without a thousands separator inside the JSON number.
57
+ - The image URL returns the product image to an anonymous crawler.
58
+ - `name` and `description` are the language of the page. Translate the schema with the page. Do not leave English schema on a Persian page unless the visible title is English.
59
+ - RTL does not change the JSON. Do not insert Persian digits into `price` or `sku`. Display digits in HTML. Keep ASCII in the structured data.
60
+ - Do not mark up products that are not on the page. A category page is not a single Product with one offer unless the page is actually one product.
61
+
62
+ ## Canonical and language
63
+
64
+ - The page has one canonical URL, and the offer `url` matches the page the shopper landed on, including the selected variant when the variant has its own URL.
65
+ - When several languages exist, each language URL's canonical and `hreflang` annotations have to agree. Structured data does not replace `hreflang`.
66
+
67
+ ## Gotchas
68
+
69
+ - A formatted string such as `"19,00 USD"` or `"۱۹.۰۰"` is not a price. Use a JSON number.
70
+ - `availability: InStock` on a parent while every variant is sold out contradicts the page.
71
+ - Copying a review `ratingValue` you do not display, or marking up reviews the page does not show, is not a fix for a missing offer.
72
+ - Sitewide `Organization` or return-policy markup is separate from the product offer. Add it once, on the pages where it is visible, rather than inventing a return policy inside every offer.
73
+
74
+ ## Verification
75
+
76
+ - The checker exits 0 for the profile you ship.
77
+ - View source shows one Product for the item, inside server-rendered HTML.
78
+ - The price, currency, and availability match the variant on the page, including an out-of-stock variant.
79
+ - A Persian product page keeps Persian text in `name` and ASCII in `price` and `priceCurrency`.
80
+ - Disabling JavaScript still leaves the JSON-LD in the document.
81
+
82
+ ## Evaluation
83
+
84
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,15 @@
1
+ {
2
+ "@context": "https://schema.org",
3
+ "@type": "Product",
4
+ "name": "Cotton shirt",
5
+ "image": "https://shop.example/shirt.jpg",
6
+ "description": "Short-sleeve cotton shirt.",
7
+ "sku": "SHIRT-BLUE",
8
+ "offers": {
9
+ "@type": "Offer",
10
+ "url": "https://shop.example/shirt?color=blue",
11
+ "priceCurrency": "USD",
12
+ "price": 19.0,
13
+ "availability": "https://schema.org/InStock"
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ Filter the existing markup.
2
+
3
+ ```php
4
+ add_filter( 'woocommerce_structured_data_product', function ( $markup, $product ) {
5
+ $markup['brand'] = array( '@type' => 'Brand', 'name' => 'Example' );
6
+ return $markup;
7
+ }, 10, 2 );
8
+ ```
@@ -0,0 +1,7 @@
1
+ Use the variant offer.
2
+
3
+ ```json
4
+ { "price": 19.0, "priceCurrency": "USD", "availability": "https://schema.org/InStock" }
5
+ ```
6
+
7
+ The JSON-LD price matches the blue shirt on the page.
@@ -0,0 +1,29 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "extend-woocommerce-schema",
5
+ "task": "Add a brand to the product structured data WooCommerce already prints, without a second Product node.",
6
+ "expects": {
7
+ "must_include": [
8
+ "woocommerce_structured_data_product"
9
+ ],
10
+ "must_not_include": [
11
+ "second Product"
12
+ ]
13
+ }
14
+ },
15
+ {
16
+ "id": "variant-offer",
17
+ "task": "Mark up the blue variant that the page sells, including a numeric price and in-stock availability.",
18
+ "expects": {
19
+ "must_include": [
20
+ "priceCurrency",
21
+ "InStock"
22
+ ],
23
+ "must_not_include": [
24
+ "19,00 USD"
25
+ ]
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,83 @@
1
+ # Product JSON-LD
2
+
3
+ ## One node on a WooCommerce product page
4
+
5
+ WooCommerce builds product structured data in `WC_Structured_Data` and prints it with the single product template. Filter the array before it is encoded:
6
+
7
+ ```php
8
+ add_filter(
9
+ 'woocommerce_structured_data_product',
10
+ function ( $markup, $product ) {
11
+ if ( ! $product instanceof WC_Product ) {
12
+ return $markup;
13
+ }
14
+ $markup['brand'] = array(
15
+ '@type' => 'Brand',
16
+ 'name' => 'Example',
17
+ );
18
+ return $markup;
19
+ },
20
+ 10,
21
+ 2
22
+ );
23
+ ```
24
+
25
+ Only add fields you can see on the page. If a variable product page shows a range, do not force a single variant price into the parent markup.
26
+
27
+ For a custom storefront, emit one `<script type="application/ld+json">` block from the server. Do not also emit WooCommerce's block if the theme already loaded it.
28
+
29
+ ## Merchant listing shape
30
+
31
+ ```json
32
+ {
33
+ "@context": "https://schema.org",
34
+ "@type": "Product",
35
+ "name": "Cotton shirt",
36
+ "image": "https://shop.example/shirt.jpg",
37
+ "sku": "SHIRT-BLUE",
38
+ "description": "Short-sleeve cotton shirt.",
39
+ "offers": {
40
+ "@type": "Offer",
41
+ "url": "https://shop.example/shirt?color=blue",
42
+ "priceCurrency": "USD",
43
+ "price": 19.0,
44
+ "availability": "https://schema.org/InStock"
45
+ }
46
+ }
47
+ ```
48
+
49
+ `price` is a JSON number. `priceCurrency` is ISO 4217. Merchant listing documentation requires a positive price. Product snippets can use `AggregateOffer` with `lowPrice`, `highPrice`, `priceCurrency`, and `offerCount` when the page shows a range.
50
+
51
+ ## Availability values
52
+
53
+ Use a single value:
54
+
55
+ - `https://schema.org/InStock`
56
+ - `https://schema.org/OutOfStock`
57
+ - `https://schema.org/PreOrder`
58
+ - `https://schema.org/BackOrder`
59
+ - `https://schema.org/LimitedAvailability`
60
+ - `https://schema.org/SoldOut`
61
+ - `https://schema.org/Discontinued`
62
+ - `https://schema.org/InStoreOnly`
63
+ - `https://schema.org/OnlineOnly`
64
+ - `https://schema.org/PreSale`
65
+
66
+ The short name, such as `InStock`, is also accepted by the checker. Map WooCommerce `instock` to `InStock`, `outofstock` to `OutOfStock`, and `onbackorder` to `BackOrder`.
67
+
68
+ ## Checker
69
+
70
+ `checkProductJsonLd(data, profile)` in `scripts/check-product-jsonld.mjs` reads a Product object or a `@graph` that contains one. Profiles:
71
+
72
+ - `merchant` requires `name`, `image`, and one positive `Offer`.
73
+ - `snippet` requires `name` and at least one of `review`, `aggregateRating`, or `offers`.
74
+
75
+ Warnings do not fail the process. Errors do. A warning on a missing `availability` still means you should set it when the page shows stock.
76
+
77
+ ## Breadcrumbs
78
+
79
+ WooCommerce can also emit `BreadcrumbList`. Add a breadcrumb node only when the visible breadcrumb is absent from existing structured data. Do not invent crumbs that are not on the page.
80
+
81
+ ## What this checker does not do
82
+
83
+ It does not fetch the image URL, does not call Google's rich results test, and does not validate return-policy or shipping-policy shapes. Those policies have their own required fields in Google's documentation. Add them when the shop displays that policy, and re-read the current field list before shipping them.
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ const AVAILABILITY = new Set([
5
+ "BackOrder",
6
+ "Discontinued",
7
+ "InStock",
8
+ "InStoreOnly",
9
+ "LimitedAvailability",
10
+ "OnlineOnly",
11
+ "OutOfStock",
12
+ "PreOrder",
13
+ "PreSale",
14
+ "SoldOut",
15
+ ]);
16
+
17
+ export function checkProductJsonLd(data, profile = "merchant") {
18
+ const errors = [];
19
+ const warnings = [];
20
+ const products = collectProducts(data);
21
+ if (products.length === 0) {
22
+ errors.push("No schema.org Product node found.");
23
+ return { errors, warnings };
24
+ }
25
+ products.forEach((product, index) => checkProduct(product, profile, `Product[${index}]`, errors, warnings));
26
+ return { errors, warnings };
27
+ }
28
+
29
+ function collectProducts(data) {
30
+ if (Array.isArray(data)) {
31
+ return data.flatMap((item) => collectProducts(item));
32
+ }
33
+ if (!data || typeof data !== "object") {
34
+ return [];
35
+ }
36
+ const found = [];
37
+ if (hasType(data, "Product")) {
38
+ found.push(data);
39
+ }
40
+ if (Array.isArray(data["@graph"])) {
41
+ found.push(...data["@graph"].flatMap((item) => collectProducts(item)));
42
+ }
43
+ return found;
44
+ }
45
+
46
+ function checkProduct(product, profile, label, errors, warnings) {
47
+ if (!nonEmptyString(product.name)) {
48
+ errors.push(`${label}: name is required`);
49
+ }
50
+ const hasReview = product.review !== undefined;
51
+ const hasRating = product.aggregateRating !== undefined;
52
+ const hasOffers = product.offers !== undefined;
53
+
54
+ if (product.aggregateRating) {
55
+ const rating = product.aggregateRating;
56
+ if (typeof rating.ratingValue !== "number") {
57
+ errors.push(`${label}: aggregateRating.ratingValue must be a number`);
58
+ }
59
+ if (typeof rating.reviewCount !== "number" && typeof rating.ratingCount !== "number") {
60
+ errors.push(`${label}: aggregateRating needs reviewCount or ratingCount`);
61
+ }
62
+ }
63
+
64
+ if (profile === "merchant") {
65
+ if (!hasImage(product.image)) {
66
+ errors.push(`${label}: merchant listings require image`);
67
+ }
68
+ if (!hasOffers) {
69
+ errors.push(`${label}: merchant listings require offers`);
70
+ } else {
71
+ checkMerchantOffers(product.offers, `${label}.offers`, errors, warnings);
72
+ }
73
+ } else if (!hasReview && !hasRating && !hasOffers) {
74
+ errors.push(`${label}: product snippets require review, aggregateRating, or offers`);
75
+ } else if (hasOffers) {
76
+ checkSnippetOffers(product.offers, `${label}.offers`, errors, warnings);
77
+ }
78
+
79
+ if (profile === "snippet" && !hasImage(product.image)) {
80
+ warnings.push(`${label}: image is recommended`);
81
+ }
82
+ }
83
+
84
+ function checkMerchantOffers(offers, label, errors, warnings) {
85
+ if (Array.isArray(offers)) {
86
+ errors.push(`${label}: merchant listings need one Offer, not an array`);
87
+ return;
88
+ }
89
+ if (hasType(offers, "AggregateOffer")) {
90
+ errors.push(`${label}: merchant listings need Offer, not AggregateOffer`);
91
+ return;
92
+ }
93
+ if (!hasType(offers, "Offer")) {
94
+ errors.push(`${label}: offers @type must be Offer`);
95
+ return;
96
+ }
97
+ checkActivePrice(offers, label, errors, { requirePositive: true });
98
+ checkAvailability(offers, label, warnings);
99
+ }
100
+
101
+ function checkSnippetOffers(offers, label, errors, warnings) {
102
+ const list = Array.isArray(offers) ? offers : [offers];
103
+ for (const [index, offer] of list.entries()) {
104
+ const itemLabel = Array.isArray(offers) ? `${label}[${index}]` : label;
105
+ if (hasType(offer, "AggregateOffer")) {
106
+ if (typeof offer.lowPrice !== "number" || typeof offer.highPrice !== "number") {
107
+ errors.push(`${itemLabel}: AggregateOffer needs numeric lowPrice and highPrice`);
108
+ }
109
+ if (!isoCurrency(offer.priceCurrency)) {
110
+ errors.push(`${itemLabel}: AggregateOffer.priceCurrency must be a 3-letter ISO 4217 code`);
111
+ }
112
+ if (typeof offer.offerCount !== "number" || offer.offerCount < 1) {
113
+ errors.push(`${itemLabel}: AggregateOffer.offerCount must be a positive number`);
114
+ }
115
+ continue;
116
+ }
117
+ if (!hasType(offer, "Offer")) {
118
+ errors.push(`${itemLabel}: offers @type must be Offer or AggregateOffer`);
119
+ continue;
120
+ }
121
+ checkActivePrice(offer, itemLabel, errors, { requirePositive: false });
122
+ checkAvailability(offer, itemLabel, warnings);
123
+ }
124
+ }
125
+
126
+ function checkActivePrice(offer, label, errors, { requirePositive }) {
127
+ const direct = offer.price;
128
+ const specified = offer.priceSpecification?.price;
129
+ const currency = offer.price !== undefined ? offer.priceCurrency : offer.priceSpecification?.priceCurrency;
130
+ const price = direct !== undefined ? direct : specified;
131
+ if (typeof price !== "number" || Number.isNaN(price)) {
132
+ errors.push(`${label}: price must be a JSON number`);
133
+ } else if (requirePositive && price <= 0) {
134
+ errors.push(`${label}: merchant listing price must be greater than zero`);
135
+ } else if (direct !== undefined && specified !== undefined && direct <= 0 && specified > 0) {
136
+ errors.push(`${label}: offers.price overrides priceSpecification and is not a positive price`);
137
+ }
138
+ if (!isoCurrency(currency)) {
139
+ errors.push(`${label}: priceCurrency must be a 3-letter ISO 4217 code`);
140
+ }
141
+ }
142
+
143
+ function checkAvailability(offer, label, warnings) {
144
+ if (offer.availability === undefined) {
145
+ warnings.push(`${label}: availability is recommended`);
146
+ return;
147
+ }
148
+ const shortName = String(offer.availability).replace("https://schema.org/", "");
149
+ if (!AVAILABILITY.has(shortName)) {
150
+ warnings.push(`${label}: unrecognized availability value ${offer.availability}`);
151
+ }
152
+ }
153
+
154
+ function hasImage(image) {
155
+ if (typeof image === "string" && image.trim() !== "") {
156
+ return true;
157
+ }
158
+ if (Array.isArray(image)) {
159
+ return image.length > 0 && image.every((item) => hasImage(item));
160
+ }
161
+ if (image && typeof image === "object" && typeof image.url === "string") {
162
+ return image.url.trim() !== "";
163
+ }
164
+ return false;
165
+ }
166
+
167
+ function hasType(node, type) {
168
+ if (!node || typeof node !== "object") {
169
+ return false;
170
+ }
171
+ const value = node["@type"];
172
+ return value === type || (Array.isArray(value) && value.includes(type));
173
+ }
174
+
175
+ function nonEmptyString(value) {
176
+ return typeof value === "string" && value.trim() !== "";
177
+ }
178
+
179
+ function isoCurrency(value) {
180
+ return typeof value === "string" && /^[A-Z]{3}$/.test(value);
181
+ }
182
+
183
+ function readOption(name) {
184
+ const index = process.argv.indexOf(name);
185
+ if (index === -1 || !process.argv[index + 1]) {
186
+ return undefined;
187
+ }
188
+ return process.argv[index + 1];
189
+ }
190
+
191
+ const isCli = process.argv[1] && process.argv[1].endsWith("check-product-jsonld.mjs");
192
+ if (isCli) {
193
+ const file = readOption("--file");
194
+ const profile = readOption("--profile") ?? "merchant";
195
+ if (!file || (profile !== "merchant" && profile !== "snippet")) {
196
+ console.error("Usage: node check-product-jsonld.mjs --file product.json [--profile merchant|snippet]");
197
+ process.exit(2);
198
+ }
199
+ const data = JSON.parse(await readFile(file, "utf8"));
200
+ const result = checkProductJsonLd(data, profile);
201
+ for (const warning of result.warnings) {
202
+ console.error(`WARN ${warning}`);
203
+ }
204
+ if (result.errors.length > 0) {
205
+ for (const error of result.errors) {
206
+ console.error(`ERROR ${error}`);
207
+ }
208
+ process.exit(1);
209
+ }
210
+ console.log(`JSON-LD passed the ${profile} profile.`);
211
+ }
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: fraud-and-risk-basics
3
+ description: "Hold or review risky payments using provider signals, velocity, and 3-D Secure without storing card numbers. Use when adding payment risk checks, a manual review queue, or decline handling for card abuse."
4
+ license: MIT
5
+ metadata:
6
+ author: Naser Sadrmanesh
7
+ version: "0.3.0"
8
+ ---
9
+
10
+ # Fraud and risk basics
11
+
12
+ Use this skill to decide whether a confirmed payment attempt should capture, wait for review, or be declined. The payment skill still verifies the provider. This skill does not collect card numbers and it does not describe how to test stolen cards.
13
+
14
+ ## Procedure
15
+
16
+ 1. Let the payment provider collect the card. You store the provider payment id, AVS result, CVV result, and 3-D Secure result. You do not store PAN or CVV.
17
+ 2. Start from the provider's risk recommendation. Do not replace it with a score you computed from a single field.
18
+ 3. Add only signals you can explain: velocity, address mismatch, and a mismatch between the paid amount and the order.
19
+ 4. Map the outcome to one action: capture, authorize and review, or do not capture.
20
+ 5. A review queue shows the order id, amount, currency, and the signals. A person captures or voids. The queue does not auto-capture because it sat overnight.
21
+
22
+ Worked examples and checks are in [evals/tasks.json](evals/tasks.json).
23
+
24
+ ## Actions
25
+
26
+ | Situation | Action |
27
+ | --- | --- |
28
+ | Provider says the payment is acceptable and no local rule fired | Capture, or accept the provider's capture, through the payment skill. |
29
+ | Provider asks for step-up authentication | Send the shopper through 3-D Secure. Do not capture before it finishes. |
30
+ | AVS or CVV is a mismatch and the amount is above the store's review threshold | Authorize if the provider allows it, and queue for review. Do not capture yet. |
31
+ | Velocity rule fires | Same review path. Do not silently decline every later order from that customer without a reason you store. |
32
+ | Amount, currency, or order id does not match | Do not capture. Follow the payment skill's mismatch rule. |
33
+ | Provider declines | Leave the order unpaid. Keep the cart. Do not retry the charge in a loop. |
34
+
35
+ AVS and CVV are signals. A match is not proof the shopper is the cardholder. A mismatch is not proof of fraud. International addresses often fail AVS. Do not decline every AVS mismatch unless the store has chosen that policy.
36
+
37
+ ## Velocity
38
+
39
+ Count paid and attempted orders by the identifiers you are allowed to store: account id, shipping address hash, and provider customer id. A default worth configuring is more than three payment attempts from one account in ten minutes, or more than two shipping addresses used by one account in one hour. Tune the numbers. Do not present them as measured fraud rates.
40
+
41
+ Do not put the full card number into the velocity key.
42
+
43
+ ## Review queue
44
+
45
+ - Store order id, amount, currency, provider payment id, the signals that fired, and the decision.
46
+ - Capture and void go through the payment provider, idempotently.
47
+ - The reviewer's user id is stored with the decision.
48
+ - A shopper-facing page says the payment is being reviewed. It does not say the order shipped.
49
+
50
+ ## Gotchas
51
+
52
+ - Capturing before 3-D Secure returns loses the liability shift the provider offered. Wait for the result.
53
+ - A client-supplied `risk: low` field is not a signal. Ignore it.
54
+ - Logging the full webhook body can store a PAN the provider included by mistake. Log ids and result codes.
55
+ - Declining the renewal of every international card will cancel good subscriptions. Use the review path for subscriptions instead of an immediate cancel, unless the store has a written policy.
56
+
57
+ ## Verification
58
+
59
+ - A mismatched amount is not captured.
60
+ - A 3-D Secure challenge does not capture before the provider reports success.
61
+ - The database and logs for a test payment contain no PAN and no CVV.
62
+ - Two clicks in the review queue do not capture twice.
63
+
64
+ ## Evaluation
65
+
66
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,3 @@
1
+ When AVS mismatches and the amount is above the store threshold, authorize if the provider allows it and add a review row.
2
+
3
+ The row stores the order id, amount, and AVS signal. A browser field named as a risk flag is ignored.
@@ -0,0 +1,3 @@
1
+ Store the provider payment id and the 3-D Secure state.
2
+
3
+ If authentication is pending, do not capture. Capture only after the provider reports success. Never store the PAN.