Freeeのアプリを作成してみる Part 3
根幹なところは昨日実装をしましたので、今日は実際にアプリを利用する際に必要となるほか部分を見ていきたいと思います。 昨日の投稿はコチラです。
まずは事業所を選択してもらう必要があります。なので事業所一覧を表示して、該当の事業所IDをこちらのDBに保存します。
次にトークンが24時間しか有効でないとのことで、有効期限が切れているときにトークンをリフレッシュしてみたいと思います。
事業所の一覧を取得する
では、まず事業所の一覧を取得してみます。事業所の一覧はこのAPIを使います。 https://developer.freee.co.jp/docs/accounting/reference#/Companies/get_companies
実際のコードはこちらになります。
public async Task<FreeeCompaniesResponseModel> GetCompaniesAsync(string userId) { var accessToken = await GetExistingTokenOrRefreshAsync(userId); var companies = new FreeeCompaniesResponseModel(); using (var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.freee.co.jp/api/1/companies")) { request.Headers.TryAddWithoutValidation("accept", "application/json"); request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + accessToken); request.Headers.TryAddWithoutValidation("X-Api-Version", "2020-06-15"); var response = await _httpClient.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); companies = JsonSerializer.Deserialize<FreeeCompaniesResponseModel>(responseString); } return companies; }
では↑のコードを使ったページを作成します。
こちらがView
@using wppReceiptRoller.Areas.Usr.Models @model UsrConnectMidConnectFreeeViewModel @{ ViewData["Title"] = "ConnectFreee"; Layout = "~/Views/Shared/_Layout.cshtml"; ViewData["ShowFooter"] = false; } <div class="container py-3"> <h1 class="font-lg">Freee 連携</h1> <hr /> @if (string.IsNullOrEmpty(Model.User.FreeeToken)) { <div class="py-2"> Freeeと連携する際には連携ボタンをクリックしてFreeeのユーザーIDとパスワードを入力後、ReceiptRollerアプリに対して許諾をお願いいたします。 </div> <a href="https://accounts.secure.freee.co.jp/public_api/authorize?response_type=code&client_id=0270f3a7a5039868018e50c2e60727c54c7134c7d18d776a5fa4de21a9789dca&redirect_uri=https%3A%2F%2Flocalhost%3A44375%2Ffreee%2Fcallback" class="btn btn-primary rounded-0">Freeeと連携</a> } else { <div class="row"> <div class="col-md-6 my-2"> <div class="py-2"> <span class="btn btn-info p-2 rounded-0 w-100">Freee連携済み</span> </div> </div> <div class="col-md-6 my-2"> <h2 class="font-lg"> Freee事務所 </h2> <hr /> <div> 事業所を選択してください。 </div> @foreach (var company in Model.Companies.companies) { <div class="card my-2 card-body"> <div class="py-1 d-flex justify-content-between"> <div> @company.display_name </div> <div> @if (Model.User.FreeeCompanyId == company.id.ToString()) { // Current company <button class="btn btn-info rounded-0 text-dark" type="button" disabled>連携中</button> } else { <form method="post" action="/Usr/Connect/Freee/SetCompany"> @Html.AntiForgeryToken() <input type="hidden" name="id" value="@company.id" /> <button class="btn btn-secondary rounded-0" type="submit">事業所連携する</button> </form> } </div> </div> </div> } </div> </div> } </div>
こちらがControllerです。
[HttpGet, Route("/Usr/Connect/Freee")] public async Task<IActionResult> ConnectFreee() { // Connect user var user = await _userManager.GetUserAsync(User); // var companies = await _freeeHandlers.GetCompaniesAsync(user.Id); var view = new UsrConnectMidConnectFreeeViewModel() { User = user, Companies = companies }; return View(view); }
事業所を選択するページはこのようになります。
これで、選択した事務所に対して画像をファイルボックスに保存することができます。
今回の改修でコア部分のコードも下記のように書き直しました。
public async Task UploadFilebox(string userId, string tid) { var user = await _userManager.FindByIdAsync(userId); // Get file var image = (from i in _db.Images where i.Tid == tid select i).FirstOrDefault(); var myWebClient = new WebClient(); byte[] imageArray = myWebClient.DownloadData(image.BlobUrl); using (var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.freee.co.jp/api/1/receipts")) { request.Headers.TryAddWithoutValidation("accept", "application/json"); request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + user.FreeeToken); request.Headers.TryAddWithoutValidation("X-Api-Version", "2020-06-15"); var multipartContent = new MultipartFormDataContent(); multipartContent.Add(new StringContent(user.FreeeCompanyId), "company_id"); multipartContent.Add(new ByteArrayContent(imageArray), "receipt", tid + ".jpg"); request.Content = multipartContent; var response = await _httpClient.SendAsync(request); } }
トークンをリフレッシュする
トークンの有効期限が24時間とのことで、それを過ぎると新しいトークンを取得する必要があります。 今回は一気に実装したので実際に期限がきれることはありませんでしたが、明日にはもう必要なる箇所です。
新しいトークンは下記のPOSTで取得できます。
POST: https://accounts.secure.freee.co.jp/public_api/token params: grant_type=refresh_token&client_id=UID&client_secret=SECRET&refresh_token=REFRESH_TOKEN
実際のコードはこちらです。 トークンの有効期限をみてすでに期限が過ぎていたら再度取得しに行くようになっています。
public async Task<string> GetExistingTokenOrRefreshAsync(string userId, bool ignoreExpireTime = false) { var user = (from u in _db.Users where u.Id == userId select u).FirstOrDefault(); if (user != null) { if (!string.IsNullOrEmpty(user.FreeeRefreshToken)) { if (!IsTokenValid(user.FreeeTokenExpire) || ignoreExpireTime) { using (var request = new HttpRequestMessage(new HttpMethod("POST"), "https://accounts.secure.freee.co.jp/public_api/token")) { var contentList = new List<string>(); contentList.Add("grant_type=refresh_token"); contentList.Add("client_id=" + _clientId); contentList.Add("client_secret=" + _clientSecret); contentList.Add("refresh_token=" + user.FreeeRefreshToken); request.Content = new StringContent(string.Join("&", contentList)); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded"); var response = await _httpClient.SendAsync(request); if (response.StatusCode != System.Net.HttpStatusCode.OK) { return ""; } else { var responseString = await response.Content.ReadAsStringAsync(); var token = JsonSerializer.Deserialize<FreeeAuthTokenResponseModel>(responseString); // Save token user.FreeeToken = token.access_token; user.FreeeRefreshToken = token.refresh_token; user.FreeeTokenExpire = Helpers.DateTimeHelpers.GetUnixTime(DateTime.Now.AddDays(1)); var result = await _userManager.UpdateAsync(user); return token.access_token; } } } // Your token is OK, so just return it. return user.FreeeToken; } // Then you are not connected to Freee return ""; } // No such user, just return empty string. return ""; }
これでだいぶ完成系に近づいてきましたね。 ここからはユーザーテストをしてもらいリリースに向かって進めたいと思います。 FreeeさんのAPIで実際の経費処理業務までできそうなので、次回はその実装を進めたいと思います。 まずは・・ファイル連携だけでリリースをしてみます。 また気づきがあったらここで共有いたします。