【Amazon Connect × Salesforce】禁止ワード発話時にSalesforceに通知を送信する

## はじめに

近年コンタクトセンター業務では、カスタマーハラスメントと呼ばれる理不尽なクレームな暴力的な言動が多く見受けられ、エージェントを守るために様々な対策を行う必要性が出てきています。

Amazon Connectでは「ルール」という機能を使用することにより、禁止ワード発話時にアクションを起こすことができます。

この記事では、Amazon Connectの「ルール」と、Salesforceの「カスタム通知」の機能を組み合わせて、禁止ワード発話時に、スーパーバイザーのSalesforceアカウントへ通知する方法を記載します。

**※この記事では、Amazon Connectの通話開始時にSalesforceのケースが作成されることを前提とします。**

## システム構成図

1. 禁止ワード発火検知
2. EventBridgeからLambda呼び出し
3. LambdaよりSalesforce Platform Event発火
4. Platform Eventトリガーが呼び出され、カスタム通知送信
5. カスタム通知表示
6. カスタム通知をクリックすることで対象のケースレコードにアクセスできる

![カスタム通知.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/1b77a068-82eb-4bf2-8ed4-256e455e43d5.png)

## 実際の通知

Salesforce右上を確認することで禁止ワード通知を確認することができる。

![スクリーンショット 2025-06-02 17.09.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/a89bebb9-f8dd-4c50-a3c3-9d7cc94c7790.png)

![スクリーンショット 2025-06-02 17.07.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/6ecb0153-c48d-44c1-ad44-f6e1004a873d.png)

通知をクリックすることで、対象のケースレコードにアクセスすることができる。

![スクリーンショット 2025-06-02 17.08.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/673a47d8-a313-44c9-8cfc-367a9d5e54f0.png)


## 設定方法 Salesforce

### プラットフォームイベントの作成

- 表示ラベル : 任意の表示ラベル
- 表示ラベル（複数形） : 任意の表示ラベル
- オブジェクト名 : 任意のオブジェクト名（例 : CustomNotificationPlatformEvent）
- カスタム項目
    - コンタクトID
        - API参照名 : ContactID__c
        - データ型 : テキスト

![スクリーンショット 2025-06-02 17.02.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/ab323dcc-df2f-49a1-8644-be26c3b04398.png)

### カスタム通知の作成

- カスタム通知名 : 任意の名前
- API参照名 : 任意のAPI参照名（例 : NGWordNotification）
- サポートされるチャネル : デスクトップ

![スクリーンショット 2025-06-02 17.03.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/05365274-c6d6-4c58-b250-b86800e60dca.png)

### フローの作成

- フロー種別 : プラットフォームイベントトリガーフロー
- プラットフォームイベントを選択 : 作成したプラットフォームイベントを選択
- ブロック
    - レコードを取得
        - このオブジェクトのレコードを取得 : `ケース`
        - ケースレコードの絞り込み
            - 条件の要件 : `すべての条件に一致（AND）`
            - 項目 : `amazonconnect__AC_Contact_Id__c`
            - 演算子 : `次の文字列と一致する`
            - 値 : `{!$Record.ContactID__c}`
    - レコードを取得
        - このオブジェクトのレコードを取得 : `カスタム通知種別`
        - ケースレコードの絞り込み
            - 条件の要件 : `すべての条件に一致（AND）`
            - 項目 : `DeveloperName`
            - 演算子 : `次の文字列と一致する`
            - 値 : 作成したカスタム通知のAPI名
    - 割り当て
        - 変数 : `{!MentionMembers}`
        - 演算子 : `追加`
        - 値 : 通知を送信したSalesforceユーザーのSalesforceID
    - カスタム通知を送信
        - カスタム通知種別ID : 取得したカスタム通知種別のID
        - 受信者ID : `{!MentionMembers}`
        - 通知タイトル : `禁止ワードが検出されました`
        - 通知本文 : `禁止ワードが検出されました。エージェントのサポートをお願いいたします。`
        - 対象ID : 取得したケースのID


![スクリーンショット 2025-06-02 16.52.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/2e2d4540-86d3-4547-87f5-cff5f38dd25a.png)


## 設定方法 AWS

### Lambda作成

#### 1. 設定値

- 関数名 : 任意の名前
- ランタイム : `Node.js 22.x`
- アーキテクチャ : `x86_64`

#### 2. 環境変数

- CUSTOM_NOTIFICATION_TYPE_API_NAME : Salesforce カスタム通知のAPI名
- SALESFORCE_PLATFORM_EVENT_API_NAME : Salesforce プラットフォームイベントのAPI名
- SALESFORCE_API_VERSION : `63.0`
- SECRET_NAME : Salesforceの認証情報を保存しているSecrets Managet名
- SALESFORCE_LOGIN_URL : 使用するSalesforceのログインURL

#### 3. コード

```javascript
import axios from 'axios';
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

export const handler = async (event) => {
  const contactArn = event.detail.contactArn;
  const parts = contactArn.split('/');
  const contactId = parts[parts.length - 1];

  const secretsManagerClient = new SecretsManagerClient({ region: 'ap-northeast-1' });

  // 環境変数の取得
  const loginUrl = process.env.SALESFORCE_LOGIN_URL;
  const apiVersion = process.env.SALESFORCE_API_VERSION;
  const customNotificationTypeApiName = process.env.CUSTOM_NOTIFICATION_TYPE_API_NAME;
  const platformEventName = process.env.SALESFORCE_PLATFORM_EVENT_API_NAME;
  const secretName = process.env.SECRET_NAME;

  try {
    // Secret Managerから認証情報を取得
    const secretData = await getSecret(secretsManagerClient, secretName);
    const secrets = JSON.parse(secretData.SecretString);
    const consumerKey = secrets.SALESFORCE_CONSUMER_KEY;
    const secretKey = secrets.SALESFORCE_SECRET_KEY;

    if (!consumerKey || !secretKey || !loginUrl || !apiVersion || !customNotificationTypeApiName || !platformEventName) {
      console.error('必要な環境変数が設定されていません (Secret Managerまたは環境変数)。');
      return {
        statusCode: 400,
        body: JSON.stringify({ message: '必要な環境変数が設定されていません (Secret Managerまたは環境変数)。' }),
      };
    }

    // 認証エンドポイントの構築
    const tokenUrl = `${loginUrl}/services/oauth2/token`;

    // 認証リクエストのデータ
    const tokenData = {
      grant_type: 'client_credentials',
      client_id: consumerKey,
      client_secret: secretKey,
    };

    // 認証リクエストの送信
    const tokenResponse = await axios.post(tokenUrl, tokenData, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    });

    const accessToken = tokenResponse.data.access_token;
    const instanceUrl = tokenResponse.data.instance_url;
    const notificationUrl = `${instanceUrl}/services/data/v${apiVersion}/sobjects/${platformEventName}/`;

    const notificationResponse = await axios.post(notificationUrl, {'ContactID__c': contactId},{
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
    });

    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'プラットフォームイベントを送信しました。', response: notificationResponse.data }),
    };

  } catch (error) {
    console.error('エラー発生:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'プラットフォームイベントの送信に失敗しました。', error: error.message }),
    };
  }

  async function getSecret(secretsManagerClient, secretName) {
    const command = new GetSecretValueCommand({
        SecretId: secretName,
    });

    try {
        const response = await secretsManagerClient.send(command);
        return response;
    } catch (error) {
        console.error("エラー発生:", error);
        throw error;
    }
}
};
```

### EventBridge作成

EventBridge > ルール > ルールを作成

![スクリーンショット 2025-06-02 14.08.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/9c96588e-022e-4f0c-90b3-a07e8bd2360a.png)

#### 1. ルールの詳細を定義

- 名前 : 任意の名前
- イベントバス : `default`
- ルールタイプ : `イベントパターンを持つルール`

#### 2. イベントパターンを構築

- イベントソース : `AWS イベントまたは EventBridge パートナーイベント`
- イベントパターン : `パターンフォームを使用する`
    - イベントソース : `AWSのサービス`
    - AWSのサービス : `Amazon Connect`
    - イベントタイプ : `Contact Lens Realtime Rules Matched` 
    - イベントタイプの仕様 : 任意のアクションタイプ

![スクリーンショット 2025-06-02 14.13.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/a3ee6353-d638-4079-b9c8-19472e8772df.png)

#### 3. ターゲットを選択

- ターゲットタイプ : `AWSのサービス`
- ターゲットを選択 : `Lambda関数`
    - ターゲットの場所 : `このアカウントのターゲット`
    - ターゲット名 : 作成したLambda関数名
    - 許可 : `実行ロールを使用`

![スクリーンショット 2025-06-02 14.15.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/fac7c0c2-e47d-4b91-9933-cc87559f39e3.png)

### Amazon Connectルール作成

Amazon Connect > 分析と最適化 > ルール > ルールを作成 > 会話分析

![スクリーンショット 2025-06-02 14.00.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/68988e93-cc4d-4e05-a472-bb9c4742c997.png)

![スクリーンショット 2025-06-02 14.00.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/034ee1ce-de9f-421e-b6ce-3dbaecfa0f53.png)

#### 1. 条件を定義

- いつ : `Contact Lens リアルタイム通話分析が可能`
- 仮定 : `すべて`
- 単語またはフレーズ - 完全一致 : 禁止ワードとしたい単語を入力（例として「NG」と発話した際に発火するようにしています）

![スクリーンショット 2025-06-02 14.03.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/c16fd5f9-358e-46eb-bcc2-4a3a063ea04f.png)

#### 2. アクションを定義

- コンタクトカテゴリの割り当て : 任意のカテゴリ名
- EventBridgeイベントの生成 : 作成したEventBridge名

## おわりに

今回通知対象をSalesforceIDで設定していますが、Salesforceの環境設定次第で動的に通知相手を決定することも可能です。
例えば、Salesforceで設定しているマネージャーに通知を飛ばしたり、ロールの設定より動的に通知相手を設定することも可能です。