こんにちは!エンジニアの岩間です。
今回は、Salesforceで「取引先の所在地に最も近いサービステリトリー担当者へメール通知する」仕組みを、Apexを使って構築した話〜後編〜をお届けします!
前編では、Salesforce のオブジェクトの概要や、緯度・経度(ジオロケーション)機能、Apex クラスについて紹介しました。
※まだ前編をご覧になっていない方はぜひ下記からどうぞ!
取引先から最短距離にいる担当者へメール通知する仕組みをSalesforce Apexで実現してみた〜前編〜
さて、後編ではいよいよ実際のApexコードの実装に入っていきます。
今回は Salesforce の「フロー」から呼び出せるように設計した Apex クラスを使って、緯度・経度をもとに最寄りの担当者を見つけて、自動でメール通知を送る処理を組み立てています。
後半ではフローへの組み込み方や設計時のちょっとした注意点についても紹介していくので、ぜひ最後までお読みいただけると嬉しいです!
Apex プログラム例
早速ここからは、実際のApexコード例をご紹介していきます
今回のクラス NearestServiceTerritoryFinder
は、Salesforceのフローから呼び出し可能な形で作成しています。
主な処理の流れは以下の通りです。
-
リクエストのバリデーション
必要な情報(氏名・緯度・経度)が揃っているかをチェックします。 -
最寄りのサービステリトリーを検索
ジオロケーション機能で距離を計算し、最短距離のサービステリトリーを1件取得します。 -
関連ユーザーのメールアドレス取得
取得したサービステリトリーに紐づくユーザーのメールを取得します。 -
通知メール送信
該当ユーザーに通知メールを送信します。
Apex からメール送信まで一通り自動化することで、担当者への素早い通知が可能になります。
距離ベースの自動通知を実現する一例として、ぜひ参考にしてみてください!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
/** * @description 最寄りのサービステリトリーを検索するクラス * @author xxxx * @date 2025/xx/xx */ public with sharing class NearestServiceTerritoryFinder { /** * @description サービステリトリー検索のためのリクエストクラス * @group Models */ public class Request { /** @description 請求先緯度 */ @InvocableVariable(required=true) public Double billingLatitude; /** @description 請求先経度 */ @InvocableVariable(required=true) public Double billingLongitude; /** @description 取引先責任者姓 */ @InvocableVariable(required=true) public String contactLastName; /** @description 取引先責任者名 */ @InvocableVariable(required=true) public String contactFirstName; } /** * @description リクエストの妥当性を検証 * @param req 検証対象のリクエスト * @return 妥当な場合はtrue、そうでない場合はfalse */ private static Boolean isValidRequest(Request req) { return req != null && String.isNotBlank(req.contactLastName) && String.isNotBlank(req.contactFirstName) && req.billingLatitude != null && req.billingLongitude != null; } /** * @description チームアクセス権限を検証 */ private static void validateTeamAccessPermissions() { if (!Schema.sObjectType.ServiceTerritoryMember.fields.ServiceResourceId.isAccessible()) { throw new System.SecurityException('ServiceTerritoryMember の ServiceResourceId にアクセスする権限がありません。'); } } /** * @description ユーザーアクセス権限を検証 */ private static void validateUserAccessPermissions() { if (!Schema.sObjectType.User.fields.Email.isAccessible()) { throw new System.SecurityException('ユーザーのメールアドレスにアクセスする権限がありません。'); } } /** * @description サービステリトリーアクセス権限を検証 */ private static void validateServiceTerritoryAccessPermissions() { if (!Schema.sObjectType.ServiceTerritory.fields.Id.isAccessible()) { throw new System.SecurityException('ServiceTerritory の Id にアクセスする権限がありません。'); } } /** * @description サービステリトリーメンバーアクセス権限を検証 */ private static void validateServiceResourceAccessPermissions() { if (!Schema.sObjectType.ServiceResource.fields.RelatedRecordId.isAccessible()) { throw new System.SecurityException('ServiceResource の RelatedRecordId にアクセスする権限がありません。'); } } /** * @description メール送信 * @param emailList 送信先のメールアドレスのリスト * @param req リクエスト */ private static void sendEmail(List emailList, Request req) { // メール送信 Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(emailList); // 送信先(String[]) mail.setSubject('対応リクエストのお知らせ'); // 件名(String) String body = 'req.contactLastName + req.contactFirstName様より、至急対応リクエストがありました。'; mail.setHtmlBody(body); // HTML メールの本文を設定 mail.setSenderDisplayName('Salesforce Support'); mail.setBccSender(false); mail.setUseSignature(false); List mailResults = Messaging.sendEmail(new List{mail},false); for (Messaging.SendEmailResult result : mailResults) { if (!result.isSuccess()) { throw new System.EmailException(result.getErrors()[0].getMessage()); } } } /** * @description 最寄りのサービステリトリーを検索、メール送信するメソッド * @param requests サービステリトリー検索のためのリクエストクラスのリスト * @return 最寄りのサービステリトリーのIDのリスト */ @InvocableMethod public static List getNearestServiceTerritory(List requests) { List results = new List(); // 最初のリクエストのみを処理 Request req = requests[0]; if (!isValidRequest(req)) { results.add(null); return results; } // 最も近い ServiceTerritory を検索 validateServiceTerritoryAccessPermissions(); List territories = [ SELECT Id FROM ServiceTerritory WHERE Latitude != NULL AND Longitude != NULL ORDER BY DISTANCE(Address, GEOLOCATION(:req.billingLatitude, :req.billingLongitude), 'km') LIMIT 1 ]; if (territories.isEmpty()) { results.add(null); return results; } ServiceTerritory nearestServiceTerritory = territories[0]; // サービステリトリーのメンバーを取得 validateTeamAccessPermissions(); List territoryMembers = [ SELECT ServiceResourceId FROM ServiceTerritoryMember WHERE ServiceTerritoryId = :nearestServiceTerritory.Id ]; if (territoryMembers.isEmpty()) { results.add(null); return results; } List serviceTerritoryMemberIds = new List(); // ServiceTerritoryMember から ServiceResourceId を取得 for (ServiceTerritoryMember stm : territoryMembers) { if (stm.ServiceResourceId != null) { serviceTerritoryMemberIds.add(stm.ServiceResourceId); } } // ServiceResource から RelatedRecordId を取得 validateServiceResourceAccessPermissions(); List userIds = new List(); for (ServiceResource sr : [ SELECT RelatedRecordId FROM ServiceResource WHERE Id IN :serviceTerritoryMemberIds AND RelatedRecordId != NULL ]) { userIds.add(sr.RelatedRecordId); } validateUserAccessPermissions(); List emailList = new List(); for (User user : [ SELECT Email FROM User WHERE Id IN :userIds AND Email != NULL ]) { emailList.add(user.Email); } // メールがあるメンバーに送信 if (!emailList.isEmpty()) { sendEmail(emailList, req); } results.add(nearestServiceTerritory.Id); return results; } } |
フローへの組み込み
ここからは、作成した Apex クラスを実際の業務フローにどう組み込むかを見ていきましょう。
Salesforce には「フロー(Flow)」という自動化ツールがあります。
レコードの作成や更新といったイベントをトリガーに、ノーコードでさまざまな処理を実行できるのが特徴です。
今回は、「ケース」レコードが作成されたタイミングで、先ほどの NearestServiceTerritoryFinder
クラスを呼び出すフローを構成していきます。
これにより、例えばお問い合わせや通話を受け付けた瞬間に、所在地に最も近い担当者へ自動で通知を飛ばすことが可能になります。
フロー作成手順
ここからはフローの作成手順です。
1. Salesforceにログイン後、右上の歯車アイコン→「設定」をクリックし、フローを検索します。
2. フローの画面に移動後、右上の「新規フロー」をクリックします。
3. Flow Builderのページが表示されるので、「最初から開始」を選択し、次へをクリックします。
4. 次に、「レコードトリガーフロー」を選択し、作成をクリックします。
5. レコードトリガーフローが作成されるので、オブジェクトに「ケース」を選択します。
6. 次に、開始と終了ブロックの間にある「+」アイコンをクリックし、アクションの要素を追加します。
7. アクションを検索のサイドバーが表示されるので、先ほど作成したApexを選択します。
8. 任意の表示ラベル、API参照名を入力したら、Apexに渡す値を下の画像のように設定します。
最後に画面右上の保存→有効化ボタンをクリックすればフロー作成完了です!
フローでは、必要なトリガー、変数のマッピング、Apex アクションの呼び出しなども直感的に設定できるため、非エンジニアでも運用に組み込みやすいと思います。
これで、「ケース作成 → 担当者特定 → メール通知」の一連の流れが完全に自動化されました!
テスト
それでは、実際に動作確認してみましょう。
Salesforceで取引先と取引先責任者を指定してケースを新規作成します。
保存後、最寄りのサービステリトリーメンバーにメールが届いていれば成功です!
もしメールが届かない場合は、こちらの設定などをチェックしてみてください。
Salesforce Code Analyzerを活用しよう
Apex コードを書く際は、セキュリティ、パフォーマンス、保守性にも注意が必要です。
Salesforce 公式の静的解析ツール「Salesforce Code Analyzer」を使えば、コードの問題点を自動で検出できます。
このようなコマンドで、SOQLインジェクションのリスクや無駄な DML 操作などを洗い出せます。
本番環境へデプロイする前に、ぜひ一度チェックしてみてください。
レート制限に注意
最後に、システム運用上の制限(レートリミット)についても触れておきます。
今回のように Apex クラスからメール通知を行う場合、Salesforce 側の1日あたりのメール送信数制限に引っかかる可能性があります。(自分も引っかかりました)
Developer Edition 組織とトライアル期間中に Salesforce を評価している組織では、各ユーザーは 1 日あたり最大 50 人の受信者にメールを送信でき、個々のメールには最大 15 人の受信者を含めることができます。
エディションによってもルールは異なるため、チェックしておきましょう。
また、今回は使用していませんが、APIコール数の上限も存在します。
API使用状況は「設定 → システムの概要」から確認できるため、定期的にチェックして、運用上のトラブルを未然に防ぐようにしましょう。
まとめ
お疲れ様でした!
以上、取引先から最短距離にいる担当者へメール通知する仕組みをSalesforce Apexで実現してみた〜後編〜でした。
後編では、このような内容を扱いました。
-
Salesforceのジオロケーション機能を活用し、最寄りのサービステリトリーを特定
-
ApexクラスをFlowから呼び出し、通知メールを自動送信
-
運用時のメール送信制限など、注意すべきポイント
Salesforceのノーコード・ローコード機能とApexを上手に組み合わせることで、効率的かつ柔軟な業務自動化が可能になります。
この記事が、少しでもみなさんのにお役に立てば幸いです。
ここまでお読みいただき、ありがとうございました!


- 取引先から最短距離にいる担当者へメール通知する仕組みをSalesforce Apexで実現してみた〜後編〜 - 2025-05-28
- 取引先から最短距離にいる担当者へメール通知する仕組みをSalesforce Apexで実現してみた〜前編〜 - 2025-05-23
- Amazon PollyのSSMLを使って日本語のイントネーションを自然にする - 2025-05-07
- [会話型AI活用]Amazon LexとConnectでシーン別にお花をレコメンドする仕組みを作ってみた - 2024-12-23
- FreePBXで留守電メッセージをメール通知するまでの設定手順 - 2024-12-16
【採用情報】一緒に働く仲間を募集しています
