SAP Workflow Serviceの個人的PoC

この記事は [chillSAP 夏の自由研究2021](https://note.com/chillsap/n/n08ef5ec73b4c) の記事として執筆しています。

::: note
当記事ではSAP Build Process Automationの前身であるWorkflow Serviceを使用しています。ワークフローインスタンスのステータス遷移やAPIの使い方など、基本的な部分はBuild Process Automationでも同じです。
:::

# 1. はじめに
SAP BTPのWorkflow Serviceは、[SAP Workflow Management](https://discovery-center.cloud.sap/serviceCatalog/workflow-management?region=all&tab=service_plan)に含まれるサービスの一つです。Workflow Serviceを使うことで、BTP上でユーザの承認を伴うプロセスの実装や、複数システムを連携させてタスクを遂行する、といったことが可能になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/00c4c427-32ff-bbdd-bb9e-5db451d99d77.png)
図：[SAP Workflow Management – Live Process Content](https://blogs.sap.com/2021/03/02/sap-workflow-management-live-process-content/)より引用

SAP Workflow Managementには[Live Process Content Packages](https://api.sap.com/content-type/Workflow/workflowmanagement/packages)と呼ばれる、事前定義されたパッケージが用意されており、これらは設定だけで使うことができます。

一方、独自のワークフローを一から作る場合は以下のようなワークフローエディターでフローを定義していくのですが、こちらはかなり自由度が高く、やりたいことを実現するには知恵と工夫が必要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e8e058eb-cc26-79ed-8c93-43f829a09e0e.png)


# 2. PoCのテーマ
今回のテーマは、**「実際にありそうなワークフローの要件を実現することができるか？」**ということです。
具体的には、以下のような要件です。

- 承認者が後続の承認者を変更、追加する
- 引き戻し：依頼者、または中間承認者がタスクを取り戻して編集し、再申請する

この検証をする前までは、Workflow Serviceではプロセスは一方通行で進むので、特に引き戻しは不可能ではないかと思っていました。
しかし、[API](https://api.sap.com/api/SAP_CP_Workflow_CF/overview)を使ってワークフローのステータスやコンテキストの情報を変更できることがわかり、実現の可能性が見えてきました。

# 3. Workflow Serviceにおけるワークフローのライフサイクル
検証にあたり、まずはワークフローのライフサイクルを確認しておきます。

## 3.1. ワークフローインスタンス
ワークフローを開始すると、ワークフローインスタンスが登録されます。ワークフローインスタンスとは、ワークフローの定義を参照して登録されるワークフローの実体です。


ワークフローインスタンスのステータスは以下のように遷移します。通常目にするステータスは`RUNNING`（実行中）、または`COMPLETED`（完了）です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/dd1e6b84-cf89-cbbf-e5d2-88e3f69dafee.png)
図：[Status Changes for Workflow Instances](https://help.sap.com/viewer/e157c391253b4ecd93647bf232d18a83/Cloud/en-US/696028f3569c42a8a31c4492afeea9a1.html)より引用

[API](https://api.sap.com/api/SAP_CP_Workflow_CF/overview) `/v1/workflow-instances/{workflowInstanceId}`を使うと、ワークフローのステータスを`RUNNING`から`SUSPENDED`または`CANCELED`に変更できます。また、`SUSPENDED`になったワークフローを`RUNNING`に戻すこともできます。

## 3.2. タスクインスタンス
ワークフローが開始しユーザータスクのステップに到達すると、タスクインスタンスが登録されます。これにより、承認者の受信ボックスにタスクが入ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/598534a8-bb02-e6ec-8e10-a765b37fc7fd.png)

タスクのインスタンスのステータスは以下のように遷移します。タスクが受信ボックスにある状態は`READY`、承認、却下などのアクションにより完了すると`COMPLETED`となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b4a208da-48e7-2b7b-7d0f-e43fb5205128.png)
図：[Status Changes for Task Instances](https://help.sap.com/viewer/e157c391253b4ecd93647bf232d18a83/Cloud/en-US/539ab87c451e4c0aa8321d2f226c44d0.html)より引用

[API](https://api.sap.com/api/SAP_CP_Workflow_CF/overview) `/v1/task-instances/{taskInstanceId}`を使うと、タスクのステータスを`COMPLETED`に変更することができます。

# 4. 実現方針
以下の要件をどのように実現するか検討します。

- 承認者が後続の承認者を変更、追加
- 引き戻し

## 4.1. 承認者が後続の承認者を変更、追加
### 4.1.1. 要件の定義
- 承認者が受信ボックスからタスクを開くと、ワークフローに設定された承認者のリストが表示される
- 承認者は、自分より後の承認者を追加または変更することができる

### 4.1.2. 実現方針
ワークフローを以下のように定義することで実現できそうです。

- ワークフローに渡されるコンテキスト（データ）を使って動的に承認者を決定する
- コンテキストはワークフローの開始時に設定し、承認時に上書き可能とする
- 全ての承認ステップが完了するまで、ユーザータスクを繰り返し登録する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/afb8f8f3-985a-5c63-2b12-53e3ec67d4f4.png)

これが実現できる前提には、**先行のタスクが完了しない限り、次のタスクのインスタンスは登録されない**というWorkflow Serviceの特徴があります。このおかげで、次のタスクが開始されるまでは自由に承認者を変更することができます。

## 4.2. 引き戻し
### 4.2.1. 要件の定義
- 依頼者、または中間承認者は、自分の次のステップの承認者が未承認の段階において、申請を一時的に取り下げることができる（これを「引き戻し」と呼ぶ）
- 引き戻しされると、次ステップの承認者の受信ボックスからタスクが消える
- 引き戻ししたユーザーは、申請内容を変更することができる。この際、次ステップの承認者を変更することも可能とする
- 引き戻ししたユーザーが再度申請を行うことにより、ワークフローが再開する

### 4.2.2. 実現方針
引き戻し実現のポイントは、2つあります。

①引き戻しされると、次ステップの承認者の受信ボックスからタスクが消える
これについては、ワークフローのステータスを`SUSPENDED`に変えることで実現できます。

②引き戻ししたユーザーが再度申請を行うことにより、ワークフローが再開する
引き戻しの際に次のステップの承認者を変更するケースと変更しないケースに分けて考えてみます。

a. 次のステップの承認者を変更しないケース
このケースでは、単純にワークフローのステータスを`RUNNING`に戻すことによってワークフローを再開することができます。これにより受信ボックスから消えていたタスクが戻ってきます。

b. 次のステップの承認者を変更するケース
一度登録されたタスクインスタンスはAPIによって中止、取り消しができません。よって、まず実行中のワークフローのステータスを`CANCELED`に変えて中止し、その上で新規のワークフローインスタンスを登録します。2つのワークフローインスタンスは関連を持たないので、承認履歴を新しいワークフローインスタンスのコンテキストに引き継ぐなどの工夫が必要になります。

承認者の変更有無によって方法を変えるのは煩雑なので、b.の方法で統一することにします。

# 5. 検証方法
以下の方法で検証を行います。

- 簡単なワークフローを作成
- APIを使用してワークフローを登録したり、ステータスを変更したりする

## 5.1. 簡単なワークフローを作成
4.1.2.のイメージでワークフローを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fff3d01f-9f94-d78b-8014-4b82127b9ab1.png)

開始時にコンテキストに承認者のリストを受け取る想定としています。

```json:コンテキストのサンプル
{
  "approvalSteps": [{
      "id": "xxxx@gmail.com",
      "isComplete": false,
      "decision": "" 
  },
    {
      "id": "xxxx@gmail.com",
      "isComplete": false,
      "decision": "" 
  }]
}
```

Initializeのスクリプトで次の承認者を決定し、コンテキストの`nextApprover`に設定します。

```javascript:Initialize.js
// find next approver
var approvalSteps = $.context.approvalSteps;
var nextApproverIndex;
for (var i = 0; i < approvalSteps.length; i++) {
    if (!approvalSteps[i].isComplete) {
        nextApproverIndex = i;
        break;
    }
};

var nextApprover = {
    id: approvalSteps[nextApproverIndex].id,
    index: nextApproverIndex
}
$.context.nextApprover = nextApprover;
$.context.isComplete = false;
$.context.decision = "";
```

ユーザータスクの直後のProcessResultのスクリプトで完了済のステップに`isComplete`フラグを設定します。また、却下された場合はそこでプロセス終了とします。

```javascript:ProcessResult.js
//check approval result
var isApproved = false;
if ($.context.decision === "approve" || $.usertasks.usertask1.last.decision === "approve") {
    isApproved = true;
}

//set current approval status to complete
var approvalSteps = $.context.approvalSteps;
var index = $.context.nextApprover.index;
approvalSteps[index].isComplete = true;
approvalSteps[index].decision = isApproved ? "approve" : "reject";
$.context.approvalSteps = approvalSteps;

//check if this is the final step
if(approvalSteps.length === index + 1 || !isApproved) {
    $.context.isComplete = true;
    $.context.finalDecision = approvalSteps[index].decision;
}
```


## 5.2. サービスインスタンスの登録
APIを呼ぶためには、各エンドポイント/メソッドに対応した権限が必要になります。必要な権限は[API Business Hub](https://api.sap.com/api/SAP_CP_Workflow_CF/resource)で確認することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/93e77ee3-27ad-61c7-60d3-151bc9d397b4.png)

必要な権限を持ったサービスインスタンスを作成します。このためにまず、持たせたい権限を設定したJSONファイルを作成します。
※以下のファイルでは今回使用する権限よりも多くを設定しています

```json:authorities.json
{
    "authorities": [
        "WORKFLOW_INSTANCE_START",
        "WORKFLOW_INSTANCE_GET_CONTEXT",
        "WORKFLOW_INSTANCE_GET",
        "TASK_GET",
        "TASK_COMPLETE", 
        "TASK_UPDATE",
        "TASK_GET_CONTEXT",
        "WORKFLOW_INSTANCE_UPDATE_CONTEXT",
        "WORKFLOW_INSTANCE_RETRY_RESUME",
        "WORKFLOW_INSTANCE_SUSPEND",
        "WORKFLOW_INSTANCE_CANCEL",
        "WORKFLOW_INSTANCE_GET_EXECUTION_LOGS"
    ]
}
```
以下のコマンドでワークフローのサービスインスタンスを作成します。

```
cf create-service workflow lite wf_sample -c authorities.json
```

APIを呼ぶために必要なサービスキーを作成します。

```
cf create-service-key wf_sample key1
```
サービスキーを表示します。

```
cf service-key wf_sample key1
```

## 5.3. APIを使用した検証
Postmanを使用してAPIを呼びます。まず、"Authorization"タブで認証の設定をしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/99d2c86e-349b-33f1-b2cc-de9f51434056.png)

|項目  |設定値  |
|---|---|
|Type  |OAuth 2.0  |
|Grant Type  |Client Credentials  |
|Access Token URL  |サービスキーのuaa.url + /oauth/token  |
|Client ID  |サービスキーのuaa.clientid  |
|Client Secret  |サービスキーのuaa.secret  |
上記項目を設定したら、"Get New Access Token"をクリックしてトークンを取得します。


### 5.3.1. 承認者変更、追加の検証
#### 5.3.1.1. ワークフローを開始
以下のリクエストを発行し、ワークフローを開始します。

|項目  |設定値  |
|---|---|
|メソッド  |POST  |
|URL  |`https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances`  |
|ヘッダ  |application/json  |
ボディ

```json
{
	"definitionId": "<ワークフローの定義ID>",
	"context": {
		"approvalSteps": [{
				"id": "xxxx.01@gmail.com",
				"isComplete": false,
				"decision": ""
			},
			{
				"id": "xxxx.03@gmail.com",
				"isComplete": false,
				"decision": ""
			}
		]
	}
}
```
※idはSAP ID Serviceに登録されているメールアドレス

レスポンスの例

```
{
    "id": "acac5e64-ee6f-11eb-ba43-eeee0a9045ab",
    "definitionId": "multilevelapproval",
    "definitionVersion": "31",
    "subject": "multilevelApproval",
    "status": "RUNNING",
    "businessKey": "",
    "parentInstanceId": null,
    "rootInstanceId": "acac5e64-ee6f-11eb-ba43-eeee0a9045ab",
    "applicationScope": "own",
    "startedAt": "2021-07-27T00:14:57.069Z",
    "startedBy": "sb-clone-52964022-f257-4e19-a621-463a969e3e70!b54386|workflow!b10150",
    "completedAt": null
}
```

#### 5.3.1.2. ユーザータスクのインスタンスを取得
以下のリクエストを発行し、ユーザータスクのインスタンスを取得します。

|項目  |設定値  |
|---|---|
|メソッド  |GET  |
|URL  |`https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/task-instances?workflowInstanceId=<ワークフローインスタンスのID>`  |
※ワークフローインスタンスのIDは前のステップのレスポンスに設定された`id`


レスポンスの例

```
[
    {
        "activityId": "usertask1",
        "claimedAt": null,
        "completedAt": null,
        "createdAt": "2021-07-27T00:14:58.022Z",
        "description": null,
        "id": "ad4431b2-ee6f-11eb-ba43-eeee0a9045ab",
        "processor": null,
        "recipientUsers": [
            "xxxx.01@gmail.com
        ],
        "recipientGroups": [],
        "status": "READY",
        "subject": "Multi Level Approval Test",
        "workflowDefinitionId": "multilevelapproval",
        "workflowInstanceId": "acac5e64-ee6f-11eb-ba43-eeee0a9045ab",
        "priority": "MEDIUM",
        "dueDate": null,
        "createdBy": "sb-clone-52964022-f257-4e19-a621-463a969e3e70!b54386|workflow!b10150",
        "definitionId": "usertask1@multilevelapproval",
        "lastChangedAt": "2021-07-27T00:14:58.022Z",
        "applicationScope": "own"
    }
]
```

#### 5.3.1.3. 承認
承認の際に自分の後の承認者を追加することを想定します。このために、コンテキストの`approvalSteps`を変更して渡します。

|項目  |設定値  |
|---|---|
|メソッド  |PATCH  |
|URL  |`https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/task-instances/<タスクインスタンスのID>`  |
|ヘッダ  |application/json  |
※タスクインスタンスのIDは前のステップのレスポンスに設定された`id`

ボディ

```json
{
	"status": "COMPLETED",
	"context": {
		"decision": "approve",
		"approvalSteps": [{
				"id": "xxxx.01@gmail.com",　←現在のステップ
				"isComplete": false,
				"decision": ""
			},
			{
				"id": "xxxx.02@gmail.com",　←追加したステップ
				"isComplete": false,
				"decision": ""
			},
			{
				"id": "xxxx.03@gmail.com",
				"isComplete": false,
				"decision": ""
			}
		]
	}
}
```
このあとユーザータスクを再取得すると、1件目のタスクが`COMPLETED`になり、2件目のタスクができています。2件目の承認者は追加したステップの承認者となっているはずです。
これで、承認者の変更、追加が可能なことが確認できました。

### 5.3.2. 引き戻しの検証
5.3.1.の最後の状態では、ステップ1が承認済、ステップ2の承認者のところにタスクがあります。ここからステップ1の承認者が取り戻しを行い、ステップ2の承認者を変更することを想定した操作を行ってみます。

ステップ2の承認者の受信ボックス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f1a18dc6-1c05-aefc-a8a7-ae37ad609605.png)


##### 5.3.2.1 ワークフローを一時停止
ワークフローのステータスを`SUSPENDED`に変更し、ワークフローを一時停止します。

|項目  |設定値  |
|---|---|
|メソッド  |PATCH  |
|URL  |`https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances/<ワークフローインスタンスのID>`  |

ボディ

```json
{
  "status": "SUSPENDED",
  "cascade": false
}
```

この結果、受信ボックスからタスクが消えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/eb89394b-960d-7134-ff6d-ac7b93560aab.png)

このあと、ステップ2の承認者を変更し、再申請を行ったと想定します。

##### 5.3.2.2 ワークフローをキャンセル
ワークフローのステータスを`CANCELED`に変更し、ワークフローをキャンセルします。
※SUSPENDEDを経ずにキャンセルしてもよいのですが、「取戻ししたけどやっぱりやめる」ケースもあるかもしれないので一応SUSPENDEDを入れています

|項目  |設定値  |
|---|---|
|メソッド  |PATCH  |
|URL  |`https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances/<ワークフローインスタンスのID>`  |

ボディ

```json
{
  "status": "CANCELED",
  "cascade": false
}
```

##### 5.3.2.3 キャンセルしたワークフローのコンテキストを取得
以下のリクエストを発行し、キャンセルしたワークフローのコンテキストを取得します。

|項目  |設定値  |
|---|---|
|メソッド  |GET  |
|URL  |`https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances/<ワークフローインスタンスのID>/context`  |

レスポンスの例

```
{
    "approvalSteps": [
        {
            "decision": "approve",
            "id": "xxx.01@gmail.com",
            "isComplete": true
        },
        {
            "decision": "",
            "id": "xxx.02@gmail.com",
            "isComplete": false
        },
        {
            "decision": "",
            "id": "xxx.03@gmail.com",
            "isComplete": false
        }
    ],
    "decision": "",
    "nextApprover": {
        "id": "xxx.02@gmail.com",
        "index": 1.0
    },
    "isComplete": false
}
```

##### 5.3.2.4 新規ワークフローを開始
新規のワークフローを開始します。この際、承認済のステップについては前のワークフローから引き継ぐようにします。ここではステップ2、3の承認者を入れ替えました。

|項目  |設定値  |
|---|---|
|メソッド  |POST  |
|URL  |`https://api.workflow-sap.cfapps.eu10.hana.ondemand.com/workflow-service/rest/v1/workflow-instances`  |
|ヘッダ  |application/json  |
ボディ

```json
{
	"definitionId": "multilevelapproval",
	"context": {
		"approvalSteps": [{
				"decision": "approve", ←承認済のステップ（引継ぎ）
				"id": "xxx.01@gmail.com",
				"isComplete": true
			},
			{
				"decision": "",
				"id": "xxx.03@gmail.com", ←承認者変更
				"isComplete": false
			},
			{
				"decision": "",
				"id": "xxx.02@gmail.com", ←承認者変更
				"isComplete": false
			}
		]
	}
}
```

この結果、ワークフローがステップ2の承認者から開始します。ワークフローのインスタンスは分かれていますが、ユーザからは同じワークフローとして見えると思います。

# 6. まとめ
ワークフローの設計、およびAPIでのステータス更新によって、以下の要件が実現できそうなことがわかりました。

- 承認者が後続の承認者を変更、追加
- 引き戻し：依頼者、または中間承認者がタスクを取り戻して編集し、再申請

ただし、実際に使う場合はワークフローだけでは完結せず、アプリ側でのコントロールも必要になります。
たとえば、承認者の追加、変更では、すでに承認済のステップを変更したり、自分の前に承認ステップを追加したりできないような制御が必要です。
引き戻しに関しては、ワークフローインスタンスが複数登録されることになるので、ユーザーからは一つのワークフローに見えるように、新規インスタンスの登録時に履歴を引き継ぐ工夫が必要です。履歴はコンテキストに持ってもいいですが、アプリケーションからの利用のしやすさを考えるとテーブルに格納したほうがよいかもしれません。