freeeの申請で特定申請のみをSlack通知して楽に情報把握したい！

こんにちは！
この記事はアドベントカレンダー[freee APIで業務を楽しく便利にハックしよう！2021](https://qiita.com/advent-calendar/2021/freee-api)の1日目の記事になります！


## 課題 / やりたいこと

### 課題

- 情報把握のために特定範囲の申請内容を見たい (※特定範囲 = 自身が管理する組織/人の内容など)
- freeeに[共有機能](https://support.freee.co.jp/hc/ja/articles/900002144686-%E7%94%B3%E8%AB%8B%E5%86%85%E5%AE%B9%E3%82%92%E4%BB%96%E3%81%AE%E3%83%A1%E3%83%B3%E3%83%90%E3%83%BC%E3%81%A8%E5%85%B1%E6%9C%89%E3%81%99%E3%82%8B)があるが、画面上から都度共有を行ってもらう必要があるが手間で共有を忘れることがある
- 共有可能なfreeeの機能として[Slack連携](https://support.freee.co.jp/hc/ja/articles/115005314383-Slack%E3%81%A8freee%E4%BC%9A%E8%A8%88%E3%82%92%E9%80%A3%E6%90%BA%E3%81%97%E3%81%A6%E7%B5%8C%E8%B2%BB%E7%B2%BE%E7%AE%97%E3%81%AA%E3%81%A9%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E9%80%9A%E7%9F%A5%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%82%8B#4)がありますが、こちらは全申請内容が流れてしまうため目的に合致しない

### やりたいこと

WebHook + APIを活用して、把握する必要がある申請内容のみをSlackに通知することで申請内容をリアルタイムで共有を行う！

例) エンジニアのリーダー層には、エンジニアが申請した内容を把握しておいてほしい等(エンジニア以外の申請は見えて欲しくない)

![2021-11-30_19h56_38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324647/3c184480-38b2-3e13-cd43-da479abbc981.png)


## 使う技術

- [API](https://developer.freee.co.jp/docs/accounting/reference)
    - [各種申請API](https://developer.freee.co.jp/docs/accounting/reference#operations-tag-Approval%20requests)
    - [ユーザーAPI](https://developer.freee.co.jp/docs/accounting/reference#/Users)
- [各種申請WebHook](https://developer.freee.co.jp/docs/accounting/webhook)
- Slack通知
- lambda
- Dynamodb

※注意1 : freee会計のWebhookはまだβ版となります
※注意2 : AWSの部分は省略してます 

---

## STEP1 : freeeアプリ作成

https://developer.freee.co.jp/tutorials/starting-api

基本的にはこちらを参考にすれば問題なく進めると思います。

### 権限設定

権限設定に関しては、参照のみで以下を設定しました。

- [会計] 各種申請 : 参照
- [会計]（ログインユーザー以外の）ユーザー関連情報 : 参照


### Webhook設定

通知を送信する条件に関して、作成のみで以下を設定しました。

- 各種申請 : 申請の作成


### 注意点

申請情報やユーザー情報にアクセス可能な人が、アプリ作成を行う必要があります。


## STEP2 : トークン取得

https://developer.freee.co.jp/tutorials/getting-access-token

こちらを参考にすれば問題なく進めると思います。

### アクセストークン / リフレッシュトークンについて

アクセストークン/リフレッシュトークンは、 `24時間` で有効期限が切れるので、リフレッシュトークンを使って定期的にトークンの再取得を行ってください。

```javascript:トークン取得サンプル
async function getToken()
{
    // Dynamodb等に保存しておく
    let tokenData = {
        'access_token': 'クライアントID',
        'expires_in' : 86400,
        'refresh_token': 'クライアントシークレット',
        'created_at': 1638251549
    };

    const expireTime = tokenData.created_at + tokenData.expires_in;
    const now = dayjs();

    if (now.unix() < expireTime) {
        // トークン有効期限内 (実際にはギリギリを狙わずに余裕を持つ方がいいと思います)
        return tokenData.access_token;
    }

    const params = {
        grant_type: 'refresh_token',
        client_id: 'クライアントID',
        client_secret: 'クライアントシークレット',
        refresh_token: tokenData.refresh_token,
        redirect_uri: 'リダイレクトURI'
    };
    const post = bent('https://accounts.secure.freee.co.jp/', 'POST', 'json', 200);
    const response = await post('public_api/token', params);


    // この値をDynamodb等に保存しなおしておく
    tokenData = {
        'access_token': response.access_token,
        'expires_in' : response.eexpires_in,
        'refresh_token': response.refresh_token,
        'created_at': response.created_at
    };
```

## STEP3 : WebHookを受け取る

https://developer.freee.co.jp/docs/accounting/webhook

### リクエストのチェック

リクエストがfreeeのWebHookからのアクセスか、ヘッダーに含まれている`x-freee-token` と 検証用トークンが同一かをチェックする
※検証用トークンは、アプリ設定画面のWebhook設定画面にあります

```javascript:Webhookアクセスチェック
function checkFreeeAccess(headerToken)
{
    const VERIFICATION_TOKEN = '検証用トークン';

    if (VERIFICATION_TOKEN == headerToken) {
        return true;
    } else {
        return false;
    }
}
```

## STEP4 : Webhookからのリクエスト内容

リクエストのサンプル以下になります。

```json
{
    "id": "固有ID",
    "application_id": "アプリID",
    "resource": "リクエスト種別の判別用？",
    "action": "処理内容",
    "created_at": "時間",
    "approval_request": {
        "id": "申請ID",
        "company_id": "企業ID",
        "status": "申請のステータス",
        "applicant_id": "ユーザーID"
    }
}
```

このリクエスト内容のうち以下を活用してます。

| キー  | 内容  |
|---|---|
| company_id | APIをコールするのに利用します |
| approval_request.status | 以下を判断することができます<br>下書き修正のたびに通知が来ると邪魔なのでin_progress(申請中)のみ通知が来るように分岐をした方がいいと思います。<br><br>draft(下書き)<br>in_progress(申請中)  |
| approval_request.id | 後ほど申請内容の詳細をAPIから取得します |
| approval_request.applicant_id | 後ほど申請者の情報をAPIから取得します |


## STEP5 : 申請内容を取得

https://developer.freee.co.jp/docs/accounting/reference#/Approval%20requests/get_approval_request


こちらを参考にすれば問題なく進めると思います。

※申請フォームが複雑な場合は、request_itemsの入力種別(type)に応じて、入力値が金額なのか、日付なのか、ファイルなのかなどで処理を分岐する必要があります

| キー  | 内容  |
|---|---|
| approval_request_form.parts | フォームの内容が入っています |
| approval_request_form.request_items | フォームに入力された内容が入ってます<br>注意点は、typeに応じて処理内容を分岐する必要があります |
| form_id | 今回利用してないですが、formの種類を判別できるようです |

```javascript:申請内容取得
async function getApprovalRequest(token, id, companyId)
{
    const params = {
    };
    const headers = {
        'Authorization': 'Bearer ' + token
    };
    const get = bent('https://api.freee.co.jp/', 'GET', 'json');
    const response = await get('api/1/approval_requests/' + id + '?company_id=' + companyId, params, headers);

    const requestItems = response.approval_request.request_items;
    let index = 0;
    let dataList = [];
    for(let v of response.approval_request.approval_request_form.parts) {
        let value = requestItems[index].value;
        if (requestItems[index].type == 'amount') {
            // ※typeに応じて処理内容を分岐する必要がある
            // ※この場合は、金額の入力であればカンマ区切りを行う
            value = Number(value).toLocaleString();
        }

        dataList.push({
            label: v.label,
            value: value
        });
        index++;
    }

    return dataList;
}
```

## STEP6: ユーザー情報を取得

https://developer.freee.co.jp/docs/accounting/reference#/Users/get_users

こちらを参考にすれば問題なく進めると思います。
※ユーザーを全件取得するので、`キャッシュの検討` や `レコード件数の上限を超えた場合にどうするか` などの検討が必要です
※名前を取得するだけですので、人数が少ない場合/固定されている場合は、わざわざAPIを叩く必要がないかもしれません

```javascript:申請者名の取得
async function getUserName(token, companyId, userId)
{
    const params = {
    };
    const headers = {
        'Authorization': 'Bearer ' + token
    };
    const get = bent('https://api.freee.co.jp/', 'GET', 'json');
    const response = await get('api/1/users?company_id=' + companyId, params, headers);

    let name = '';
    for (let v of response.users) {
        if (v.id == userId) {
            // 全件取得して、ユーザーIDが一致する人を取り出す
            name = v.display_name
        }
    }
    
    return name;
}
```

## STEP7: Slack通知

Slackの特定チャンネルに通知するだけです。

```
async function sendSlack(userId, list)
{
    let blocks = [];

    for (v of list) {
        blocks.push({
            type: 'section',
            text: {
                type: 'mrkdwn',
                text: "*" +v.label +"*\n" + "　" + v.value
            }
        });
    }

    // @申請者のIDに応じてSLACK_WEBHOOK_URLのURLを分岐します
    // @特定チームの場合、特定業種の場合など

    const url = "SLACK_WEBHOOK_URL";
    const webhook = new IncomingWebhook(url);
    await webhook.send({
        text: '新しい申請が登録されました',
        blocks: blocks
    });
}
```

### 申請が上がったタイミングで以下の通知をSlackのチャンネルに共有します

![2021-11-30_18h45_31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324647/b8c99e5a-c0b9-7e00-06ed-fdeded6f78c3.png)


## 明日の記事

明日は、@hagurin の「CakePHPでfreeeAPIを叩くラッパーを作成した話」の話です