Salesforce B2C Commerce のヘッドレス API を呼んでみる(データ系 API 編)

※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。

***
この記事はヘッドレス API の使い方のシリーズの一つです。その他の記事は下記を参照してください。
* [ショッパー系 API - SCAPI パブリッククライアント編](https://qiita.com/syunyama/items/89b204486ace9fd10440)
* [ショッパー系 API - SCAPI プライベートクライアント編](https://qiita.com/syunyama/items/34bd2a11dabb93bb1dc1)
* [ショッパー系 API - OCAPI 編](https://qiita.com/syunyama/items/0bb4a6e095a53dac7267)
* [データ系 API 編](https://qiita.com/syunyama/items/060f5777b0fc04e4cea1)
***

**今回は SCAPI と OCAPI のデータ系の API の呼び方を紹介したいと思います。**

## ２ つの分類
SCAPI も OCAPI も大きく分けて ２ つ分類ができます。

* 顧客がサイトで利用する API (以下、**ショッパー系**)
* 管理者が他システムとの連携などで利用する API (以下、**データ系**)

:::note warn
「ショッパー系」や「データ系」といった分類は公式な分類ではありません。
:::



## データ系 API の呼び方

データ系の API を呼ぶ際の基本的な流れは SCAPI も OCAPI も同じです。


### 1. API クライアントの作成

「[Add an API Client](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2Fcontent%2Fb2c_commerce%2Ftopics%2Faccount_manager%2Fb2c_account_manager_add_api_client_id.html)」 に沿って API クライアントを作成してください。その際に入力画面で、**[Roles]** には **「Salesforce Commerce API」** ロールと利用するインスタンスを設定してください。また、**[Default Scopes]** には下記の内容を入力してください。
```
roles
tenantFilter
openId
```
そのほかの項目は後ほど説明するのでスキップしてもらって問題ないです。

:::note
この時作成した ID とパスワードをメモしておいてください。
:::


### 2. スコープの設定

#### a.(SCAPI) Account Manager での設定

上記で作成した API クライアントの詳細画面に遷移し、**[Allowed Scopes]** の項目に下記の内容をを入力してください。
```
sfcc.customerlists
```
他にも追加したいスコープがあればこちらの「[スコープ一覧](https://developer.salesforce.com/docs/commerce/commerce-api/guide/auth-z-scope-catalog.html)」を参照してください。


#### b.(OCAPI) Business Manager での設定

Business Manager で **_管理 > サイトの開発 > Open Commerce API 設定_** に遷移して、今回はデータ系の API なので **[タイプの選択]** 項目は **「データ」** を選択した上で、下記のような JSON フォーマットで利用するリーソースの権限を設定し、保存してください。JSON のフォーマットに関しては「[OCAPI Setting](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-commerce-ocapi/ocapisettings.html)」をご覧ください。


```json
{
    "_v": "21.10",
    "clients": [
        {
            "client_id": "1.で作成したクライアントAPIのUUID",
            "resources": [
                {
                    "methods": [
                        "post"
                    ],
                    "read_attributes": "(**)",
                    "write_attributes": "(**)",
                    "resource_id": "/customer_lists/*/customer_search"
                }
            ]
        }
    ]
}
```



### 3. トークンの取得

データ系 API を呼ぶためのアクセストークンの取得方法は２種類あります。
* **「1. API クライアントの作成」** で取得した ID /パスワードでアクセストークンをリクエストする（[ヘルプ](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-commerce-ocapi/oauth.html#requesting-an-access-token-with-the-client-id-and-password)）
* JWT を生成してアクセストークンをリクエストする（[ヘルプ](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-commerce-ocapi/oauth.html#requesting-an-access-token-using-a-jwt-and-key-pair)）

#### a. ID /パスワード

下記のようなリクエストになります。`QWxhZGRpbjpvcGVuIHNlc2FtZQ==` 部分は ID とパスワードをで `:` で結合した `{ID}:{パスワード}` フォーマットを Base64 でエンコードした文字列です。

```http
REQUEST:
POST /dwsso/oauth2/access_token HTTP/1.1
Host: account.demandware.com
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Content-Type: application/x-www-form-urlencoded

REQUEST PARAMETER: 
grant_type=client_credentials
```

#### b. JWT 

下記のようなリクエストになります。 `QeyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9......` 部分に生成した JWT を入れてリクエストします。リクエストをする前に証明書の作成や証明書を Account Manager の API クライアントに設定する必要がありますので、「[Requesting an access token using a JWT and key pair](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-commerce-ocapi/oauth.html#requesting-an-access-token-using-a-jwt-and-key-pair)」を参考にしてください

```http
REQUEST:
POST /dwsso/oauth2/access_token HTTP/1.1
Host: account.demandware.com
Content-Type: application/x-www-form-urlencoded

REQUEST PARAMETER: 
grant_type=client_credentials
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9......
```

Postman の Pre-request Script で JWT を作成する場合はこのようなコードを利用しましたのでサンプルとして記載しておきます。

```js
var jwtSecret = pm.environment.get('jwtSecret') || ''
var clientID = pm.environment.get('clientID') || ''

var header = {
    'typ': 'JWT',
    'alg': 'RS256'
};

var currentTimestamp = Math.floor(Date.now() / 1000)
var data = {
    'iss': clientID,
    'sub': clientID,
    'aud': "https://account.demandware.com:443/dwsso/oauth2/access_token",
    'exp': currentTimestamp + 30, 
}

function base64url(source) {
    encodedSource = CryptoJS.enc.Base64.stringify(source)
    encodedSource = encodedSource.replace(/=+$/, '')
    encodedSource = encodedSource.replace(/\+/g, '-')
    encodedSource = encodedSource.replace(/\//g, '_')
    return encodedSource
}

function generateJwt() {
    eval(pm.globals.get('jsrsasign-js'));
    var sHeader = JSON.stringify(header);
    var sPayload = JSON.stringify(data);
    var signedToken = KJUR.jws.JWS.sign(header.alg, sHeader, sPayload, jwtSecret);
    pm.environment.set('jwtSigned', signedToken)
    console.log('Signed and encoded JWT', signedToken)
}

var navigator = {}; // fake a navigator object for the lib
var window = {}; // fake a window object for the lib

if (pm.globals.has('jsrsasign-js')) generateJwt();
else pm.sendRequest(
    'https://kjur.github.io/jsrsasign/jsrsasign-all-min.js',
    function (err, res) {
        if (err) {
            console.log(err);
        } else {
            pm.globals.set('jsrsasign-js', res.text());
            generateJwt();
        }});
```

#### OCAPI と SCPI でのトークンの取得方法の違い

OCAPI のデータ系の API を呼ぶ場合は上記のままで構わないのですが、 SCAPI の場合はリクエストの中に `scope` を追加する必要があります。スコープは半角空白区切りで使用します。スコープ  `sfcc.customerlists` を利用する場合、例えばこのような感じです。`SALESFORCE_COMMERCE_API` は必ず必要となります。

```http
REQUEST:
POST /dwsso/oauth2/access_token HTTP/1.1
Host: account.demandware.com
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Content-Type: application/x-www-form-urlencoded

REQUEST PARAMETER: 
grant_type=client_credentials
&scope=SALESFORCE_COMMERCE_API:{tenantID} sfcc.customerlists
```



### 4. データ系 API を呼ぶ


今回は SCAPI、OCAPI 共に顧客の検索を行った時のリクエスト内容を記載します。

:::note
その他の API については[ SCAPI の API ドキュメント](https://developer.salesforce.com/docs/commerce/commerce-api/references?meta=assignments:Summary)の「Shopper〜」から始まっていないもの、[ OCAPI の API ドキュメント](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-commerce-ocapi/opencommerceapi.html)の Data API をご参照ください。
:::


* `token` は上記でアクセストークンリクエストした際に返ってくるレスポンスボディの中の `access_token` の内容を利用してください。
* `customerListID` については Business Manager の **_管理 > サイト > 顧客リスト_** から ID をご確認ください。
* * `shortCode`、`tenantID`、`orgID`、`siteID`、`realmID` については 「[B2C Commerce に関わる ID を把握](https://qiita.com/syunyama/items/58433ae8e3c5fda7f37e#a--b2c-commerce-%E3%81%AB%E9%96%A2%E3%82%8F%E3%82%8B-id-%E3%82%92%E6%8A%8A%E6%8F%A1)」 を参照してください。

#### SCAPI

```http
REQUEST:
POST /customer/customers/v1/organizations/{orgID}/customer-lists/{customerListID}/customer-search HTTP/1.1
Host: {shortCode}.api.commercecloud.salesforce.com
Authorization: Bearer {token}
Content-Type: application/json

REQUEST BODY:
{
  "limit": 5,
  "query": {
    "textQuery": {
      "fields": [
        "first_name"
      ],
      "searchPhrase": "だ"
    }
  },
  "offset": 0
}
```

#### OCAPI

```http
REQUEST:
POST /s/-/dw/data/v21_10/customer_lists/{customerListID}/customer_search HTTP/1.1
Host: {利用しているサンドボックスのドメイン}
Authorization: Bearer {token}
Content-Type: application/json

BODY:
{
    "query": {
        "text_query": {
            "fields": ["first_name"],
            "search_phrase": "だ"
        }
    }
}
```