@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,69 @@
1
+ #!/usr/bin/env node
2
+ // Check MCP tool definitions for store safety rules.
3
+ // Usage: node check-tool-definitions.mjs <tools.json>
4
+ import { readFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const NAME = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
9
+ const WRITE_VERB = /^(create|update|delete|refund|cancel|set|add|remove|bulk|send|apply|adjust)_/;
10
+ const RISKY_VERB = /^(delete|refund|cancel|bulk|remove)_/;
11
+
12
+ /** Returns a list of problems with a list of tool definitions. */
13
+ export function checkToolDefinitions(tools) {
14
+ if (!Array.isArray(tools)) return ["tools must be an array"];
15
+ const errors = [];
16
+ const names = new Set();
17
+ for (const [index, tool] of tools.entries()) {
18
+ const label = typeof tool?.name === "string" ? tool.name : `tool ${index}`;
19
+ if (!tool || typeof tool !== "object") {
20
+ errors.push(`${label}: must be an object`);
21
+ continue;
22
+ }
23
+ if (typeof tool.name !== "string" || !NAME.test(tool.name)) errors.push(`${label}: name must be lowercase snake_case`);
24
+ else if (names.has(tool.name)) errors.push(`${label}: duplicate name`);
25
+ else names.add(tool.name);
26
+ if (typeof tool.description !== "string" || tool.description.trim().length < 20) errors.push(`${label}: description must be at least 20 characters`);
27
+ if (tool.inputSchema?.type !== "object") errors.push(`${label}: inputSchema.type must be "object"`);
28
+ const annotations = tool.annotations;
29
+ if (!annotations || typeof annotations.readOnlyHint !== "boolean") {
30
+ errors.push(`${label}: annotations.readOnlyHint must be set`);
31
+ continue;
32
+ }
33
+ const writes = WRITE_VERB.test(tool.name ?? "");
34
+ if (writes && annotations.readOnlyHint) errors.push(`${label}: looks like a write but readOnlyHint is true`);
35
+ if (!annotations.readOnlyHint && typeof annotations.destructiveHint !== "boolean") {
36
+ errors.push(`${label}: write tools must set destructiveHint`);
37
+ }
38
+ if (!annotations.readOnlyHint && typeof annotations.idempotentHint !== "boolean") {
39
+ errors.push(`${label}: write tools must set idempotentHint`);
40
+ }
41
+ if (RISKY_VERB.test(tool.name ?? "")) {
42
+ const properties = tool.inputSchema?.properties ?? {};
43
+ if (!properties.dryRun) errors.push(`${label}: risky writes need a dryRun input`);
44
+ if (!properties.confirm) errors.push(`${label}: risky writes need a confirm input`);
45
+ }
46
+ const list = /^list_/.test(tool.name ?? "");
47
+ if (list) {
48
+ const properties = tool.inputSchema?.properties ?? {};
49
+ const perPage = properties.perPage ?? properties.limit;
50
+ if (!perPage || typeof perPage.maximum !== "number") errors.push(`${label}: list tools need a perPage or limit with a maximum`);
51
+ }
52
+ }
53
+ return errors;
54
+ }
55
+
56
+ const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
57
+ if (isCli) {
58
+ const [file] = process.argv.slice(2);
59
+ if (!file) {
60
+ console.error("Usage: node check-tool-definitions.mjs <tools.json>");
61
+ process.exit(2);
62
+ }
63
+ const errors = checkToolDefinitions(JSON.parse(await readFile(file, "utf8")));
64
+ if (errors.length > 0) {
65
+ for (const error of errors) console.error(`- ${error}`);
66
+ process.exit(1);
67
+ }
68
+ console.log("Tool definitions look safe.");
69
+ }
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: multi-vendor-payouts
3
+ description: "Split a marketplace order by vendor, record commission as ledger entries, and pay vendors only settled amounts. Use when adding vendor catalogs, commissions, refunds that cross vendors, or payout batches."
4
+ license: MIT
5
+ metadata:
6
+ author: Naser Sadrmanesh
7
+ version: "0.3.0"
8
+ ---
9
+
10
+ # Multi-vendor payouts
11
+
12
+ Use this skill when more than one vendor sells on one order. Catalog shape is the product-catalog skill. Charging the shopper is the payment skill. This skill records who is owed what after that charge.
13
+
14
+ Do not name a vendor plugin's internal functions unless that plugin is in the project and you have read them. The ledger below is the model to implement, including when the store uses an existing multi-vendor extension.
15
+
16
+ ## Procedure
17
+
18
+ 1. Put `vendor_id` on every sellable line. A line without a vendor cannot be paid out.
19
+ 2. On payment, write ledger entries. Do not edit the line price to "take commission". Commission is its own entry.
20
+ 3. Decide, in data, who is merchant of record for the goods, the tax, and the shipping. Do not assume the vendor is.
21
+ 4. Hold entries until the refund window or the delivery rule you stored. A payout batch sums only entries that are payable.
22
+ 5. Send the payout with an idempotency key. A second run of the same batch does not pay twice.
23
+ 6. A refund writes reversing entries. It does not delete the original lines.
24
+
25
+ The entry types are in [the ledger notes](references/ledger.md). Worked examples and checks are in [evals/tasks.json](evals/tasks.json).
26
+
27
+ ## Ledger entry
28
+
29
+ Each entry has: `id`, `order_id`, `vendor_id` or `platform`, `type`, `amount` in integer minor units, `currency`, `available_at`, and `payout_id` once paid.
30
+
31
+ Amounts are signed. A sale credit to the vendor is positive. Commission the platform keeps is negative on the vendor and positive on the platform, or two explicit entries that sum to the same result. Pick one representation and use it everywhere.
32
+
33
+ `amount` uses the order currency. Do not convert to a payout currency inside the entry. Conversion, if it exists, is a later payout step with its own rate and timestamp.
34
+
35
+ ## When money becomes payable
36
+
37
+ - Authorization is not a sale. Write no payable entry until the payment skill marks the order paid.
38
+ - `available_at` is at least the refund window, or the delivery confirmation, whichever the marketplace rule uses.
39
+ - Pending entries can be displayed as upcoming. They are excluded from the payout sum.
40
+
41
+ ## Payout batch
42
+
43
+ - Select entries where `available_at` is in the past, `payout_id` is null, and the vendor account is allowed to receive funds.
44
+ - Group by vendor and currency.
45
+ - Persist the batch and stamp `payout_id` in the same transaction as the decision to pay.
46
+ - Call the payout provider with `payout_{batchId}`. If the call times out, retry with that key and do not create a second batch.
47
+ - A provider failure clears nothing silently. The batch stays `processing` or moves to `failed` with the entries still reserved, so a retry cannot select them into a new batch.
48
+
49
+ ## Refunds
50
+
51
+ - Refund the shopper through the payment skill for the amount they paid.
52
+ - Reverse the vendor net and the commission in proportion to the refunded lines, including tax if the vendor was credited for tax.
53
+ - If the payout already left, the reversal is a negative balance on the next batch, not a deleted history.
54
+
55
+ ## Gotchas
56
+
57
+ - One order can contain two vendors. A single commission percent on the order total assigns one vendor's shipping to the other.
58
+ - Paying the gross and also forgetting to retain commission overpays the vendor.
59
+ - Summing order totals again at payout time double-counts if the ledger is also summed.
60
+ - Blocking a vendor after entries are `processing` must not release those entries back into the selectable pool.
61
+
62
+ ## Verification
63
+
64
+ - A paid two-vendor order produces entries whose vendor nets plus platform commission equal the goods total you intended to split.
65
+ - An unpaid order produces no payable entries.
66
+ - Running the payout job twice creates one provider payout per vendor.
67
+ - A refund after payout reduces the next batch and leaves the original entries in place.
68
+
69
+ ## Evaluation
70
+
71
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,3 @@
1
+ Stamp `payout_id` on the selected entries in the same transaction as the batch.
2
+
3
+ Retry the provider with that batch idempotency key. Do not select those entries into a second batch.
@@ -0,0 +1,3 @@
1
+ Write ledger entries when the shopper payment is confirmed.
2
+
3
+ The vendor `goods` entry and the platform `commission` entry split the line total. The catalog line price stays what the shopper paid.
@@ -0,0 +1,30 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "split-paid-order",
5
+ "task": "Record vendor goods and platform commission for a paid marketplace order without editing the line price.",
6
+ "expects": {
7
+ "must_include": [
8
+ "ledger",
9
+ "commission"
10
+ ],
11
+ "must_not_include": [
12
+ "edit the line price"
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "retry-payout",
18
+ "task": "Run a vendor payout batch twice without sending the money twice.",
19
+ "expects": {
20
+ "must_include": [
21
+ "payout_id",
22
+ "idempotency"
23
+ ],
24
+ "must_not_include": [
25
+ "sum the order totals again"
26
+ ]
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,35 @@
1
+ # Marketplace ledger
2
+
3
+ ## Entry types
4
+
5
+ | Type | Who is credited | When it is written |
6
+ | --- | --- | --- |
7
+ | `goods` | Vendor | Order is paid, for that vendor's line totals after line discounts |
8
+ | `commission` | Platform | Same moment, as the platform's share of those lines |
9
+ | `shipping` | Vendor or platform | Whoever the marketplace rule says keeps the shipping the shopper paid |
10
+ | `tax` | Vendor or platform | Whoever is responsible for remitting that tax |
11
+ | `refund_goods` | Reverses `goods` | Refund is confirmed |
12
+ | `refund_commission` | Reverses `commission` | Refund is confirmed |
13
+ | `adjustment` | Either | A person records a correction with a reason |
14
+
15
+ `goods` plus `commission` for a line equals the line total the shopper paid for those goods, before the shipping and tax entries. If commission is stored as a negative vendor entry instead of a platform entry, the vendor net is `goods + commission` and the platform total is the absolute commission. Do not store it both ways.
16
+
17
+ ## Worked numbers
18
+
19
+ Shopper pays 100.00 for vendor goods. Commission is 10%. Vendor shipping is 0. The currency has two minor units.
20
+
21
+ - Vendor `goods`: `10000`
22
+ - Vendor `commission`: `-1000` if commission is a negative vendor entry
23
+ - Vendor payable after the hold: `9000`
24
+
25
+ Tax and shipping stay out of that 100.00 until their own entries exist. Do not subtract commission from the tax entry unless the contract says the commission base includes tax.
26
+
27
+ ## Payout record
28
+
29
+ `id`, `vendor_id`, `currency`, `amount` (sum of reserved entries), `status` (`processing`, `paid`, `failed`), `idempotency_key`, `provider_payout_id`.
30
+
31
+ `paid` requires the provider id. `failed` keeps `idempotency_key` so the retry is the same payout.
32
+
33
+ ## Merchant of record
34
+
35
+ Store `merchant_of_record` on the order or the line as `platform` or `vendor`. This is a business and legal choice the marketplace has already made. The ledger follows that flag. It does not decide it.
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: order-and-webhook-reliability
3
+ description: "Accept order and payment webhooks with a raw-body signature check, durable deduplication, and reconciliation against the source of truth. Use when handling order events, payment notifications, or outbound webhook delivery."
4
+ license: MIT
5
+ compatibility: "The signature checker requires Node.js 20 or newer. No network access."
6
+ metadata:
7
+ author: Naser Sadrmanesh
8
+ version: "0.3.0"
9
+ ---
10
+
11
+ # Order and webhook reliability
12
+
13
+ Use this skill for inbound payment or order notifications and for outbound webhooks your shop sends. Provider-specific state mapping stays in the payment skill. WooCommerce header names and the failure limit are in [the WooCommerce webhook notes](references/woocommerce-webhooks.md).
14
+
15
+ Verify a WooCommerce signature locally with [`scripts/verify-hmac-signature.mjs`](scripts/verify-hmac-signature.mjs).
16
+
17
+ ## Procedure
18
+
19
+ 1. Read the raw body before any JSON parser reorders or re-encodes it.
20
+ 2. Verify the signature with a constant-time compare. Reject unsigned or invalid requests before you look at the event type.
21
+ 3. Store the event in a durable inbox, unique on the provider event id when that id is stable across retries.
22
+ 4. Acknowledge with 2xx only after that store succeeds. Do the order update after the store, in the same transaction when you can, or in a worker that can retry.
23
+ 5. Apply the change only if this event is newer than what you stored, or if the fetched resource still matches the event.
24
+ 6. When the event is incomplete or out of order, fetch the order or payment from the source of truth and write that.
25
+
26
+ ## Inbox
27
+
28
+ A practical unique key:
29
+
30
+ - Provider event id, when the provider reuses it for retries of the same event.
31
+ - Otherwise a hash of topic, resource id, and the resource's `date_modified_gmt` or equivalent version.
32
+
33
+ Do not dedupe only on a delivery attempt id. Those change when the sender times out and posts again.
34
+
35
+ Store: event id, topic, resource id, raw body, signature verification time, and processing status (`stored`, `applied`, `ignored`). Ignore a duplicate by returning 2xx and the original result. A second apply is a bug.
36
+
37
+ ## Apply rules
38
+
39
+ - Compare amounts in integer minor units before marking an order paid.
40
+ - Do not move an order backward because a late `pending` event arrived after `paid`, unless the source fetch shows the payment was reversed.
41
+ - Record the provider payment id on the first paid transition. The paid transition itself is idempotent.
42
+ - Outbound side effects (email, stock, fulfillment) run inside that same idempotent transition, not in the HTTP handler after a commit you might repeat.
43
+
44
+ ## Outbound webhooks
45
+
46
+ When your shop sends webhooks:
47
+
48
+ - Sign the exact bytes you send.
49
+ - Include your event id and the resource version.
50
+ - Retry with backoff on connection failures and 5xx. Stop on a 4xx that means the subscription is invalid.
51
+ - Use an outbox row written in the same transaction as the order change. A webhook built only in memory is lost when the process dies after the order commit.
52
+ - Receivers will see duplicates and gaps. Document the event id so they can dedupe.
53
+
54
+ ## Failures
55
+
56
+ - Invalid signature: 401 or 403, and do not store the body as a trusted event. You may log the rejection id.
57
+ - Unknown resource: 200 after you record the event as ignored, or 404 if the provider treats 404 as "do not retry" and you are sure the resource will never exist. Prefer recording it and fetching. A retry storm from a 500 is worse than an ignored row you can inspect.
58
+ - Database down: 503 so the sender retries. The unique key makes the retry safe.
59
+ - Handler bug after a durable store: leave the row unapplied and retry the worker. Do not ask the sender to redeliver once you have the bytes, unless you failed to store them.
60
+
61
+ ## WooCommerce receivers
62
+
63
+ WooCommerce signs `base64(hmac_sha256(secret, raw_body))`. The secret is passed through `wp_specialchars_decode( $secret, ENT_QUOTES )` before the MAC. `decodeWebhookSecret()` in the checker matches that decode.
64
+
65
+ ```bash
66
+ WEBHOOK_SECRET="..." node scripts/verify-hmac-signature.mjs \
67
+ --body payload.json \
68
+ --signature "$SIGNATURE"
69
+ ```
70
+
71
+ You can pass `--secret-file` instead of the environment variable. The checker does not print the secret.
72
+
73
+ `X-WC-Webhook-Delivery-ID` is a new log id per attempt. Dedupe on the resource id plus `date_modified_gmt` and the topic, then fetch `/wp-json/wc/v3/orders/<id>` if the payload is not enough.
74
+
75
+ The activation ping is not an order event. Return 2xx and do not create an order.
76
+
77
+ WooCommerce counts a delivery as failed when the status is outside `2xx`, `301`, and `302`. After more than five consecutive failures it disables the webhook. The threshold filter is `woocommerce_max_webhook_delivery_failures` (default 5). `WC_Webhook::deliver()` posts with redirects disabled, so a 301 does not mean the JSON was delivered to the next URL. Answer on the registered URL.
78
+
79
+ There is no separate backoff retry of one payload described here. Timeouts and repeated order saves still deliver more than once. Handle duplicates anyway.
80
+
81
+ ## Gotchas
82
+
83
+ - Parsing JSON and signing `JSON.stringify(object)` will not match. The sender signed the original bytes, including key order and spacing.
84
+ - Returning 200 before the inbox write loses the event if the process dies.
85
+ - Applying events in arrival order without a version can reopen a canceled order.
86
+ - Logging the raw body of a payment webhook can capture data you did not mean to keep. Store the body in the inbox with restricted access, and keep card data out of application logs.
87
+ - A shared webhook secret across shops makes a signature from shop A valid on shop B. Use one secret per subscription.
88
+
89
+ ## Verification
90
+
91
+ - The checker accepts a known body and secret and rejects a one-byte change.
92
+ - A duplicated delivery creates one inbox row and one state change.
93
+ - An older event received after a newer one does not downgrade a paid order.
94
+ - A wrong amount does not mark the order paid.
95
+ - Stopping the database makes the handler return a retryable status and, after recovery, the redelivered event still applies once.
96
+
97
+ ## Evaluation
98
+
99
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,3 @@
1
+ Store the topic, the order id, and `date_modified_gmt` as the inbox key.
2
+
3
+ A repeat with the same version is acknowledged and not applied again. Fetch the order when the payload is older than the row you already stored.
@@ -0,0 +1,3 @@
1
+ Sign the comparison with the original raw body.
2
+
3
+ Compute Base64 HMAC-SHA256 over those bytes with the webhook secret. Parse JSON only after the signature matches, and compare with a constant-time check.
@@ -0,0 +1,29 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "verify-raw-body",
5
+ "task": "Verify a WooCommerce webhook signature against the exact posted bytes before parsing the JSON.",
6
+ "expects": {
7
+ "must_include": [
8
+ "raw body",
9
+ "HMAC-SHA256"
10
+ ],
11
+ "must_not_include": [
12
+ "JSON.stringify"
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "dedupe-resource-version",
18
+ "task": "Dedupe an order.updated webhook when WooCommerce delivers it more than once.",
19
+ "expects": {
20
+ "must_include": [
21
+ "date_modified_gmt"
22
+ ],
23
+ "must_not_include": [
24
+ "X-WC-Webhook-Delivery-ID"
25
+ ]
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,58 @@
1
+ # WooCommerce webhook delivery
2
+
3
+ Source of this note: WooCommerce webhook documentation and `WC_Webhook::deliver()`. Confirm header names against the shop's WooCommerce version when you debug a live miss.
4
+
5
+ ## Request
6
+
7
+ - Method: `POST`
8
+ - Content type: `application/json`
9
+ - Body: the REST representation of the resource, or a ping payload when the webhook is first activated
10
+ - User-Agent contains `WooCommerce` and `Hookshot`. Do not authenticate with the User-Agent.
11
+
12
+ Headers:
13
+
14
+ | Header | Role |
15
+ | --- | --- |
16
+ | `X-WC-Webhook-Source` | Store home URL. |
17
+ | `X-WC-Webhook-Topic` | For example `order.updated`. |
18
+ | `X-WC-Webhook-Resource` | For example `order`. |
19
+ | `X-WC-Webhook-Event` | For example `updated`. |
20
+ | `X-WC-Webhook-Signature` | Base64 HMAC-SHA256 of the raw body. |
21
+ | `X-WC-Webhook-ID` | Webhook id. |
22
+ | `X-WC-Webhook-Delivery-ID` | This attempt's log id. Not stable across attempts. |
23
+
24
+ The signature is computed after JSON encoding. The hash algorithm filter is `woocommerce_webhook_hash_algorithm` and defaults to `sha256`. If a shop changes that filter, a receiver that always uses SHA-256 will reject valid deliveries. Prefer leaving the filter at the default.
25
+
26
+ The secret is the webhook's configured secret. If the secret was left empty, WooCommerce uses the current API user's consumer secret. Set an explicit secret when you register the webhook.
27
+
28
+ ## Signature check
29
+
30
+ ```js
31
+ import { verifyWooCommerceSignature } from "../scripts/verify-hmac-signature.mjs";
32
+
33
+ const result = verifyWooCommerceSignature({
34
+ secret,
35
+ rawBody,
36
+ signature: headerValue,
37
+ });
38
+ ```
39
+
40
+ `rawBody` is a Buffer or string of the exact POST body. `decodeWebhookSecret()` decodes `&quot;`, `&#039;`, `&#39;`, `&lt;`, `&gt;`, and `&amp;` to match `wp_specialchars_decode( $secret, ENT_QUOTES )`.
41
+
42
+ ## Success and automatic disable
43
+
44
+ WooCommerce treats `2xx`, `301`, and `302` as success. Other statuses add to the consecutive failure count. The webhook is disabled after more than five consecutive failures. Filter: `woocommerce_max_webhook_delivery_failures`, default `5`.
45
+
46
+ Disabled webhooks stay disabled until someone sets them back to active. A receiver outage that returns 500 long enough will stop delivery until the merchant notices. Return 2xx once the event is durable, and do slow work afterward.
47
+
48
+ `deliver()` sets HTTP redirection to 0. Do not put a redirect on the delivery URL and expect the body to be posted to the target.
49
+
50
+ ## Ping
51
+
52
+ Saving an active webhook sends a ping. Treat a body that is only a webhook probe as a health check. Acknowledge it. Do not require an order id.
53
+
54
+ ## Topics
55
+
56
+ Resource topics include `order.created`, `order.updated`, `order.deleted`, `product.created`, `product.updated`, `product.deleted`, `customer.created`, `customer.updated`, and the coupon equivalents. `order.updated` fires for many status and meta changes. Your handler has to tolerate repeats with a newer `date_modified_gmt`.
57
+
58
+ Deleted resources may arrive after your local copy is already gone. Record the delete and return 2xx.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+
5
+ /**
6
+ * WooCommerce signs the raw JSON body with HMAC-SHA256 and Base64.
7
+ * The secret is passed through wp_specialchars_decode( $secret, ENT_QUOTES )
8
+ * before hashing. decodeWebhookSecret matches that decode.
9
+ */
10
+ export function decodeWebhookSecret(secret) {
11
+ return secret
12
+ .replaceAll("&quot;", '"')
13
+ .replaceAll("&#039;", "'")
14
+ .replaceAll("&#39;", "'")
15
+ .replaceAll("&lt;", "<")
16
+ .replaceAll("&gt;", ">")
17
+ .replaceAll("&amp;", "&");
18
+ }
19
+
20
+ export function woocommerceSignature(secret, rawBody) {
21
+ const key = decodeWebhookSecret(secret);
22
+ return createHmac("sha256", key).update(rawBody).digest("base64");
23
+ }
24
+
25
+ export function signaturesMatch(expected, received) {
26
+ if (typeof expected !== "string" || typeof received !== "string") {
27
+ return false;
28
+ }
29
+ const left = Buffer.from(expected);
30
+ const right = Buffer.from(received);
31
+ if (left.length === 0 || left.length !== right.length) {
32
+ return false;
33
+ }
34
+ return timingSafeEqual(left, right);
35
+ }
36
+
37
+ export function verifyWooCommerceSignature({ secret, rawBody, signature }) {
38
+ const expected = woocommerceSignature(secret, rawBody);
39
+ return {
40
+ ok: signaturesMatch(expected, signature),
41
+ expected,
42
+ };
43
+ }
44
+
45
+ function readOption(name) {
46
+ const index = process.argv.indexOf(name);
47
+ if (index === -1 || !process.argv[index + 1]) {
48
+ return undefined;
49
+ }
50
+ return process.argv[index + 1];
51
+ }
52
+
53
+ const isCli = process.argv[1] && process.argv[1].endsWith("verify-hmac-signature.mjs");
54
+ if (isCli) {
55
+ const bodyPath = readOption("--body");
56
+ const signature = readOption("--signature");
57
+ const secretFile = readOption("--secret-file");
58
+ const secret = secretFile ? (await readFile(secretFile, "utf8")).replace(/\r?\n$/, "") : process.env.WEBHOOK_SECRET;
59
+
60
+ if (!bodyPath || !signature || !secret) {
61
+ console.error(
62
+ "Usage: node verify-hmac-signature.mjs --body payload.json --signature <base64> (--secret-file secret.txt | WEBHOOK_SECRET=...)",
63
+ );
64
+ process.exit(2);
65
+ }
66
+
67
+ const rawBody = await readFile(bodyPath);
68
+ const result = verifyWooCommerceSignature({
69
+ secret,
70
+ rawBody,
71
+ signature,
72
+ });
73
+ if (!result.ok) {
74
+ console.error("Signature mismatch.");
75
+ process.exit(1);
76
+ }
77
+ console.log("Signature matches.");
78
+ }
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: payment-gateway-integration
3
+ description: "Integrate a payment provider with idempotent charges, signature-checked webhooks, and an explicit map from provider states to order states. Use when adding or changing checkout payment, capture, refund, or payment notification handling."
4
+ license: MIT
5
+ metadata:
6
+ author: Naser Sadrmanesh
7
+ version: "0.3.0"
8
+ ---
9
+
10
+ # Payment gateway integration
11
+
12
+ Apply this skill to any provider: card acquirer, wallet, bank redirect, or buy-now-pay-later. The WooCommerce class layout is in [the WooCommerce gateway notes](references/woocommerce-gateway.md). Read it when the store runs WooCommerce. The webhook inbox, signature check, and retries are in the order and webhook skill.
13
+
14
+ ## Procedure
15
+
16
+ 1. Keep card entry on the provider. Use hosted fields, a redirect, or a native wallet. Do not post the PAN or CVV to the merchant server and do not store them. Taking card data on the merchant server changes the store's PCI scope.
17
+ 2. Create the shop order before redirecting or confirming, with a status that means unpaid (`pending` or the platform's equivalent).
18
+ 3. Send the charge with an idempotency key tied to that order and attempt. Reuse the key if the HTTP call times out. Generate a new key only when the shopper starts a new attempt.
19
+ 4. Treat the browser return URL as a hint. Mark the order paid only from a signed server notification or from a server-side retrieve of the provider payment.
20
+ 5. Compare amount and currency in minor units on the server. Reject a notification whose amount or currency does not match the order.
21
+ 6. Record the provider payment id once. A second notification for the same payment id must not capture, email, or decrement stock again.
22
+ 7. Map provider states through one table. Do not sprinkle status writes through the client and the webhook.
23
+
24
+ ## State map
25
+
26
+ Use one function from provider event to shop transition.
27
+
28
+ | Provider observation | Shop effect |
29
+ | --- | --- |
30
+ | Authorized, not captured | Leave the order unpaid or set the platform's on-hold equivalent. Store the authorization id. |
31
+ | Captured or paid | Mark paid through the platform's payment-complete path. Store the capture id. |
32
+ | Pending asynchronous payment | On hold. Do not empty the cart until the policy says the order is accepted. |
33
+ | Declined or failed | Failed. Keep the cart so the shopper can retry. |
34
+ | Canceled by the shopper | Cancel only if the order is still unpaid. |
35
+ | Refunded, full or partial | Create a refund record for the amount. Do not only change the status string. |
36
+ | Chargeback or dispute opened | Record a dispute flag. Do not automatically refund unless the shop's policy says so. |
37
+
38
+ Unknown states are logged and left unchanged. Do not default an unknown state to paid.
39
+
40
+ ## Idempotency
41
+
42
+ - The provider idempotency key is a stable string for one attempt, such as `order-{id}-attempt-{n}`.
43
+ - Persist `attempt -> provider payment id` before you depend on the response, or immediately after the first success, inside a unique constraint when the database allows it.
44
+ - The webhook uniqueness key is the provider event id when that id is stable across retries. A delivery attempt id that changes on every POST is not enough.
45
+ - If the notification arrives before your create call returns, the webhook must be able to find the order by the idempotency key or by metadata you sent to the provider.
46
+
47
+ ## Amounts
48
+
49
+ Convert the shop total to integer minor units with the currency's exponent (`100` for a two-decimal currency, `1` for a zero-decimal currency). Read the exponent from the store or from the provider's currency table. Do not compare PHP floats.
50
+
51
+ Reject the notification when the paid minor units differ from the order total. A mismatch is an exception for a person, not an automatic partial capture, unless the product explicitly sells partial payments.
52
+
53
+ ## Browser return
54
+
55
+ The return URL may include a payment id. Use it only to look up the provider payment from your server. Ignore `status=success` query parameters. A shopper, or a third party, can send those.
56
+
57
+ Show a pending confirmation when the webhook has not arrived yet. Poll your own order record, or retrieve the payment once from the provider. Do not poll the provider in a tight loop from the browser.
58
+
59
+ ## Refunds
60
+
61
+ - Refund from the shop admin through the provider API. Do not ask staff to refund only in the provider dashboard unless the shop has a reconciliation job that pulls those refunds back.
62
+ - Send an idempotency key per refund attempt.
63
+ - Store the provider refund id on the shop refund record.
64
+ - A partial refund leaves the order paid for the remainder. A full refund uses the platform's refunded state after the provider confirms it.
65
+
66
+ ## Secrets and logs
67
+
68
+ - Webhook secrets and API keys stay in the server environment.
69
+ - Log the order id, provider payment id, event id, and your decision. Do not log the PAN, CVV, full card number, or the webhook secret.
70
+ - Verify signatures on the raw body of the request before parsing. The order and webhook skill includes a WooCommerce HMAC checker. For other providers, follow that provider's documented MAC and compare with a constant-time compare.
71
+
72
+ ## Gotchas
73
+
74
+ - Emptying the cart before the provider confirms payment loses the shopper's items when they abandon the redirect.
75
+ - Marking paid inside the browser return, and again in the webhook, sends two receipts and can reduce stock twice. The paid transition has to be idempotent.
76
+ - Trusting the amount in the client payload lets a modified request pay a different total than the order.
77
+ - A `200` to the provider means you stored the event, not that you have already fulfilled the order. Fulfillment can run after the response if the event is durable. A `4xx` for a signature failure is appropriate. A `5xx` asks the provider to retry, so the handler must tolerate a duplicate.
78
+ - Test the failed, canceled, and duplicate paths. A sandbox success charge is not coverage.
79
+
80
+ ## Verification
81
+
82
+ - Two identical charge requests with the same idempotency key create one provider payment.
83
+ - A replayed signed webhook does not send a second receipt or reduce stock again.
84
+ - A signed webhook with the wrong amount does not mark the order paid.
85
+ - A forged browser return without a matching provider payment does not mark the order paid.
86
+ - A declined payment returns the shopper to checkout with the cart intact and the order not paid.
87
+
88
+ ## Evaluation
89
+
90
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,9 @@
1
+ Confirm capture through the payment-complete path.
2
+
3
+ ```php
4
+ if ( ! $order->is_paid() ) {
5
+ $order->payment_complete( $transaction_id );
6
+ }
7
+ ```
8
+
9
+ Do not treat the browser return query as proof that the capture succeeded.
@@ -0,0 +1,3 @@
1
+ Verify the signature over the raw body, then ignore a duplicate.
2
+
3
+ Store the provider payment id. If that id is already on the order, return 200 without a second capture. Use the same idempotency key when the original charge request times out.
@@ -0,0 +1,30 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "confirmed-capture",
5
+ "task": "Mark a WooCommerce order paid only after the provider confirms the capture, and store the transaction id.",
6
+ "expects": {
7
+ "must_include": [
8
+ "payment_complete",
9
+ "$transaction_id"
10
+ ],
11
+ "must_not_include": [
12
+ "add_post_meta"
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "replayed-webhook",
18
+ "task": "Apply a signed payment webhook once when the provider retries the same captured payment.",
19
+ "expects": {
20
+ "must_include": [
21
+ "idempotency",
22
+ "raw body"
23
+ ],
24
+ "must_not_include": [
25
+ "status=success"
26
+ ]
27
+ }
28
+ }
29
+ ]
30
+ }