【Apex】承認プロセスのApex

Salesforceの承認プロセスは
- ProcessInstance
- ProcessInstanceWorkitem
- ProcessInstanceNode
- ProcessInstanceStep
- ProcessInstanceHistory

というオブジェクト群で構成されているそうです。
ApexでこれらのレコードをSOQLで取得するのは可能ですが、作成・更新は不可になっています。
代わりに
- [ProcessRequest クラス](https://developer.salesforce.com/docs/atlas.ja-jp.234.0.apexcode.meta/apexcode/apex_ProcessRequest.htm)
- [ProcessResult クラス](https://developer.salesforce.com/docs/atlas.ja-jp.234.0.apexcode.meta/apexcode/apex_ProcessResult.htm)
- [ProcessSubmitRequest クラス](https://developer.salesforce.com/docs/atlas.ja-jp.234.0.apexcode.meta/apexcode/apex_ProcessSubmitRequest.htm)
- [ProcessWorkItemRequest クラス](https://developer.salesforce.com/docs/atlas.ja-jp.234.0.apexcode.meta/apexcode/apex_ProcessWorkitemRequest.htm)

を使用するのですが、ちょっとコツがいるので書き残しておきます。
申請と承認と却下をApexで書いていきます。

# 承認プロセスの仕様
適当に1ステップのみの承認プロセスを用意しました。
オブジェクト　：商談
一意の名前　　：OpportunityApprovalProcess
承認者　　　　：申請者のマネージャー
承認アクション：商談.フェーズを「完了」にする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/c3300885-0c9c-4804-abdb-7af4091e289f.png)

# ソースコード
今回はテストクラスに書いてます。2パターン用意しました。
テストユーザ:ぴえが申請し、テストユーザ:まねーじゃーが承認・却下をします。

### パターン1_申請と承認、申請と却下が同メソッド内
申請と承認・却下を同メソッド内に書いてます。
setup()はその他に必要なテストデータを作成しているだけなので、無視でOKです。

```apex:    
    @TestSetup 
    static void setup(){
        // プロファイルを取得
        Profile p = [SELECT Id FROM Profile WHERE Name = 'システム管理者'];
        
        // マネージャーを作成
        User manager = new User(
            LastName = 'まねーじゃー',
            Alias = 'まねーじゃー',
            Email = 'manager@pie.com',
            UserName = 'manager@pie.com',
            EmailEncodingKey = 'ISO-2022-JP',
            TimeZoneSidKey = 'Asia/Tokyo',
            LocaleSidKey = 'ja_JP',
            LanguageLocaleKey = 'ja',
            profileId = p.Id
        );
        insert manager;        
        
        // ユーザを作成
        User pie = new User(
            LastName = 'ぴえ',
            Alias = 'ぴえ',
            Email = 'pie@pie.com',
            UserName = 'pie@pie.com',
            EmailEncodingKey = 'ISO-2022-JP',
            TimeZoneSidKey = 'Asia/Tokyo',
            LocaleSidKey = 'ja_JP',
            LanguageLocaleKey = 'ja',
            profileId = p.Id,
            managerId = manager.Id
        );
        insert pie;
        
        // 取引先を作成
        Account acc = new Account(
            Name = 'テスト取引先'
        );
        insert acc;
        
        // 商談を作成
        Opportunity opp = new Opportunity(
            Name = '受付未承認',
            AccountId = acc.Id,
            StageName = '新規',
            CloseDate = Date.Today()
        );
        insert opp; 
     
    }
    
    @isTest    
    static void Approve() {
        // マネージャーを取得
        User manager = [SELECT Id FROM User WHERE UserName = 'manager@pie.com' LIMIT 1];
        // ユーザを取得
        User pie = [SELECT Id FROM User WHERE UserName = 'pie@pie.com' LIMIT 1];

        // 商談を取得
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];
        
        // 申請
        Approval.ProcessSubmitRequest submit =  new Approval.ProcessSubmitRequest();
        submit.setObjectId(opp.id);
        submit.setSubmitterId(pie.Id); 
        submit.setComments('申請ぴえ！');
        submit.setProcessDefinitionNameOrId('OpportunityApprovalProcess');
        Approval.ProcessResult result = Approval.process(submit);
        
        // 承認
        Approval.ProcessWorkitemRequest workItemApprove = new Approval.ProcessWorkitemRequest();
        workItemApprove.setWorkitemId(result.getNewWorkitemIds().get(0));
        workItemApprove.setAction('Approve');
        workItemApprove.setComments('承認じゃー');
                
        Test.startTest();
        system.runAs(manager){
            result = Approval.process(workItemApprove);
        }
        Test.stopTest();
        
        // 検証        
        System.assertEquals('Approved',result.getInstanceStatus());
    }
    
    @isTest    
    static void Reject() {
        // マネージャーを取得
        User manager = [SELECT Id FROM User WHERE UserName = 'manager@pie.com' LIMIT 1];
        // ユーザを取得
        User pie = [SELECT Id FROM User WHERE UserName = 'pie@pie.com' LIMIT 1];

        // 商談を取得
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];

        // 申請        
        Approval.ProcessSubmitRequest submit =  new Approval.ProcessSubmitRequest();
        submit.setObjectId(opp.id);
        submit.setSubmitterId(pie.Id); 
        submit.setComments('申請ぴえ！');
        submit.setProcessDefinitionNameOrId('OpportunityApprovalProcess');
        Approval.ProcessResult result = Approval.process(submit);
        
        // 却下
        Approval.ProcessWorkitemRequest workItemReject = new Approval.ProcessWorkitemRequest();
        workItemReject.setWorkitemId(result.getNewWorkitemIds().get(0));
        workItemReject.setAction('Reject');
        workItemReject.setComments('却下じゃー');
                
        Test.startTest();
        system.runAs(manager){
            result = Approval.process(workItemReject);
        }
        Test.stopTest();
        
        // 検証        
        System.assertEquals('Rejected',result.getInstanceStatus());
    }  
```
テストなどで同メソッド内で申請・承認、申請・却下がある場合は上記の方法が楽だと思います。

申請時に``submit.setProcessDefinitionNameOrId('OpportunityApprovalProcess');``で、承認プロセスを指定してあげます。
オブジェクトに承認プロセスが一つだけであれば自動で設定されるようですが、設定しないに越したことはないです。

``workItemReject.setWorkitemId(result.getNewWorkitemIds().get(0));``では
申請のProcessInstanceWorkItemを、流れるように承認や却下に渡たすことができます。楽。

### パターン2_申請と承認、申請と却下が別メソッド
setup()で申請を出し、テストメソッドで承認・却下を書いてます。

```apex:
    @TestSetup 
    static void setup(){
        // プロファイルを取得
        Profile p = [SELECT Id FROM Profile WHERE Name = 'システム管理者'];
        
        // マネージャーを作成
        User manager = new User(
            LastName = 'まねーじゃー',
            Alias = 'まねーじゃー',
            Email = 'manager@pie.com',
            UserName = 'manager@pie.com',
            EmailEncodingKey = 'ISO-2022-JP',
            TimeZoneSidKey = 'Asia/Tokyo',
            LocaleSidKey = 'ja_JP',
            LanguageLocaleKey = 'ja',
            profileId = p.Id
        );
        insert manager;        
        
        // ユーザを作成
        User pie = new User(
            LastName = 'ぴえ',
            Alias = 'ぴえ',
            Email = 'pie@pie.com',
            UserName = 'pie@pie.com',
            EmailEncodingKey = 'ISO-2022-JP',
            TimeZoneSidKey = 'Asia/Tokyo',
            LocaleSidKey = 'ja_JP',
            LanguageLocaleKey = 'ja',
            profileId = p.Id,
            managerId = manager.Id
        );
        insert pie;
        
        // 取引先を作成
        Account acc = new Account(
            Name = 'テスト取引先'
        );
        insert acc;
        
        // 商談を作成
        Opportunity opp = new Opportunity(
            Name = '受付未承認',
            AccountId = acc.Id,
            StageName = '新規',
            CloseDate = Date.Today()
        );
        insert opp; 
        
        // 申請
        Approval.ProcessSubmitRequest submit =  new Approval.ProcessSubmitRequest();
        submit.setObjectId(opp.id);
        submit.setSubmitterId(pie.Id); 
        submit.setComments('申請ぴえ！');
        submit.setProcessDefinitionNameOrId('OpportunityApprovalProcess');
        Approval.process(submit);
    }

    @isTest
    static void Approve() {
        // マネージャーを取得
        User manager = [SELECT Id FROM User WHERE UserName = 'manager@pie.com' LIMIT 1];
        
        // 商談を取得
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];
        
        // 申請を取得
        ProcessInstanceWorkItem piwi = [SELECT Id FROM ProcessInstanceWorkItem WHERE ProcessInstance.TargetObjectId = :opp.Id ORDER BY CreatedDate DESC LIMIT 1];
        
        // 承認
        Approval.ProcessWorkitemRequest workItemApprove = new Approval.ProcessWorkitemRequest();
        workItemApprove.setWorkitemId(piwi.Id);
        workItemApprove.setAction('Approve');
        workItemApprove.setComments('承認じゃー');
        
        Approval.ProcessResult result;        
        Test.startTest();
        system.runAs(manager){
            result = Approval.process(workItemApprove);
        }
        Test.stopTest();
        
        // 検証        
        System.assertEquals('Approved',result.getInstanceStatus());
    }

    @isTest    
    static void Reject() {
        // マネージャーを取得
        User manager = [SELECT Id FROM User WHERE UserName = 'manager@pie.com' LIMIT 1];
        
        // 商談を取得
        Opportunity opp = [SELECT Id FROM Opportunity LIMIT 1];
        
        // 申請を取得
        ProcessInstanceWorkItem piwi = [SELECT Id FROM ProcessInstanceWorkItem WHERE ProcessInstance.TargetObjectId = :opp.Id ORDER BY CreatedDate DESC LIMIT 1];
        
        // 却下
        Approval.ProcessWorkitemRequest workItemReject = new Approval.ProcessWorkitemRequest();
        workItemReject.setWorkitemId(piwi.Id);
        workItemReject.setAction('Reject');
        workItemReject.setComments('却下じゃー');
        
        Approval.ProcessResult result;        
        Test.startTest();
        system.runAs(manager){
            result = Approval.process(workItemReject);
        }
        Test.stopTest();
        
        // 検証        
        System.assertEquals('Rejected',result.getInstanceStatus());
    }
```
業務ロジックなど、申請と別タイミングで承認・却下をする処理の場合は
SOQLで申請のProcessInstanceWorkItemが取得する必要があります。
``WHERE ProcessInstance.TargetObjectId = :opp.Id ORDER BY CreatedDate DESC LIMIT 1``　
のように、申請を出したオブジェクトレコードを条件に入れたり、ソートしたりしてうまく取って来れたら、
あとの流れはパターン１と同じです。

# 参考
[ワークフロー、承認に関するオブジェクト](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_erd_process.htm)
[ProcessInstance](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_objects_processinstance.htm
)
[ProcessInstanceWorkitem](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_objects_processinstanceworkitem.htm)
[ProcessInstanceNode](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_objects_processinstancenode.htm)
[ProcessInstanceStep](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_objects_processinstancestep.htm)
[ProcessInstanceHistory](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_objects_processinstancehistory.htm)