Go言語でMentaの売上をfreeeに登録するCLI開発

この記事では教育プラットフォームであるMentaの売上情報をダウンロードし、売上情報として会計サービスfreeeのAPIを利用し登録する流れを解説したいと思います。まだ連携先として追加されていない他のECサービス・収益化が行える配信サイト等を利用している方の参考にもなるかと思います。

Mentaとはプログラミング技術などを教える人・学習する人をマッチングするサイトです。私も1年以上メンターを行っており、累計50人近くの方に学習のサポートを行っています。現在調べてみたところ、このサイトの収益情報を自動登録できる会計ソフトは存在しないようなので、APIを通じてfreeeへ取引を登録するための簡単なCLIを作成しました。

https://menta.work/user/13264

成果物はこちらに掲載しています。

https://github.com/kamijin-fanta/freee-menta-importer


## freeeでの収支の入力方法を理解する

Mentaは入金から30日以上経過した売上について出金の申請が行なえます。契約者毎に支払タイミングが異なり、毎月支払いが行われるので手動で入力するのはかなり手間です。売上が発生したタイミングでは会計ソフト上で売上を登録します。売上金については一旦Mentaへ銀行口座のように貯められるものと扱い、出金を行ったタイミングでMentaの口座から自分の口座に振替を行うという風に扱います。

売上に対しての手数料は20%に対して消費税を加算した22%で、出金の手数料は1回あたり300円必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61226/501f8cb4-5ade-7d0b-c021-f8662ad4a789.png)

参考までにMenta上でダウンロードできる売上情報はこのようなCSVフォーマットでダウンロード可能です。

```csv
契約No,日付,ユーザー名,プラン名,招待割引,キャンセル,金額,手数料,売上額,出金日
11111,2021-09-24,Example User,"プラン名",,,1000,220,780,"2021-11-10 12:34:56"
22222,2021-10-24,Example User,"プラン名",,,1000,220,780,
```

取引入力の前に、Menta上の残高を登録するための口座を作成する必要が有ります。「決済サービス・電子マネーを登録する」→「利用しているPOSレジ・ESサイト（出店）・決済サービスが見つからない場合」と遷移することで、手動で入力が可能な空の口座が作成できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61226/68c8874d-29b1-b372-9576-8d873dd3ae45.png)

freeeでの収支入力の例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61226/368168f4-71bc-ea36-ef6f-0db3c268e491.png)

今回の例は1000円の契約に対しての売上があり、それに対して220円の手数料が運営側に差し引かれています。収入で売上高として1000円を入力し、マイナス収入として手数料の220円を差し引いています。そして、その合計金額780円と同額をMentaの口座で受け取りします。また、備考欄にてMentaでの注文IDを記述しています。

これから取引情報をAPI経由で投げ込んでいきます。一旦Web画面から入力しておくと、Developer Toolsでの通信内容などを覗くことで、APIで投げるJSONペイロードの形がわかりやすくなるのでオススメです。


## freeeでOauth2認証を行う

APIを呼び出すまでの手順はこちらで解説されています。開発者アカウントは無料で登録することができ、一定期間毎にデータが消去されるようです。登録が完了すると事業者IDとアクセストークンが画面に表示されます。

freee API スタートガイド https://developer.freee.co.jp/getting-started

しかしながら、今回は開発者アカウントではなく個人事業主として作成した事業所でのAPI呼び出しを行いたいです。この場合はスタートガイドに記述されていない方法を利用する必要がありました。アカウント作成時に表示されたアクセストークンは一定時間で失効し、テスト環境の事業所でしか使えません。実際に本番利用するためには、Oauth2認証を利用し、ClientID,ClientSecretからアクセストークンを取得する必要があります。具体的には以下の方法があります。

1. サーバサイドでの標準的な方法。HTTPエンドポイントを立てて、そこにコールバックを受けてアクセストークンを取得。
1. 組み込みブラウザ等で認証を行う際の方法。認証後の画面に直接アクセストークンが表示され、それをコピペ/自動読み取り等で認識を行う。リダイレクトURLに `urn:ietf:wg:oauth:2.0:oob` を指定する。 https://app.secure.freee.co.jp/developers/tutorials/3

今回は手元で動くCLIなのでどちらの方法でも構いませんが、より利便性が高くなるように1番の方法で、ローカルサーバを立ててアクセストークンを取得する方法を採用しました。以下のような流れでアクセストークンを取得することができます。’

1. `http://localhost:9999/` でサーバを起動
1. ブラウザで `https://accounts.secure.freee.co.jp/public_api/authorize` へアクセス　その際GETパラメータの `redirect_uri=http://localhost:9999/` をつける
1. freeeのサイト上でログイン等を行う
1. `http://localhost:9999/` にリダイレクトされる　その際GETパラメータに `code` が付いている
1. CLIが `https://accounts.secure.freee.co.jp/public_api/token` へGETリクエストを投げる　その際GETパラメータに `code` をつけるとレスポンスでアクセストークンが受け取れる

freeeのOauthアプリケーションの作成方法はこちらで紹介されています。
https://app.secure.freee.co.jp/developers/tutorials/2

Oauth2の設定 `oauth2.Config` とサーバの待受ポートを渡すことで、HTTPサーバの起動からアクセストークンの取得までを行うコードはこちらです。再利用性が良さそうなので外から使えそうな形にしました。(実際のコードでは攻撃を防ぐためのstateの検証・エラーハンドリング等を行っていますが簡略化したものを掲載しています。)

```go:oauth2.go
func Oauth2AuthorizeLocalServer(config *oauth2.Config, port int) (*oauth2.Token, error) {
	ctx := context.Background()

	// redirect to authorized page
	url := config.AuthCodeURL(state)
	open.Start(url) // ブラウザが起動する

	// ローカルサーバの起動・認証コードの受け取り
	code, _ := func() (string, error) {
		l, _ := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
		defer l.Close()
		quit := make(chan string)
		go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			query := req.URL.Query()
			w.Write([]byte(`auth success`))
			w.(http.Flusher).Flush()
			quit <- query.Get("code")
			
		}))
		return <-quit, nil
	}()

	oauth2Token, err := config.Exchange(ctx, code)
	return oauth2Token, err
}
```

```go:利用例
func main () {
	oauth2Config := &oauth2.Config{
		ClientID:     os.Getenv("FREEE_CLIENT_ID"),
		ClientSecret: os.Getenv("FREEE_CLIENT_SECRET"),
		Endpoint: oauth2.Endpoint{
			AuthURL:  "https://accounts.secure.freee.co.jp/public_api/authorize",
			TokenURL: "https://accounts.secure.freee.co.jp/public_api/token",
		},
		RedirectURL: "http://localhost:9999/callback",
	}}
	token, _ := Oauth2AuthorizeLocalServer(oauth2Config, 9999)
	log.Println(token)
}
```

https://github.com/kamijin-fanta/freee-menta-importer/blob/master/oauth2.go

受け取った `oauth2.Token` 構造体にはAccess TokenやRefresh Tokenが入っているので、これを永続化して再利用すればログインを省略することができます。今回は `json.Encoder` を利用して `secret.json` に保存しました。


## Goからfreee APIの呼び出し

対応APIは一部だけですが、Go言語向けのクライアントライブラリがサードパーティによって提供されています。今回はこれを使います。対応していないエンドポイントを利用したい場合は、 https://qiita.com/kamijin_fanta/items/67dfa90675f0d66c9670 で紹介したようにOpenAPI定義を利用したクライアント生成を行うのがオススメです。

https://github.com/LayerXcom/freee-go

READMEにも記載がありますが、Oauth2のClientID,ClientSecret,AccessToken等が有れば呼び出しが可能です。クライアント生成後には以下のような記述で取引をfreeeに登録することが出来ます。

```go
details := []freee.DealCreateParamsDetails{
	// 取引の詳細
}
payments := []freee.DealCreateParamsPayments{
	// 関連する支払情報
}
client.CreateDeal(ctx, oauth2Token, freee.DealCreateParams{
	IssueDate: "yyyy-mm-dd",
	Type:      "expense",  // 収支区分 (収入: income, 支出: expense)
	CompanyID: companyId,
	Details:   details,
	Payments:  &payments,
})
```

## Mentaの売上情報CSVをパース

先程お見せしましたが、Mentaはダッシュボードから以下のようなフォーマットのCSVを出力できます。

```csv
契約No,日付,ユーザー名,プラン名,招待割引,キャンセル,金額,手数料,売上額,出金日
11111,2021-09-24,Example User,"プラン名",,,1000,220,780,"2021-11-10 12:34:56"
22222,2021-10-24,Example User,"プラン名",,,1000,220,780,
```

Go標準ライブラリの `csv.NewReader` を利用することで簡単にパースを行うことが出来ます。難しいことは行っていないのですがコードは長いのでGitHubのリンクを貼り付けておきます。

パーサ: https://github.com/kamijin-fanta/freee-menta-importer/blob/master/parser.go
パーサのテスト: https://github.com/kamijin-fanta/freee-menta-importer/blob/master/parser_test.go

パースにより以下のような構造体のスライスが得られます。

```go:model.go
type Sale struct {
	ContractNo     string // 契約No
	Date           string // 日付
	UserName       string // ユーザ名
	PlanName       string // プラン名
	Price          int    // 金額
	Fee            int    // 手数料
	Sales          int    // 売上額
	// ...省略
}
```

## Mentaへ取引登録する

MentaのCSVから注文情報が取得できたので、あとは取引の登録をするだけです。先程作成したAPIクライアントで各種パラメータを埋めた構造体を投げます。descriptionに文字列を指定することで備考として確認が可能となるので、契約IDを指定しました。


```go:cmd_sales.go
description := fmt.Sprintf("menta-contract=%s", sale.ContractNo)
details := []freee.DealCreateParamsDetails{
	// 売上の収入登録
	{
		TaxCode:       129,       // sales_with_tax_10: 課税売上10%
		AccountItemID: 429799547, // 売上高
		Amount:        int32(sale.Price),
		Description:   &description,
	},
	// 利用手数料の支出登録
	{
		TaxCode:       136,       // purchase_with_tax_10: 課対仕入10%
		AccountItemID: 429799593, // 販売手数料
		Amount:        int32(sale.Fee) * -1,
	},
}
payments := []freee.DealCreateParamsPayments{
	{
		Amount:             int32(sale.Sales),
		FromWalletableID:   walletableId,
		FromWalletableType: "wallet",
		Date:               sale.Date,
	},
}
client.CreateDeal(ctx, oauth2Token, freee.DealCreateParams{
	IssueDate: date,
	Type:      "income",  // 収支区分 (収入: income, 支出: expense)
	CompanyID: companyId,
	Details:   details,
	Payments:  &payments,
})
```

リクエスト後にfreeeのWeb UIを確認すると正常に登録されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61226/9da02215-8f63-f14b-793f-6caa620f2ee7.png)

今回利用した全体のコードはこちらに掲載していますので、興味があれば御覧ください。また、MITライセンスとしましたのでコードの再利用等も歓迎します。 https://github.com/kamijin-fanta/freee-menta-importer


## 所感

前回のアドベントカレンダーを試した際に便利だと感じたので、今年からは契約を行い個人事業主で実際に利用するユーザとして記事を書かせていただきました。(去年の記事: https://qiita.com/kamijin_fanta/items/67dfa90675f0d66c9670 )

数十件・数百件の売上情報を手入力するのは現実的では無いので、こういった個人でも気軽に利用できる形でAPIが提供されているのは非常に有り難いです。今後も自動入力したいデータが増えたら自作しようと思います。