【Salesforce】【Apex】CTI連携を実装してみた


Salesforceと各種CTIとの連携をするサービスは色々あるだろうけど
今回は訳あって自作することになったのでざっくりとご紹介します。

仕様

実行方法:外部システムからブラウザに指定のURLを送る
URL:Salesforce組織に保存しているVisualforceページのURL+パラメータ(電話番号)
処理:Visualforceページを読み込む
   ↓
   Apexアクション実行(電話番号で取引先責任者を検索)
   ↓
   取引先責任者がヒットした場合 :詳細ページを開く
   取引先責任者がヒットしない場合:電話番号をもつ取引先責任者レコードを作成し詳細ページをひらく
   ※電話番号が不正な値である場合:エラー画面表示
   
です。それではさっそく作っていきます!

コード

CTI_Coordination.page
<apex:page controller="CTI_CoordinationController" action="{!process}">
    <apex:outputPanel >
        <apex:pageBlock title="受電エラー">
            <apex:outputtext style="font-weight: bold; font-size:20px; color: #F00;" value="電話番号が正常に読み取れませんでした。"/>
            <br/>
            <apex:outputtext value="{!errMsg}"/>
            <br/>
            <apex:form >
          	    <apex:commandButton value="戻る" action="{!URLFOR(escapeUrl)}" style="width:110px; height:30px"/>
            </apex:form>
        </apex:pageBlock>
    </apex:outputPanel>
</apex:page>

Visualforceページです。コントローラーを設定しています。
今回、画面はエラー表示のためだけなのであっさりですね。

CTI_CoordinationController.cls
public class CTI_CoordinationController {
    
    // 受電番号
    public String inboundNumber{get; set;}
    // 取引先責任者の検索結果リスト
    public List<Contact> contacts {get;set;} 
    // 避難用URL
    public String escapeUrl {get;set;} 
    // エラーメッセージ
    public String errMsg {get;set;} 
    // 電話番号の例外
    public class PhoneNumberException extends Exception {}

    // コンストラクタ
    public CTI_CoordinationController() {
        // 避難用URL
        this.escapeUrl = '/lightning/o/Contact/list';
        // 受電番号
        this.inboundNumber = ApexPages.currentPage().getParameters().get('inboundNumber');
    }

    // 受電処理
    public PageReference process() {
        System.debug('******受電処理開始******');
        System.debug('受電番号:' + this.inboundNumber);
        
        String url = this.escapeUrl;

        try{
            // 桁数チェック
            if (this.inboundNumber == null || this.inboundNumber.length() == 0) {
                throw new PhoneNumberException('受電番号が空です');
            }
            if (this.inboundNumber.length() > 40) {
                throw new PhoneNumberException('受電番号が41桁以上です '+ this.inboundNumber);
            }

            // 文字チェック
            String regex = '(\\D)';
            Pattern p = Pattern.compile(regex);
            Matcher m = p.matcher(this.inboundNumber);
            if (m.find()){
                throw new PhoneNumberException('受電番号に数字以外の文字がふくまれています '+ this.inboundNumber);
            }

            // 電話番号で取引先責任者を検索
            this.contacts = getContactsByPhoneNumber(this.inboundNumber);
            System.debug('取引先責任者ヒット件数:' + this.contacts.size());
            
            if(this.contacts.size() == 0){
                // 取引先責任者がヒットしない場合 →取引先責任者を作成し詳細ページを開く
                Contact con =  insertContactByPhoneNumber(this.inboundNumber);
                url = '/lightning/r/Case/' + con.Id + '/view';
            }else{
                // 取引先責任者が複数ヒットする場合 →最終更新日が一番新しいレコードの詳細ページを開く
                url = '/lightning/r/Case/' + this.contacts[0].Id + '/view';    
            }

        }catch(Exception e){
            // エラーの場合 →メッセージと受付リストページへ遷移するボタンを表示
            this.errMsg = e.getTypeName() +':'+ e.getMessage();  
            System.debug(errMsg);
            return null;
        }
        
        System.debug('URL:' + url);
        return new PageReference(url);     

    }
    
       
    // 電話番号で取引先責任者を検索
    public static List<Contact> getContactsByPhoneNumber(String phoneNumber){
        return [SELECT Id FROM Contact WHERE phone = :phoneNumber ORDER BY LastModifiedDate desc];
    }
    
    // 取引先責任者を1件登録
    public static Contact insertContactByPhoneNumber(String phoneNumber){
        String dtStr = Datetime.now().format();
        Contact con = new Contact(
            LastName = dtStr,
            Description = dtStr + 'に受電した番号で自動作成された取引先責任者です',
            Phone = phoneNumber
        );
        insert con;
        return con;
    }

}

Apexです。コンストラクタで受電した番号を取得しています。
Visualforceのactionで呼ばれるprocess()では
電話番号のチェック、取引先責任者の検索、(取引先責任者レコード作成)とページ遷移をしています。

電話番号のチェックは1~40桁の半角数字のみを許容しています。(取引先責任者の電話番号がMAX40桁)
これらに引っかかる場合はカスタム例外をthrowします。

取引先責任者がヒットしない場合は取引先責任者レコードを新規作成します。
LastNameは必須なので処理した日時をいれてあげています。

取引先責任者が複数ヒットしてしまった場合を考慮して「ORDER BY LastModifiedDate desc」をSOQLにいれました。
最終更新日が一番新しい取引先責任者が表示されます。

動作確認

CTIがマシンのブラウザにURLを飛ばしてきたという想定で、URLにを手入力します。

https://★★★.lightning.force.com/apex/CTI_Coordination?inboundNumber=◆◆◆

★★★は組織のドメイン、◆◆◆が電話番号です。

こちらの組織には3人の取引先責任者が登録されています。
2人の田中さんは携帯電話をシェアしているのでしょう、同じ番号です。

【取引先責任者が1件ヒット】
09012345678(佐藤花子さん)から電話がかかってきました。

佐藤花子さんのページが表示されました。

【取引先責任者が複数件ヒット】
次は09011111111(田中さん)からの入電です。

最終更新日が新しい一郎さんのほうが表示されました。

【取引先責任者がヒットしない】
さて次は 09099999999 未登録の番号です。

新しい取引先責任者が作成されてページが表示されました。

正常系はこのような感じです。
異常系は画像だけ載せます。

【電話番号が空】

【電話番号が41桁以上】

【電話番号に半角数字以外が含まれる】

あとがき

getContactsByPhoneNumber()とinsertContactByPhoneNumber()をコントローラー内に入れましたが
ContactDaoクラスがあればそこに入れたほうがいいと思います。

しれっと正規表現を使っていますが、こんな記事も書いてるので良ければご覧ください
【Salesforce】【Apex】正規表現でテキスト内から指定文字列を複数取得する
【Salesforce】【Apex】正規表現で年月テキストから月末日を取得する

今回は急ごしらえなのでVisualforceを使いましたが、時間があるときにLWCでおしゃれにダイアログ表示で作ってみたいと考えています。
取引先責任者が複数件ヒットした場合もできればリスト表示したいですね。そのためにListで取ってきています。

参考

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_system_pagereference.htm
https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_system_pagereference.htm