b2c-hooks について
Salesforce B2C Commerce の開発者向けに、OCAPI/SCAPI、システム、カスタムフックの登録と実装を説明するスキルです。戻り値の挙動、リクエスト内のデータ受け渡し、注文決済の承認フローを扱います。
主な機能
- 01フック登録ファイルとスクリプト構成
- 02HookMgr の呼び出しとステータス処理
- 03OCAPI/SCAPI フックの挙動
- 04request.custom によるリクエスト内のデータ受け渡し
- 05注文決済の承認と注文確定
活用シーン
- 01OCAPI または SCAPI の動作を拡張
- 02注文計算などのシステム拡張を実装
- 03注文決済の承認と失敗処理を設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ salesforcecommercecloud/ b2c-developer-tooling --skill b2c-hooksインストール後、Claude Code で/b2c-hooksを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/trigger-evals.jsonreferences/OCAPI-SCAPI-HOOKS.mdORDER-HOOK-LIFECYCLE.mdSYSTEM-HOOKS.md詳細
B2C Commerce フック
フックは、スクリプトを登録してビジネスロジックをカスタマイズできる拡張ポイントです。B2C Commerce は、次の 2 種類のフックをサポートしています。
- OCAPI/SCAPI フック - before、after、modifyResponse フックで API リソースを拡張します
- システムフック - 注文計算、決済、その他のコア機能向けのカスタム拡張ポイントです
フックタイプの概要
| タイプ | 目的 | 例 |
|---|---|---|
| OCAPI/SCAPI | API の動作を拡張します | dw.ocapi.shop.basket.afterPOST |
| システム | コアビジネスロジック | dw.order.calculate |
| カスタム | 独自の拡張ポイント | app.checkout.validate |
フックの登録
ファイル構成
my_cartridge/
├── package.json # References hooks.json
└── cartridge/
└── scripts/
├── hooks.json # Hook registrations
└── hooks/ # Hook implementations
├── basket.js
└── order.js
package.json
フック設定ファイルを参照します。
{
"name": "my_cartridge",
"hooks": "./cartridge/scripts/hooks.json"
}
hooks.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"
}
]
}
フックスクリプト
フックメソッド名(パッケージプレフィックスを除く)に一致する関数をエクスポートします。
// 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
dw.system.HookMgr を使用して、プログラムからフックを呼び出します。
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);
}
| メソッド | 説明 |
|---|---|
hasHook(extensionPoint) | フックが登録されているか、デフォルト実装がある場合に true を返します |
callHook(extensionPoint, functionName, args...) | フックを呼び出し、結果または undefined を返します |
Status オブジェクト
フックは、成功または失敗を示すために dw.system.Status を返します。
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;
| ステータス | HTTP レスポンス | 動作 |
|---|---|---|
Status.OK | 続行 | フックの実行を続行します |
Status.ERROR | 400 Bad Request | トランザクションをロールバックし、処理を停止します |
| 捕捉されない例外 | 500 Internal Error | トランザクションをロールバックします |
戻り値の動作(重要)
値を返す OCAPI/SCAPI フックは、システム実装と、その拡張ポイントに登録されている後続のすべてのフックをスキップします。
これはよくあるバグの原因です。たとえば、フックが Status.OK を返すと、システムの dw.order.calculate 実装が実行されず、カートの合計が正しくならないことがあります。
値を返す場合
次の場合に限り、Status オブジェクトを返します。
- エラー(
Status.ERROR)で処理を停止したい場合 - システム実装を意図的にスキップしたい場合
値を返さない場合
カート計算などのシステム実装が実行されるようにするには、何も返さないでください。
// 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;
};
まとめ
| 戻り値 | OCAPI/SCAPI の動作 | カスタムフックの動作 |
|---|---|---|
undefined(戻り値なし) | システム実装と後続のフックが実行されます | すべてのフックが実行されます |
Status.OK | システム実装と後続のフックをスキップします | すべてのフックが実行されます |
Status.ERROR | 処理を停止し、エラーを返します | すべてのフックが実行されます |
デバッグのヒント:カートの合計が正しくない場合やフックが実行されない場合は、先行するフックが値を返していないか確認してください。
OCAPI/SCAPI フック
OCAPI と SCAPI は同じフックを共有します。Business Manager で有効にします。 Administration > Global Preferences > Feature Switches > Enable Salesforce Commerce Cloud API hook execution
フックタイプ
| フック | 呼び出されるタイミング | 用途 |
|---|---|---|
before<METHOD> | 処理の前 | 検証、アクセス制御 |
after<METHOD> | 処理の後(トランザクション内) | データの変更、外部呼び出し |
modify<METHOD>Response | レスポンスの送信前 | レスポンスプロパティの追加・変更 |
よくあるフックパターン
// 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;
};
フック間でのデータの受け渡し
同じリクエスト内のフック間でデータを受け渡すには、request.custom を使用します。
// 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;
};
SCAPI と OCAPI の判別
exports.afterPOST = function(basket) {
if (request.isSCAPI()) {
// SCAPI-specific logic
} else {
// OCAPI-specific logic
}
};
ヘッドレス注文の決済:Order-PI 認証シームを使用する
ドキュメントに記載された Shopper Orders のライフサイクルを優先してください。デフォルトでは
dw.ocapi.shop.order.afterPOST で決済を承認しないでください。
POST /orders
-> order persists in CREATED
PATCH /orders/{orderNo}/payment-instruments/{paymentInstrumentId}
-> platform updates the order PI
-> dw.order.payment.authorizeCreditCard or dw.order.payment.authorize
-> order.payment_instrument.afterPATCH(..., successfullyAuthorized)
-> default afterPATCH places the order when authorized coverage >= order total
これにより、注文作成とゲートウェイ呼び出しが分離され、復旧に使える永続的な注文番号が保持されます。また、標準の Commerce 実装および決済拡張機能と同じ認証拡張ポイントを使用でき、最終的な対象範囲に基づく注文確定をプラットフォームに任せることができます。
承認のセマンティクス
order-PI の PATCH では、次のように決済フックを選択します。
- 選択した決済方法が有効であり、注文に適用可能であることを確認します。
CREDIT_CARDの場合は、リクエストにpaymentCardを含めます。 - リクエストに
paymentCardが含まれる場合、または注文の PI がクレジットカードタイプの場合は、dw.order.payment.authorizeCreditCardを呼び出します。 - それ以外の場合は、
dw.order.payment.authorizeを呼び出します。 - 保存済みの決済方法では、ドキュメントに記載されている顧客の決済手段 ID(OCAPI のドキュメントでは
customer_payment_instrument_idと呼ばれ、生成された SCAPI クライアントではキャメルケースのプロパティとして公開される場合があります)を渡します。Commerce は注文顧客の決済手段からこれを解決し、承認用にその PI をコピーします。未知の ID が渡されると例外が発生します。リクエストの金額とカードセキュリティコードは、引き続きauthorizeCreditCardに渡されます。 - 顧客 PI への参照がない場合、承認入力には
paymentCardや銀行口座情報などのリクエストデータが設定されます。paymentDetails引数が、すでにorderに紐付けられた永続的な PI と同じオブジェクトインスタンスであると想定しないでください。プロバイダーの状態がそこに保存されている場合は、永続的な決済手段を解決して検証します。 - クライアントから渡された再利用可能な決済トークンを、所有権の代替として受け入れないでください。
- 承認の実装では、決済トランザクションのプロセッサー、金額、タイプ、プロバイダーのトランザクション識別子を設定します。承認が成功した場合にのみ
Status.OKを返し、それ以外の場合はStatus.ERRORを返します。
dw.order.payment.authorizeCreditCard、dw.order.payment.authorize、および order-PI の beforePATCH/afterPATCH フックを登録します。beforePATCH では、ゲートウェイを呼び出す前に、信頼できないリクエストを永続的な注文状態と照合して検証します。検証対象は、PI 数、決済方法、適用可能なプロセッサー、サーバー管理の決済配分、金額、所有権/トークンの出所、統合状態です。単一 PI のチェックアウトでは、要求金額が注文合計と一致することを求められます。分割決済の場合は、サーバー管理の各配分を検証します。
ドキュメントに記載されている afterPATCH の 4 つのパラメーターすべてを使用します。プラットフォームは決済承認の Status を successfullyAuthorized に集約します。同じリクエスト内でその区別が必要な場合に限り、より詳細なプロセッサーの結果を request.custom に保持します。
var Status = require('dw/system/Status');
exports.afterPATCH = function (order, paymentInstrument, newPaymentInstrument, successfullyAuthorized) {
if (!successfullyAuthorized) {
if (request.custom.deterministicPaymentDecline === true) {
return new Status(Status.ERROR, 'PAYMENT_DECLINED');
}
return; // Preserve indeterminate/review state for reconciliation.
}
var validation = validateAuthorizedInstrument(order, paymentInstrument);
if (validation) { return validation; }
// Return undefined so the default placement implementation still runs.
};
successfullyAuthorized === true は、処理された決済手段に適用されます。これだけでは、複数の決済手段を使う注文の決済がすべて完了したことの証明にはなりません。承認済みの決済額が注文合計以上になった場合にのみ、デフォルトの実装が引き続き CREATED 状態の注文を確定するようにします。この処理によって注文は NEW に変わり、エクスポートできる状態になります。
ストアフロント/BFF での結果処理
返された注文ステータスを、注文確定の正式な結果として扱います。
NEW:注文の確定に成功しました。CREATED:注文は確定されていません。複数の決済手段を承認している間は想定される状態の場合がありますが、拒否/審査/不明な状態を表している場合もあります。- 決定的な承認失敗:理由コード
payment_auth_failureを指定して、Shopper Orders のPOST /orders/{orderNo}/actions/fail?reopenBasket=trueを呼び出し、Commerce に失敗した注文を永続化させてバスケットを復元します。 - ゲートウェイの結果が不確定:自動的に再試行したり、バスケットを再度開いたりしないでください。重複承認を防ぐため、照合用に注文を
CREATEDのまま保持します。
この推奨フローでは、order POST フック間で失敗処理を調整するのではなく、永続的な状態遷移に Shopper Orders の fail アクションを使用します。
代替案:order.afterPOSTで承認して注文を確定する
チェックアウトで、注文の作成、すべてのPIの承認、Commerce側での注文確定または失敗への遷移を、注文作成トランザクション内のサーバーサイドフェーズで実行する必要がある場合、または決済プロセッサーが注文PI承認エンドポイントに対応できない場合は、dw.ocapi.shop.order.afterPOSTを使用します。これにより、ストアフロントのオーケストレーションをシンプルに保てます。外部ゲートウェイでの副作用はCommerceトランザクションとともにロールバックされないため、次のトレードオフを考慮してください。
- 外部決済呼び出しは注文作成のプラットフォームトランザクション内で実行され、注文POSTのタイムアウト時間を消費します。
Status.ERRORを返すと注文作成がロールバックされるため、呼び出し元は注文と元のバスケット参照の両方を失う可能性があります。- 実装では、すべてのPIを手動で承認し、合計金額をカバーしていることを確認し、
OrderMgr.placeOrder/failOrderを呼び出して、後続のプラットフォームまたはカートリッジの動作を維持する必要があります。 afterPOST内でOrderMgr.placeOrder()またはOrderMgr.failOrder()をTransaction.wrap()で囲まないでください。これらはすでにプラットフォームトランザクション内で実行されます。
この方法を選択する場合は、トランザクションのセマンティクスと、HTTPエラーを返しながら失敗した注文を永続化する2フック方式について、 注文フックのライフサイクルを参照してください。
フック間のデータ受け渡しに使うrequest.custom
request.customは、フックフェーズ間でリクエストスコープのデータを受け渡す目的にのみ使用してください。同一リクエスト内でauthorize*からafterPATCHへ詳細な結果を渡すためだけに、永続メタデータを追加しないでください。
システムフック
計算フック
| 拡張ポイント | 関数 | 目的 |
|---|---|---|
dw.order.calculate | calculate | バスケット/注文の全体計算 |
dw.order.calculateShipping | calculateShipping | 配送計算 |
dw.order.calculateTax | calculateTax | 税額計算 |
// 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);
};
決済フック
| 拡張ポイント | 関数 | 目的 |
|---|---|---|
dw.order.payment.authorize | authorize | 決済承認 |
dw.order.payment.authorizeCreditCard | authorizeCreditCard | 注文PIのPOST/PATCHによって呼び出されるクレジットカード承認 |
dw.order.payment.capture | capture | 承認済み決済の売上確定 |
dw.order.payment.refund | refund | 決済の返金 |
dw.order.payment.validateAuthorization | validateAuthorization | 承認の有効性を確認 |
dw.order.payment.reauthorize | reauthorize | 期限切れの承認を再承認 |
注文フック
| 拡張ポイント | 関数 | 目的 |
|---|---|---|
dw.order.createOrderNo | createOrderNo | カスタム注文番号の生成 |
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;
};
カスタムフック
独自の拡張ポイントを作成します。
// 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...
}
hooks.jsonに登録します。
{
"hooks": [
{
"name": "app.checkout.validate",
"script": "./hooks/checkout.js"
}
]
}
カスタムフックは、戻り値にかかわらず、登録されているすべての実装を常に実行します。
フック内のリモートインクルード
他のSCAPIエンドポイントからのデータでAPIレスポンスを拡張します。
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] };
};
ベストプラクティス
- システム実装が実行されるよう、OCAPI/SCAPIフックから
undefined(戻り値なし)を返します - 処理を停止する必要がある場合にのみ
Status.ERRORを返します Status.OKを返すと、システム実装と後続のフックがスキップされます- フック間でデータを渡すには
request.customを使用します - 両方のAPIに対応する場合は
request.isSCAPI()を確認します - フックの役割を絞り、パフォーマンスを保ちます
- modifyResponseではカスタムプロパティ(
c_プレフィックス)を使用します - calculateフックでのトランザクションは避けてください(SCAPIが動作しなくなります)
- beforeGETで時間のかかる外部呼び出しは避けてください(キャッシュに影響します)
エラー処理
サーキットブレーカー
フックエラーが多すぎると、サーキットブレーカーが作動します(HTTP 503)。
{
"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"
}
タイムアウト
フックはSCAPIのタイムアウト時間内に完了する必要があります(タイムアウト時はHTTP 504)。
詳細リファレンス
- OCAPI/SCAPI Hooks - API フックのパターンと利用可能なフック
- System Hooks - 計算、支払い、注文のフック
- Order Hook Lifecycle - ライフサイクルの各フェーズ、ロールバックの動作、2 フックパターン