Salesforce B2C Commerce Cloud:フックを活用したチェックアウトフローのカスタマイズ徹底解説

背景と適用シナリオ

Salesforce B2C Commerce Cloud(旧Demandware)は、世界中のトップブランドに採用されている、スケーラビリティと柔軟性に優れたEコマースプラットフォームです。このプラットフォームの核心機能の一つが、顧客が購入を完了するためのチェックアウトフローです。チェックアウト体験は、コンバージョン率に直接的な影響を与える極めて重要な要素であり、多くの企業が独自のビジネス要件に合わせてこのプロセスを最適化したいと考えています。

標準のチェックアウトフローは多くのシナリオに対応できますが、実際のビジネスでは以下のような特殊な要件が頻繁に発生します。

  • カスタム決済連携:独自の決済代行サービスや後払いサービスとの連携。
  • 不正注文検知:注文確定前に外部の不正検知システムと連携し、リスクスコアを評価する。
  • ポイント・ロイヤリティプログラム:注文内容に基づいてポイントを計算し、顧客アカウントに付与する。
  • 特殊な配送ロジック:特定の商品や配送先に応じて、配送料を動的に計算したり、配送オプションを制限したりする。
  • 外部システム連携:注文が確定したタイミングで、ERP(Enterprise Resource Planning、企業資源計画)やWMS(Warehouse Management System、倉庫管理システム)に注文データをリアルタイムで送信する。

これらのカスタマイズを、プラットフォームのコアコードを直接変更することなく、安全かつアップグレードに強い方法で実現するために、Salesforce B2C Commerce Cloudではフック (Hooks) という仕組みが提供されています。フックは、SFRA (Storefront Reference Architecture) と呼ばれる最新のアーキテクチャにおける標準的なカスタマイズ手法であり、本記事ではこのフックを利用してチェックアウトフローを拡張する方法を詳説します。


原理説明

B2C Commerce Cloudにおけるフックとは、アプリケーションの特定の実行ポイントに割り込んで、独自のカスタムロジックを追加するための「拡張ポイント」です。SFRAのベースカートリッジ(`app_storefront_base`)内の主要なビジネスロジック、特にチェックアウトのような重要なプロセスには、あらかじめ多数のフックが定義されています。

開発者は、これらのフックに対して独自のスクリプトを登録することで、ベースの機能を上書きしたり、拡張したりすることができます。この仕組みの最大の利点は、ベースカートリッジのコードを一切変更しないため、将来SalesforceがSFRAをバージョンアップした際にも、カスタマイズ部分への影響を最小限に抑え、アップグレード作業を容易にすることです。

フックの仕組み

フックの動作は、以下の3つの要素によって成り立っています。

  1. フック拡張ポイント (Hook Extension Point): SFRAのベースコード内に定義されている、フックの呼び出し箇所です。例えば、注文を確定するプロセスでは `dw.order.hooks.placeOrder.placeOrder` という名前のフックが呼び出されます。
  2. `hooks.json` 定義ファイル: カスタムカートリッジ内に `hooks.json` というファイルを作成し、どのフック拡張ポイントに対して、どのスクリプトファイルを実行するかを定義します。このJSONファイルが、フックとカスタムロジックを結びつける役割を果たします。
  3. 実装スクリプト: `hooks.json` で指定されたJavaScriptファイルです。このファイル内に、実行したいカスタムロジックを記述します。スクリプトは、特定のインターフェース(引数と返り値)に従って実装する必要があります。

アプリケーションがフック拡張ポイントに到達すると、システムはカートリッジパス (Cartridge Path) を参照します。カートリッジパスとは、Business Manager(ビジネス・マネージャー、管理画面)で設定されるカートリッジの読み込み順序です。システムは、パスの先頭から順に各カートリッジを調べ、`hooks.json` ファイル内に該当フックの定義があるかを探します。最初に見つかった定義が採用され、指定されたスクリプトが実行されます。したがって、カスタムカートリッジをベースカートリッジより前に配置することが不可欠です。

チェックアウトにおける主要なフック

チェックアウトプロセスでは、以下のような様々なフックが利用可能です。

  • `dw.order.calculate`: カート全体の税金や合計金額を計算する際に呼び出されます。カスタムプロモーションや複雑な税計算ロジックを実装するのに使用します。
  • `dw.order.shipping.calculate`: 配送料を計算する際に呼び出されます。外部の配送キャリアAPIと連携してリアルタイムに送料を取得するなどの用途に適しています。
  • `app.payment.processor.authorize`: 支払い処理の承認ステップで呼び出されます。各種決済ゲートウェイとの連携ロジックは、通常このフックで実装します。
  • `dw.order.hooks.placeOrder.placeOrder`: 顧客が「注文を確定」ボタンを押した直後、注文が最終的にデータベースに保存されるプロセスで呼び出されます。本記事ではこのフックを例に解説します。

示例代码

ここでは、最も重要なフックの一つである `dw.order.hooks.placeOrder.placeOrder` を使用して、注文確定時にカスタムロジックを追加する例を示します。シナリオとして、「注文確定時に、独自の不正注文スコアを計算し、注文のカスタム属性に保存する」という処理を実装します。

ステップ1: `hooks.json` の作成

まず、カスタムカートリッジ(例: `my_custom_cartridge`)のルートディレクトリに `hooks.json` ファイルを作成し、`placeOrder` フックと実装スクリプトを紐付けます。

{
    "hooks": [
        {
            "name": "dw.order.hooks.placeOrder.placeOrder",
            "script": "./cartridge/scripts/hooks/placeOrderHook.js"
        }
    ]
}

この定義により、`dw.order.hooks.placeOrder.placeOrder` フックが呼び出された際に、`./cartridge/scripts/hooks/placeOrderHook.js` ファイルに記述されたロジックが実行されるようになります。

ステップ2: 実装スクリプトの作成

次に、`hooks.json` で指定したパスに `placeOrderHook.js` ファイルを作成し、具体的な処理を記述します。このフックは、引数として `dw.order.Order` オブジェクトを受け取り、返り値として `dw.system.Status` オブジェクトを返す必要があります。

以下のコードは Salesforce B2C Commerce Cloud の公式ドキュメントで示されているフック実装の標準的な構造に基づいています。

'use strict';

// Salesforce Commerce Cloud の標準APIモジュールをインポート
var OrderMgr = require('dw/order/OrderMgr');
var Order = require('dw/order/Order');
var Status = require('dw/system/Status');
var Transaction = require('dw/system/Transaction');
var Logger = require('dw/system/Logger');

// 外部の不正検知サービスを呼び出すためのヘルパースクリプト(仮)
var fraudDetectionService = require('~/cartridge/scripts/services/fraudDetectionService');

/**
 * dw.order.hooks.placeOrder.placeOrder フックの実装
 * このフックは注文が確定されるたびに呼び出される
 * @param {dw.order.Order} order - 確定処理中の注文オブジェクト
 * @returns {dw.system.Status} - 処理の成功または失敗を示すステータスオブジェクト
 */
exports.placeOrder = function (order) {
    try {
        // データベースへの書き込みは必ず Transaction.wrap 内で行う
        Transaction.wrap(function () {
            // --- ここからカスタムロジック ---

            // 1. 外部の不正検知サービスを呼び出してリスクスコアを取得
            var fraudResult = fraudDetectionService.getFraudScore(order);

            if (fraudResult.error) {
                // サービス呼び出しに失敗した場合、エラーをログに記録
                Logger.error('Fraud detection service call failed for order {0}', order.orderNo);
                // Status.ERRORを返すとトランザクションがロールバックされ、注文は確定されない
                // ここではエラーを返さず、続行するがスコアは記録しない
            } else {
                // 取得したスコアを注文のカスタム属性 'fraudRiskScore' に保存
                // 事前にBusiness ManagerでOrderのカスタム属性を作成しておく必要がある
                order.custom.fraudRiskScore = fraudResult.score;
            }

            // 2. 不正スコアが高い場合は、注文をレビューが必要な状態にする
            if (fraudResult.score > 75) {
                order.setExportStatus(Order.EXPORT_STATUS_NOTEXPORTED); // ERPへのエクスポートを保留
                order.custom.requiresManualReview = true; // レビューフラグを立てる
                Logger.warn('High fraud risk score ({0}) for order {1}. Marked for manual review.', fraudResult.score, order.orderNo);
            }

            // --- カスタムロジックここまで ---


            // SFRAの標準的な注文確定処理
            // この処理を呼び出さないと、注文が適切なステータスに更新されない
            if (order.status.value === Order.ORDER_STATUS_CREATED) {
                var placeOrderStatus = OrderMgr.placeOrder(order);
                if (placeOrderStatus.isError()) {
                    // placeOrderでエラーが発生した場合、トランザクションを失敗させる
                    throw new Error(placeOrderStatus.message);
                }

                order.setConfirmationStatus(Order.CONFIRMATION_STATUS_CONFIRMED);
                order.setExportStatus(Order.EXPORT_STATUS_READY); // 不正スコアが低ければエクスポート対象にする
            }
        });
    } catch (e) {
        // try-catchブロックで予期せぬエラーを捕捉
        Logger.error('Error in placeOrder hook for order {0}: {1}', order.orderNo, e.toString());
        // エラーステータスを返すことで、チェックアウトプロセスにエラーを通知し、トランザクションをロールバックする
        return new Status(Status.ERROR, 'PLACE_ORDER_ERROR', 'An error occurred during order placement: ' + e.message);
    }

    // すべての処理が成功した場合、OKステータスを返す
    return new Status(Status.OK);
};

このコードは、注文確定のトランザクション内で不正検知サービスの呼び出しと結果の保存、そして注文ステータスの更新を行っています。エラーが発生した場合は `Status.ERROR` を返すことで、安全にプロセスを中断し、ユーザーにエラーメッセージを表示させることができます。


注意事項

フックを実装する際には、以下の点に注意する必要があります。

権限 (Permissions)

フックスクリプトは、ストアフロントの顧客セッションのコンテキストで実行されます。したがって、スクリプトがアクセスできるデータやAPIは、そのセッションの権限に依存します。通常、チェックアウトプロセスでは顧客が必要なデータにアクセスできるため問題になることは稀ですが、管理者権限が必要な情報にアクセスしようとすると失敗します。

API制限 (API Limits) とパフォーマンス

SalesforceのCore Platform(Sales Cloudなど)のような厳格なガバナ制限はありませんが、B2C Commerce Cloudにもスクリプトの実行時間制限(デフォルトで30秒)が存在します。チェックアウトは非常にパフォーマンスが重要なプロセスであり、フック内の処理が遅いとタイムアウトの原因となり、顧客体験を著しく損ないます。特に外部APIを呼び出す際は、タイムアウト設定を適切に行い、非同期処理を検討するなど、パフォーマンスへの影響を最小限に抑える設計が不可欠です。

エラー処理 (Error Handling)

フックスクリプト内でのエラー処理は極めて重要です。`try...catch` ブロックを使用して例外を確実に捕捉し、失敗した場合は `new Status(Status.ERROR, ...)` を返してください。エラーの `Status` を返すと、進行中のデータベーストランザクションが自動的にロールバックされ、データの不整合を防ぐことができます。また、`dw.system.Logger` を使用して詳細なエラーログを記録することは、問題のデバッグに不可欠です。

カートリッジパス (Cartridge Path)

前述の通り、カスタムカートリッジがフックを正しくオーバーライドするためには、Business Managerの `Administration > Sites > Manage Sites > [サイト名] > Cartridges` で、カスタムカートリッジ(例: `my_custom_cartridge`)がベースカートリッジ(`app_storefront_base`)よりもにリストされている必要があります。この設定を誤ると、カスタムフックは一切呼び出されません。


まとめとベストプラクティス

Salesforce B2C Commerce Cloudのフックは、プラットフォームのアップグレード性を維持しながら、チェックアウトフローのようなコア機能を柔軟にカスタマイズするための強力なメカニズムです。フックを正しく利用することで、ビジネス固有の要件に対応し、他社と差別化された優れた顧客体験を提供することが可能になります。

フックを実装する際のベストプラクティスは以下の通りです。

  • 単一責任の原則:一つのフックスクリプトには、関連性の高い一つの責務だけを持たせるようにします。例えば、決済処理とポイント計算を同じフックで行うのではなく、それぞれ適切なフック(決済フックと注文計算フック)で実装します。
  • サービスフレームワークの活用:外部システムとの連携など、複雑なロジックはフックスクリプト内に直接記述するのではなく、B2C Commerce Cloudのサービスフレームワークを使用してカプセル化します。これにより、コードの再利用性、保守性、テストの容易性が向上します。
  • 徹底したテスト:ユニットテストと結合テストを必ず実施してください。特に、正常系だけでなく、外部APIのタイムアウトや不正なレスポンスといった異常系のテストも網羅することが重要です。
  • カスタムログの活用:フックの実行開始、終了、重要な分岐点、エラー発生時に詳細なログを出力するようにします。本番環境で問題が発生した際の迅速な原因究明に役立ちます。
  • トランザクション管理:データベースの状態を変更する操作は、必ず `Transaction.wrap()` または `Transaction.begin()/commit()/rollback()` を使用して、データの整合性を保証してください。

これらの原則に従うことで、安定的で保守しやすく、スケーラブルなカスタマイズを実装し、Salesforce B2C Commerce Cloudのポテンシャルを最大限に引き出すことができるでしょう。

コメント