From 767c6e8918b299b54d425007c2d6acbfbdce312d Mon Sep 17 00:00:00 2001 From: CharithaT07 Date: Thu, 16 Jul 2026 17:57:55 +0530 Subject: [PATCH] feat(skills): document order hook lifecycle and two-hook pattern @W-23157033 Add order hook rollback semantics, the afterPOST + modifyPOSTResponse two-hook pattern for persisting failed orders while returning HTTP errors, and request.custom inter-hook data passing to the b2c-hooks skill. Includes new ORDER-HOOK-LIFECYCLE.md reference and additional trigger eval queries. --- skills/b2c/skills/b2c-hooks/SKILL.md | 54 ++++++- .../skills/b2c-hooks/evals/trigger-evals.json | 2 + .../references/ORDER-HOOK-LIFECYCLE.md | 153 ++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 skills/b2c/skills/b2c-hooks/references/ORDER-HOOK-LIFECYCLE.md diff --git a/skills/b2c/skills/b2c-hooks/SKILL.md b/skills/b2c/skills/b2c-hooks/SKILL.md index b98d9ce92..7393afb3a 100644 --- a/skills/b2c/skills/b2c-hooks/SKILL.md +++ b/skills/b2c/skills/b2c-hooks/SKILL.md @@ -1,6 +1,6 @@ --- name: b2c-hooks -description: Implement hooks with HookMgr, hooks.json registration, and system extension points for order, basket, and API lifecycle events. Use this skill whenever the user needs to register a hook implementation, extend OCAPI/SCAPI behavior with before/after hooks, customize order calculation or payment authorization, or create custom extension points. Also use when debugging hook registration or Status return values -- even if they just say 'run code when an order is placed' or 'intercept the basket API'. +description: Register and implement B2C Commerce platform hooks -- scripts registered in a cartridge's hooks.json and invoked by HookMgr during OCAPI/SCAPI requests or system events. This skill applies ONLY when the user's question involves writing or debugging code that is registered in hooks.json and called by the platform's HookMgr dispatcher. Key identifiers that signal this skill: hooks.json, HookMgr.callHook(), dw.system.Status returns, dw.ocapi.shop.* extension points (beforePOST/afterPOST/modifyResponse), dw.order.calculate, app.payment.processor.*. Covers: hook script authoring, registration, Status OK/ERROR/rollback semantics, request.custom inter-hook data passing, custom extension points. Hard exclusions -- do NOT trigger for these even if the query mentions 'hook' or 'order': outbound webhook/notification endpoints on external Node.js/Express services (not hooks.json-registered); SFRA controller routes, middleware, or prepend/append chains; scheduled job step modules (execute/beforeStep/afterStep in steptypes.json, not hooks.json); Git or CI hooks (husky, pre-commit). --- # B2C Commerce Hooks @@ -338,6 +338,57 @@ Register it like any order hook: > The `app.payment.processor.` Authorize hooks are themselves custom hooks (one per payment processor, function `Authorize`). By SFRA convention they return a plain object whose `error` flag signals the outcome — `{ authorized: true }` on success, `{ error: true }` on decline — which is why the example treats a missing result or `result.error` as a failure. This mirrors the SFRA `handlePayments` checkout helper. For order-status semantics (`placeOrder`/`failOrder`, reopen-basket behavior, status transitions) see [b2c-ordering](../b2c-ordering/SKILL.md). +## Order Hook Lifecycle and Rollback Semantics + +The order POST hooks execute in a defined sequence with different transaction semantics at each phase: + +``` +beforePOST(basket) ← Validation; Status.ERROR rejects before order creation + ↓ +[Order created: CREATED] ← Platform creates order from basket + ↓ +afterPOST(order) ← Inside platform transaction; owns CREATED→NEW/FAILED + ↓ +[Transaction commits] + ↓ +modifyPOSTResponse(order, response) ← After commit; response-shaping only +``` + +### Rollback Semantics + +| Phase | Transaction context | `Status.ERROR` effect | +|-------|--------------------|-----------------------| +| `afterPOST` | Inside transaction | **Rolls back** — no order record survives | +| `modifyPOSTResponse` | After commit | **No rollback** — order already persisted; only sets HTTP response to 400 | + +This means `afterPOST` gives you EITHER a persisted failed order (return `Status.OK` after `OrderMgr.failOrder`) OR an HTTP error (return `Status.ERROR`), **not both**. + +### Two-Hook Pattern: Persist Failed Order AND Return HTTP Error + +When you need a queryable FAILED order (for metrics/triage) AND an HTTP error to the storefront (so the UI shows a decline): + +1. **`afterPOST`**: Call `OrderMgr.failOrder(order, false)`, stash decline details on `request.custom`, return `Status.OK` so the transaction commits. +2. **`modifyPOSTResponse`**: Read `request.custom`, return `new Status(Status.ERROR, code, message)` — this sets the HTTP response to 400 without rolling back the persisted order. + +See [Order Hook Lifecycle reference](references/ORDER-HOOK-LIFECYCLE.md) for the full code example and verified test results. + +### `request.custom` for Inter-Hook Data Passing + +`request.custom` (`dw.system.Request`) is the idiomatic channel to pass data between hooks within the same request. It persists for the request's lifetime and works across all hook phases (before → after → modifyResponse). + +```javascript +// In afterPOST +request.custom.declineInfo = { code: 'PAYMENT_DECLINED', reason: 'Insufficient funds' }; + +// In modifyPOSTResponse (same request, after commit) +var info = request.custom.declineInfo; +if (info) { + return new Status(Status.ERROR, info.code, info.reason); +} +``` + +This technique applies generally — not just to orders. Any pair of hooks in the same request can communicate via `request.custom`. + ## System Hooks ### Calculate Hooks @@ -482,3 +533,4 @@ Hooks must complete within the SCAPI timeout (HTTP 504 on timeout). - [OCAPI/SCAPI Hooks](references/OCAPI-SCAPI-HOOKS.md) - API hook patterns and available hooks - [System Hooks](references/SYSTEM-HOOKS.md) - Calculate, payment, and order hooks +- [Order Hook Lifecycle](references/ORDER-HOOK-LIFECYCLE.md) - Lifecycle phases, rollback semantics, and the two-hook pattern diff --git a/skills/b2c/skills/b2c-hooks/evals/trigger-evals.json b/skills/b2c/skills/b2c-hooks/evals/trigger-evals.json index 0646353b9..c1991c1cb 100644 --- a/skills/b2c/skills/b2c-hooks/evals/trigger-evals.json +++ b/skills/b2c/skills/b2c-hooks/evals/trigger-evals.json @@ -5,6 +5,8 @@ {"query": "I'm implementing custom payment authorization using the dw.order.payment.authorize hook. My hooks.json is in the cartridge but the hook never fires. I verified the cartridge is on the path. What else could be wrong?", "should_trigger": true}, {"query": "We want to create our own extension point so other cartridges can plug into our loyalty module. Specifically, when loyalty points are redeemed, other cartridges should be able to react. How do I define and call a custom hook, and how do consuming cartridges register for it?", "should_trigger": true}, {"query": "I need to modify the OCAPI response for product searches to add a custom field. I know there's a modifyGETResponse hook for shop resources. How do I access the original response, add my field, and pass data from a beforeGET hook using request.custom?", "should_trigger": true}, + {"query": "My SCAPI order afterPOST hook returns Status.ERROR when payment is declined but the order disappears completely from order_search. I need the failed order to persist for reporting AND still return an error to the storefront. How do I achieve both?", "should_trigger": true}, + {"query": "What's the difference between afterPOST and modifyPOSTResponse for the order hook? I heard one rolls back and the other doesn't. How do I pass data between them using request.custom?", "should_trigger": true}, {"query": "How do I set up Git pre-commit hooks to run our linter before every commit? I want to use husky or a similar tool in the project.", "should_trigger": false}, {"query": "I need to set up a webhook endpoint in our external Node.js service that receives order placement notifications from Commerce Cloud. How do I configure the outbound webhook?", "should_trigger": false}, {"query": "How do I add a new controller route that runs before the checkout page loads to check inventory availability? I want it as middleware in the SFRA controller chain.", "should_trigger": false}, diff --git a/skills/b2c/skills/b2c-hooks/references/ORDER-HOOK-LIFECYCLE.md b/skills/b2c/skills/b2c-hooks/references/ORDER-HOOK-LIFECYCLE.md new file mode 100644 index 000000000..229187cda --- /dev/null +++ b/skills/b2c/skills/b2c-hooks/references/ORDER-HOOK-LIFECYCLE.md @@ -0,0 +1,153 @@ +# Order Hook Lifecycle (SCAPI/OCAPI) + +## Lifecycle Phases + +The order creation request flows through these phases in order: + +``` +Request in + | + v +[1] beforePOST(basket) + - Validation phase + - Returning Status.ERROR rejects the request before any order is created + | + v +[2] Platform creates order in CREATED status (from the basket) + | + v +[3] afterPOST(order) <-- INSIDE platform transaction + - Owns the CREATED -> NEW or CREATED -> FAILED transition + - Status.ERROR here rolls back the entire order creation + | + v + (transaction commits) + | + v +[4] modifyPOSTResponse(order, orderResponse) <-- AFTER transaction commits + - Response-shaping phase only + - Order is already persisted; cannot be rolled back from here + | + v +Response out +``` + +| Phase | Hook | Runs relative to transaction | Purpose | +|-------|------|------------------------------|---------| +| 1 | `beforePOST(basket)` | Before order creation | Validate the incoming basket; reject with `Status.ERROR` to prevent order creation | +| 2 | (platform) | — | Platform creates order in `CREATED` status | +| 3 | `afterPOST(order)` | Inside platform transaction | Transition `CREATED` -> `NEW` (place) or `CREATED` -> `FAILED` (fail) | +| 4 | `modifyPOSTResponse(order, orderResponse)` | After commit | Shape the HTTP response; cannot roll back | + +Key point: a non-null `Status` return ends `modifyPOSTResponse` hook execution (same behavior as other `modifyResponse` hooks). + +## Rollback Semantics + +| Hook | Transaction context | `Status.ERROR` behavior | +|------|---------------------|-------------------------| +| `afterPOST` | Inside platform transaction | Rolls back entire order creation — NO order record survives | +| `modifyPOSTResponse` | After commit | Does NOT roll back — order already persisted; only affects HTTP response | + +This was empirically verified on sandbox zzpq-019 (2026-06-24). Order 00000502 returned HTTP 400 from `afterPOST` `Status.ERROR` and left 0 matches in `order_search`. + +Implication: `afterPOST` alone gives you EITHER a persisted failed order (by calling `failOrder` then returning `Status.OK`) OR an HTTP error (by returning `Status.ERROR`) — not both. + +## The Two-Hook Pattern: Persist Failed Order AND Return HTTP Error + +Sometimes you need BOTH: +- A persisted `FAILED` order (for metrics, triage, queryability) +- An HTTP error response to the storefront (so the UI shows a decline, not a false confirmation) + +A single `afterPOST` cannot do both — its transaction semantics force a choice. The solution is to split the responsibilities across two hook phases. + +Solution verified on zzpq-019: + +### Step 1: `afterPOST` — persist the FAILED order + +```javascript +var OrderMgr = require('dw/order/OrderMgr'); +var Status = require('dw/system/Status'); + +exports.afterPOST = function (order) { + var authResult = authorizePayment(order); + + if (authResult.declined) { + // Fail the order so it persists in FAILED status (queryable for metrics) + OrderMgr.failOrder(order, false); + + // Stash decline details for modifyPOSTResponse via request.custom + request.custom.paymentDecline = { + code: authResult.code, + message: authResult.message + }; + + // Return OK so the transaction COMMITS (FAILED order persists) + return new Status(Status.OK); + } + + // Success path: place the order + OrderMgr.placeOrder(order); + order.setConfirmationStatus(dw.order.Order.CONFIRMATION_STATUS_CONFIRMED); + order.setExportStatus(dw.order.Order.EXPORT_STATUS_READY); +}; +``` + +### Step 2: `modifyPOSTResponse` — return HTTP error without rollback + +```javascript +exports.modifyPOSTResponse = function (order, orderResponse) { + // Check if afterPOST stashed a decline + var decline = request.custom.paymentDecline; + if (decline) { + // Return ERROR here — order is already committed, no rollback + return new Status(Status.ERROR, decline.code, decline.message); + } + // No decline — let the normal 200 response through +}; +``` + +### `hooks.json` registration + +```json +{ + "hooks": [ + { "name": "dw.ocapi.shop.order.afterPOST", "script": "./hooks/order.js" }, + { "name": "dw.ocapi.shop.order.modifyPOSTResponse", "script": "./hooks/order.js" } + ] +} +``` + +### Verified behavior (zzpq-019, 2026-06-24) + +- **Tainted order 00000503** -> HTTP 400 with `extensionPoint: dw.ocapi.shop.order.modifyPOSTResponse`, `statusCode`/`statusMessage` from the `Status` AND persisted `status=FAILED` (queryable) +- **Clean order 00000504** -> HTTP 200, `status=NEW` + +## Request-Scoped Data Passing: `request.custom` + +`request.custom` is the idiomatic channel for passing data between hooks that fire within the same SCAPI/OCAPI request. + +- `request` is `dw.system.Request` — a global object whose `.custom` attribute bag persists for the lifetime of the request +- Works between any hook phases in the same request: `beforePOST` -> `afterPOST` -> `modifyPOSTResponse` +- Type: `CustomAttributes` (key-value pairs; values can be any serializable type) + +Example pattern: + +```javascript +// In afterPOST — stash data +request.custom.myKey = { someData: 'value' }; + +// In modifyPOSTResponse — read it +var data = request.custom.myKey; +``` + +Use cases beyond order decline: + +- Passing external service call results to response enrichment +- Flagging validation warnings (non-fatal) for response annotation +- Timing/instrumentation across hook phases + +## Cross-References + +- [Order afterPOST canonical example](../SKILL.md#order-afterpost-headless-order-placement) — the standard `afterPOST` pattern (without `modifyPOSTResponse`) +- [b2c-ordering](../../b2c-ordering/SKILL.md) — `OrderMgr.placeOrder`/`failOrder`, status transitions, reopen-basket behavior +- [OCAPI/SCAPI Hooks reference](./OCAPI-SCAPI-HOOKS.md) — complete hook signature list