Rubyでboard APIを叩く

# boardとは
https://the-board.jp/
>受託ベンチャーを支えるバックオフィス業務システム

ソース：[boardが目指しているもの〜バックオフィス業務のために起業したのではない](https://the-board.jp/blogs/goal_of_board)

その[公式提供API](https://developers.the-board.jp/doc/index.html)をRubyで叩きます。

# 算出したいデータ
[1] 見積中の案件について、合計額を算出する
→ 見込み売上
見積には「確度低/中/高」という区別がつけられるので、それで仕分けもする

[2] 受注後〜請求前の案件について、合計額を算出する
→ 確定売上

[3] 請求済みの請求について、合計額を算出する
→ 売掛金

出力例

```
見積中(高): ¥  1,000,000
見積中(中): ¥  2,000,000
見積中(低): ¥ 13,000,000
確定売上:   ¥  4,000,000
売掛金:   　¥  5,000,000
```

# APIキー、APIトークンの取得
公式解説：https://the-board.jp/helps/help_common_api
## APIキー
>＜APIキー＞
アカウントで1つ発行され、リクエスト制限のためのリクエスト数は、このAPIキー単位にカウントされます。アカウントの識別キーのため、変更・削除はありません。

### 発行方法
API設定でAPIを有効にすると表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272677/87b0444c-bf40-c824-0038-dd6940cb31a9.png)


## APIトークン
>＜APIトークン＞
任意の数生成することができ、APIトークンごとに可能な操作（例：顧客の新規登録）を定義することができます。
セキュリティのため、可能な操作は最低限に範囲は限定することをお勧めします。そのため、用途に合わせてAPIトークンを登録し、必要な操作のみ有効にしてください。

### 発行方法
まず「新規トークン作成」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272677/98173057-16da-b62b-a793-fcca31967f21.png)


今回必要な
[1] 案件→リストの取得
[2] 請求→リストの取得
の **2つのみ** チェックを入れ、発行。

>APIトークン登録後、生成したトークンが表示されますが、APIトークンは登録直後の一度しか表示されませんので、忘れずに安全な場所に保管してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272677/61e32a17-a365-7701-9697-a28f0a7355e2.png)


とのことなので注意して保存しましょう。

# コードと解説
## httpヘルパ
```ruby:http_request_helper
require 'jwt'
require 'net/http'
require 'openssl'

module HttpRequestHelper
  def get_request(url, headers, params = nil)
    uri = URI.parse(url)
    uri.query = URI.encode_www_form(params) if params
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'
    req = Net::HTTP::Get.new(uri)
    req.initialize_http_header(headers)
    response = http.request(req)

    response
  end

  def post_request(url, headers, params)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'

    req = Net::HTTP::Post.new(uri.path)
    req.body = params.to_json
    req.initialize_http_header(headers)
    response = http.request(req)

    response
  end

  def success?(response_code)
    response_code && (200 <= response_code.to_i && response_code.to_i < 300)
  end
end
```

## 見込み売上
進行中の案件のうち、「見積中(低/中/高)」ステータスのもの。
案件リストを`order_status`が`1,2,3`のものを取得します。
注意して欲しいのは配列的クエリの送信方法です。僕がphpサーバ等でよく見かける形式とは少し違いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272677/f3b63c37-2919-f30f-3228-b55c6db73703.png)



```ruby
require 'http_request_helper'

class BoardReporter
  include HttpRequestHelper
  BOARD_API_BASE = 'https://api.the-board.jp'
  BOARD_API_HEADER = {
    'Authorization' => 'Bearer ' + ENV['BOARD_API_TOKEN'],
    'x-api-key' => ENV['BOARD_API_KEY']
  }
  PER_PAGE = 100

  def run()
    estimated_sales_group = []
    page = 1
    has_next_page = true
    while has_next_page
      result, has_next_page = fetch_estimating_projects(page)
      page += 1
      estimated_sales_group.push(*result)
    end

    high_estimated_total = estimated_sales_group.select { |project| project[:order_status] == 1 }.inject(0) { |total, project| total + project[:total].to_i }.to_s(:delimited)
    midium_estimated_total = estimated_sales_group.select { |project| project[:order_status] == 2 }.inject(0) { |total, project| total + project[:total].to_i }.to_s(:delimited)
    low_estimated_total = estimated_sales_group.select { |project| project[:order_status] == 3 }.inject(0) { |total, project| total + project[:total].to_i }.to_s(:delimited)

    puts "見積中(高): ¥ #{high_estimated_total}"
    puts "見積中(中): ¥ #{midium_estimated_total}"
    puts "見積中(低): ¥ #{low_estimated_total}"
  end

  def fetch_estimating_projects(page) # 見積中 高/中/低, 見積もり確度別の見込み売上
    params = {
      'order_status_in[]' => '1,2,3',
      'per_page' => PER_PAGE.to_s,
      'page' => page
    }
    response = get_request(BOARD_API_BASE + '/v1/projects', BOARD_API_HEADER, params)

    if success?(response.code)
      [
        JSON.parse(response.body).map do |project|
          {
            order_status: project['order_status'],
            project_no: project['project_no'],
            total: project['total']
          }
        end,
        next_page?(page, response)
      ]
    else
      puts '見積中の案件が取得できませんでした'
      [[], false]
    end
  end

  def next_page?(current_page, response)
    response['X-Total-Count'].to_i >= current_page * 100
  end
end
```
## 確定売上
確定売上は少し複雑です。
進行中の案件のうち「受注確定/受注済」ステータスで、かつ請求前のものです。
つまり請求済のものは計上したくないわけですが、boardでは案件と請求は **別々に管理されている** ため、別途請求リストAPIから請求一覧を取得して案件No（`project_no`）別に差し引く必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272677/972b4406-a1b6-f03b-6ebf-d530e6988f52.png)


```ruby
  def run()
    invoiced_sales = []
    page = 1
    has_next_page = true
    while has_next_page
      result, has_next_page = fetch_issued_invoices(page)
      page += 1
      invoiced_sales.push(*result)
    end

    receivable_sales = []
    page = 1
    has_next_page = true
    while has_next_page
      result, has_next_page = fetch_receivable_invoices(page)
      page += 1
      receivable_sales.push(*result)
    end

    billable_total = confirmed_sales.map do |project|
      invoiced_total = invoiced_sales.select { |invoice| invoice[:project_no] == project[:project_no] }.inject(0) { |total, invoice| total + invoice[:total].to_i }
      # 1つの保守案件を毎月請求している場合などはマイナスになるので、ここで止める
      [(project[:total].to_i - invoiced_total), 0].max
    end.inject(0) {|total, billable| total + (billable ? billable : 0) }.to_s(:delimited)

    puts  "確定売上: ¥ #{billable_total}"
  end

  def fetch_ordered_projects(page) # 受注確定/受注済, 確定売上
    params = {
      'order_status_in[]' => '4,5',
      'per_page' => PER_PAGE.to_s,
      'page' => page
    }
    response = get_request(BOARD_API_BASE + '/v1/projects', BOARD_API_HEADER, params)

    if success?(response.code)
      [
        JSON.parse(response.body).map do |project|
          {
            project_no: project['project_no'],
            total: project['total']
          }
        end,
        next_page?(page, response)
      ]
    else
      puts '受注確定/受注済みの案件が取得できませんでした'
      [[], false]
    end
  end

  def fetch_issued_invoices(page) # 請求済/入金済/回収不能, 請求済み。確定売上から案件別に引く
    params = {
      'invoice_status_in[]' => '2,3,9',
      'per_page' => PER_PAGE.to_s,
      'page' => page
    }
    response = get_request(BOARD_API_BASE + '/v1/invoices', BOARD_API_HEADER, params)

    if success?(response.code)
      [
        JSON.parse(response.body).map do |invoice|
          {
            project_no: invoice['project_no'],
            total: invoice['total']
          }
        end,
        next_page?(page, response)
      ]
    else
      puts '請求済みの請求書が取得できませんでした'
      [[], false]
    end
  end
```

## 売掛金
売掛金は簡単です。
請求リストから「請求済」ステータスの請求を取得するだけです。
なお、これは「入金済」を含みません。boardの「請求済」は入金前のみに付くステータスです。
「回収不能」というステータスもあります。面白いですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272677/6e9f892d-7c21-b90e-16cb-607dbd2653f9.png)



```ruby
  def run()
    receivable_sales = []
    page = 1
    has_next_page = true
    while has_next_page
      result, has_next_page = fetch_receivable_invoices(page)
      page += 1
      receivable_sales.push(*result)
    end

    receivable_total = receivable_sales.inject(0) { |total, invoice| total + invoice[:total].to_i }.to_s(:delimited)

    puts "売掛金: ¥ #{receivable_total}"
  end

  def fetch_receivable_invoices(page) # 請求済（入金前）。いわゆる売掛金
    params = {
      'invoice_status_in[]' => '2',
      'per_page' =>PER_PAGE.to_s,
      'page' => page
    }
    response = get_request(BOARD_API_BASE + '/v1/invoices', BOARD_API_HEADER, params)

    if success?(response.code)
      [
        JSON.parse(response.body).map do |invoice|
          {
            project_no: invoice['project_no'],
            total: invoice['total']
          }
        end,
        next_page?(page, response)
      ]
    else
      puts '売掛金情報が取得できませんでした'
      [[], false]
    end
  end
```