AgentSkillsCN

b2c-hooks

在 B2C Commerce 中,借助 HookMgr 和扩展点实现钩子功能。当您需要扩展 OCAPI/SCAPI 的行为、处理订单计算等系统事件,或注册自定义钩子实现时,可运用此技能。它涵盖了 hooks.json、dw.ocapi 钩子,以及自定义扩展点。

SKILL.md
--- frontmatter
name: b2c-hooks
description: Implement hooks using HookMgr and extension points in B2C Commerce. Use when extending OCAPI/SCAPI behavior, handling system events like order calculation, or registering custom hook implementations. Covers hooks.json, dw.ocapi hooks, and custom extension points.

B2C Commerce Hooks

Hooks are extension points that allow you to customize business logic by registering scripts. B2C Commerce supports two types of hooks:

  1. OCAPI/SCAPI Hooks - Extend API resources with before, after, and modifyResponse hooks
  2. System Hooks - Custom extension points for order calculation, payment, and other core functionality

Hook Types Overview

TypePurposeExamples
OCAPI/SCAPIExtend API behaviordw.ocapi.shop.basket.afterPOST
SystemCore business logicdw.order.calculate
CustomYour own extension pointsapp.checkout.validate

Hook Registration

File Structure

code
my_cartridge/
├── package.json           # References hooks.json
└── cartridge/
    └── scripts/
        ├── hooks.json     # Hook registrations
        └── hooks/         # Hook implementations
            ├── basket.js
            └── order.js

package.json

Reference the hooks configuration file:

json
{
  "name": "my_cartridge",
  "hooks": "./cartridge/scripts/hooks.json"
}

hooks.json

Register hooks with their implementing scripts:

json
{
  "hooks": [
    {
      "name": "dw.ocapi.shop.basket.afterPOST",
      "script": "./hooks/basket.js"
    },
    {
      "name": "dw.ocapi.shop.basket.modifyPOSTResponse",
      "script": "./hooks/basket.js"
    },
    {
      "name": "dw.order.calculate",
      "script": "./hooks/order.js"
    }
  ]
}

Hook Script

Export functions matching the hook method name (without package prefix):

javascript
// hooks/basket.js
var Status = require('dw/system/Status');

exports.afterPOST = function(basket) {
    // Called after basket creation
    // Returning a value would skip system implementation
};

exports.modifyPOSTResponse = function(basket, basketResponse) {
    // Modify the API response
    basketResponse.c_customField = 'value';
};

HookMgr API

Use dw.system.HookMgr to call hooks programmatically:

javascript
var HookMgr = require('dw/system/HookMgr');

// Check if hook exists
if (HookMgr.hasHook('dw.order.calculate')) {
    // Call the hook
    var result = HookMgr.callHook('dw.order.calculate', 'calculate', basket);
}
MethodDescription
hasHook(extensionPoint)Returns true if hook is registered or has default implementation
callHook(extensionPoint, functionName, args...)Calls the hook, returns result or undefined

Status Object

Hooks return dw.system.Status to indicate success or failure:

javascript
var Status = require('dw/system/Status');

// Success - continue processing
return new Status(Status.OK);

// Error - stop processing, rollback transaction
var status = new Status(Status.ERROR);
status.addDetail('error_code', 'INVALID_ADDRESS');
status.addDetail('message', 'Address validation failed');
return status;
StatusHTTP ResponseBehavior
Status.OKContinuesHook execution continues
Status.ERROR400 Bad RequestTransaction rolled back, processing stops
Uncaught exception500 Internal ErrorTransaction rolled back

Return Value Behavior (Important)

OCAPI/SCAPI hooks that return ANY value will SKIP the system implementation and all subsequent registered hooks for that extension point.

This is a common source of bugs. For example, if a hook returns Status.OK, the system's dw.order.calculate implementation won't run, causing cart totals to be incorrect.

When to Return a Value

Return a Status object only when you want to:

  • Stop processing with an error (Status.ERROR)
  • Skip the system implementation intentionally

When NOT to Return a Value

To ensure system implementations run (like cart calculation), return nothing:

javascript
// Returning Status.OK skips system implementation
exports.afterPOST = function(basket) {
    doSomething(basket);
    return new Status(Status.OK);  // Skips dw.order.calculate
};

// No return value - system implementation runs
exports.afterPOST = function(basket) {
    doSomething(basket);
    // No return, or explicit: return;
};

Summary

Return ValueOCAPI/SCAPI BehaviorCustom Hook Behavior
undefined (no return)System implementation runs, subsequent hooks runAll hooks run
Status.OKSkips system implementation and subsequent hooksAll hooks run
Status.ERRORStops processing, returns errorAll hooks run

Debugging tip: If cart totals are wrong or hooks aren't firing, check if an earlier hook is returning a value.

OCAPI/SCAPI Hooks

OCAPI and SCAPI share the same hooks. Enable in Business Manager: Administration > Global Preferences > Feature Switches > Enable Salesforce Commerce Cloud API hook execution

Hook Types

HookWhen CalledUse Case
before<METHOD>Before processingValidation, access control
after<METHOD>After processing (in transaction)Data modification, external calls
modify<METHOD>ResponseBefore response sentAdd/modify response properties

Common Hook Patterns

javascript
// Validation in beforePUT
exports.beforePUT = function(basket, addressDoc) {
    if (!isValidAddress(addressDoc)) {
        var status = new Status(Status.ERROR);
        status.addDetail('validation_error', 'Invalid address');
        return status;
    }
};

// External call in afterPOST (within transaction)
exports.afterPOST = function(basket, paymentDoc) {
    var result = callPaymentService(paymentDoc);
    request.custom.paymentResult = result; // Pass to modifyResponse
    // Returning a Status would skip system implementation
};

// Modify response
exports.modifyPOSTResponse = function(basket, basketResponse, paymentDoc) {
    basketResponse.c_paymentStatus = request.custom.paymentResult.status;
};

Passing Data Between Hooks

Use request.custom to pass data between hooks in the same request:

javascript
// In afterPOST
exports.afterPOST = function(basket, doc) {
    request.custom.externalId = callExternalService();
};

// In modifyPOSTResponse
exports.modifyPOSTResponse = function(basket, response, doc) {
    response.c_externalId = request.custom.externalId;
};

Detect SCAPI vs OCAPI

javascript
exports.afterPOST = function(basket) {
    if (request.isSCAPI()) {
        // SCAPI-specific logic
    } else {
        // OCAPI-specific logic
    }
};

System Hooks

Calculate Hooks

Extension PointFunctionPurpose
dw.order.calculatecalculateFull basket/order calculation
dw.order.calculateShippingcalculateShippingShipping calculation
dw.order.calculateTaxcalculateTaxTax calculation
javascript
// hooks/calculate.js
var Status = require('dw/system/Status');
var HookMgr = require('dw/system/HookMgr');

exports.calculate = function(lineItemCtnr) {
    // Calculate shipping
    HookMgr.callHook('dw.order.calculateShipping', 'calculateShipping', lineItemCtnr);

    // Calculate promotions, totals...

    // Calculate tax
    HookMgr.callHook('dw.order.calculateTax', 'calculateTax', lineItemCtnr);

    return new Status(Status.OK);
};

Payment Hooks

Extension PointFunctionPurpose
dw.order.payment.authorizeauthorizePayment authorization
dw.order.payment.capturecaptureCapture authorized payment
dw.order.payment.refundrefundRefund payment
dw.order.payment.validateAuthorizationvalidateAuthorizationCheck authorization validity
dw.order.payment.reauthorizereauthorizeRe-authorize expired auth

Order Hooks

Extension PointFunctionPurpose
dw.order.createOrderNocreateOrderNoCustom order number generation
javascript
var OrderMgr = require('dw/order/OrderMgr');
var Site = require('dw/system/Site');

exports.createOrderNo = function() {
    var seqNo = OrderMgr.createOrderSequenceNo();
    var prefix = Site.current.ID;
    return prefix + '-' + seqNo;
};

Custom Hooks

Create your own extension points:

javascript
// Define custom hook
var HookMgr = require('dw/system/HookMgr');

function processCheckout(basket) {
    // Call custom hook if registered
    if (HookMgr.hasHook('app.checkout.validate')) {
        var status = HookMgr.callHook('app.checkout.validate', 'validate', basket);
        if (status && status.error) {
            return status;
        }
    }
    // Continue processing...
}

Register in hooks.json:

json
{
  "hooks": [
    {
      "name": "app.checkout.validate",
      "script": "./hooks/checkout.js"
    }
  ]
}

Custom hooks always execute all registered implementations regardless of return value.

Remote Includes in Hooks

Enhance API responses with data from other SCAPI endpoints:

javascript
var RESTResponseMgr = require('dw/system/RESTResponseMgr');

exports.modifyGETResponse = function(product, doc) {
    // Include Custom API response
    var include = RESTResponseMgr.createScapiRemoteInclude(
        'custom',           // API family
        'my-api',           // API name
        'v1',               // Version
        'endpoint'          // Endpoint
    );
    doc.c_additionalData = { value: [include] };
};

Best Practices

  • Return undefined (no return) from OCAPI/SCAPI hooks to ensure system implementations run
  • Only return Status.ERROR when you need to stop processing
  • Returning Status.OK skips system implementation and subsequent hooks
  • Use request.custom to pass data between hooks
  • Check request.isSCAPI() when supporting both APIs
  • Keep hooks focused and performant
  • Use custom properties (c_ prefix) in modifyResponse
  • Avoid transactions in calculate hooks (breaks SCAPI)
  • Avoid slow external calls in beforeGET (affects caching)

Error Handling

Circuit Breaker

Too many hook errors triggers circuit breaker (HTTP 503):

json
{
  "title": "Hook Circuit Breaker",
  "type": "https://api.commercecloud.salesforce.com/.../hook-circuit-breaker",
  "detail": "Failure rate above threshold of '50%'",
  "extensionPointName": "dw.ocapi.shop.basket.afterPOST"
}

Timeout

Hooks must complete within the SCAPI timeout (HTTP 504 on timeout).

Detailed References