[Pay.jp] C#er pay.jp APIをたたく

今日はウェブ決済サービスを提供するpay.jpのAPIをたたいてみます。管理画面からだと取引履歴が10件づつしか見れなく・・不便すぎて今ここにいます。。

オフィシャルドキュメントはこちら
https://pay.jp/docs/api/

# 決済一覧を取得
時間を指定して決済データ一覧を取得してみます。

こちらが決済データ一覧を取得するメソッドのドキュメント
https://pay.jp/docs/api/#%E6%94%AF%E6%89%95%E3%81%84%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%8F%96%E5%BE%97

## リクエスト
API側に渡せるパラメターは、下記。

|名前|タイプ|備考|
|:---|:---|:---|
|imit|Integer|取得するデータ数の最大値(1~100まで)。指定がない場合は 10 となる。|
|offset|Integer|基準点からのデータ取得を行う開始位置。指定がない場合は 0 となる。|
|since|Integer|タイムスタンプ、指定したタイムスタンプ以降に作成されたデータのみ取得|
|until|Integer|タイムスタンプ、指定したタイムスタンプ以前に作成されたデータのみ取得|
|customer|String|絞り込みたい顧客ID|
|subscription|String|絞り込みたい定期課金ID|

## レスポンス

レスポンスオブジェクトは下記

```csharp

public class Card
{
    public string address_city { get; set; }
    public string address_line1 { get; set; }
    public string address_line2 { get; set; }
    public string address_state { get; set; }
    public string address_zip { get; set; }
    public string address_zip_check { get; set; }
    public string brand { get; set; }
    public string country { get; set; }
    public int created { get; set; }
    public string cvc_check { get; set; }
    public object customer { get; set; }
    public int exp_month { get; set; }
    public int exp_year { get; set; }
    public string fingerprint { get; set; }
    public string id { get; set; }
    public string last4 { get; set; }
    public string name { get; set; }
    public string @object { get; set; }
}

public class Datum
{
    public int amount { get; set; }
    public int amount_refunded { get; set; }
    public bool captured { get; set; }
    public int captured_at { get; set; }
    public Card card { get; set; }
    public int created { get; set; }
    public string currency { get; set; }
    public string customer { get; set; }
    public string description { get; set; }
    public object expired_at { get; set; }
    public object failure_code { get; set; }
    public object failure_message { get; set; }
    public string id { get; set; }
    public bool livemode { get; set; }
    public object metadata { get; set; }
    public string @object { get; set; }
    public bool paid { get; set; }
    public object refund_reason { get; set; }
    public bool refunded { get; set; }
    public object subscription { get; set; }
}

public class TransactionList
{
    public int count { get; set; }
    public List<Datum> data { get; set; }
    public bool has_more { get; set; }
    public string @object { get; set; }
    public string url { get; set; }
}
```

# オーソライズ、認証

>PAY.JPのAPIを利用するには、ユーザー登録を行い、APIページからAPIキーを取得してください。 テスト用のキーでは、本番の支払い処理を行うサーバーへは接続されず、実際の支払い情報として計上されることもありません。本番用のキーは、本番申請を行うことで利用できるようになります。

とのことです。

> パブリックキー	HTML内に埋め込むトークン生成用のパブリックキー
> シークレットキー	サーバー側からBasic認証のユーザーネームとして渡すシークレットキー
>通常の認証は、シークレットキーをユーザーネームとして扱い、Basic認証経由で行われます。パブリックキーは、あなたの決済画面のHTML内に組み込む公開用のAPIキーで、クレジットカードのトークンを生成する際に使用します。 シークレットキーは、全てのAPIリクエスト操作が可能となる重要なキーなので、くれぐれ も取扱いにご注意ください。

シークレットキーとパスワードをBasic認証経由で行うとのことで、下記のコードを用意

```csharp
// Set Token
var base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes(token + ":" + pass));
```

# APIを叩いてみる

今回はシンプルにペラアプリケーションとしてHomeコントローラー/Indexビューページに作成しました。一度100件づつしか取得できないので、100以上ある場合はhas_moreがfalseになるまで100件づつ取得しています。あまり大量のデータを取得しようとするとPay.jpへの負担となるかもしれないので利用上の注意が必要です。今回は制限をかけていませんが、なにかしらの制限を設ける事をお勧めします。

```csharp
/// <summary>
/// Get specific result
/// </summary>
/// <param name="token"></param>
/// <param name="pass"></param>
/// <param name="fromDateTime"></param>
/// <param name="toDateTime"></param>
/// <param name="inCsv"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Index(string token, string pass, DateTime fromDateTime, DateTime toDateTime, bool inCsv)
{
            
　　// Set Date, if not set
   if (fromDateTime <= DateTime.MinValue && toDateTime <= DateTime.MinValue)
   {
   　　fromDateTime = DateTime.UtcNow.AddHours(-4);
       toDateTime = DateTime.UtcNow.AddHours(-1);
   }

   // Set ViewData
   ViewData["FROMDATETIME"] = fromDateTime;
   ViewData["TODATETIME"] = toDateTime;
   ViewData["TOKEN"] = token;
   ViewData["PASS"] = pass;

   // Token must be set first
   if (string.IsNullOrEmpty(token))
   {
      return View();
   }

   // Token
   var payjpToken = token + ":" + pass;

   // Convert to DateTimeOffset
   var fromDateTimeOffset = new DateTimeOffset(fromDateTime.Ticks, new TimeSpan(+09, 00, 00));
   var toDateTimeOffset = new DateTimeOffset(toDateTime.Ticks, new TimeSpan(+09, 00, 00));

   // To Unix seconds
   var fromDateUnixSeconds = fromDateTimeOffset.ToUnixTimeSeconds();
   var toDateUnixSeconds = toDateTimeOffset.ToUnixTimeSeconds();

   // Set Path
   var path = "charges?";
   path += "since=" + fromDateUnixSeconds + "&until=" + toDateUnixSeconds;
   path += "&offset=0&limit=100";

   // Set Token
   var base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes(payjpToken));

   // Call API
   var client = new HttpClient();
   var result = new ChargeListResponse();
   client.BaseAddress = new Uri(payJpBaseUrl);
   client.DefaultRequestHeaders.Accept.Clear();
   client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
   client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64authorization);

   HttpResponseMessage response = await client.GetAsync(path);

   // Make sure the call was a success
   if (response.IsSuccessStatusCode)
   {
     result = await response.Content.ReadAsAsync<ChargeListResponse>();
   }

   // see if there are more
   for (int i = 1; result.has_more; i++)
   {

     path = "charges?";
     path += "since=" + fromDateUnixSeconds + "&until=" + toDateUnixSeconds;
     path += "&offset=" + i * 100 + "&limit=100";

     response = await client.GetAsync(path);

     // Make sure the code was a success
     if (response.IsSuccessStatusCode)
     {
       var tresult = await response.Content.ReadAsAsync<ChargeListResponse>();

       // Add to result
       result.count += tresult.count;
       result.data.AddRange(tresult.data);
       result.has_more = tresult.has_more;
     }
   }

   client.Dispose();

   return View(result);
}
```

# 結果を表示する

Viewです。

```html
@model ChargeListResponse
@{
    ViewData["Title"] = "レポート";
}
<div class="container-fluid py-2">
  <h1>@ViewData["Title"]</h1>
  <hr />
  @using (Html.BeginForm("Index", "PayJp", FormMethod.Post))
  {
    <div class="input-group mb-3">
      @Html.TextBox("token", ViewData["TOKEN"], new { @class = "form-control", @placeholder = "本番秘密鍵" })
      @Html.TextBox("pass", ViewData["PASS"], new { @type = "password", @class = "form-control", @placeholder = "パスワード" })
    </div>
    <div class="input-group mb-3">
      @Html.TextBox("fromDateTime", ViewData["FROMDATETIME"], new { @class = "form-control" })
      @Html.TextBox("toDateTime", ViewData["TODATETIME"], new { @class = "form-control" })
    </div>
    <div class="small">
      in JST
    </div>
    <div class="form-group">
      <button class="btn btn-primary rounded-0" type="submit">検索</button>
    </div>
}

<hr />
@if (Model != null)
{
  <div class="d-flex justify-content-between">
    <div>
      @Model.count<text>データ</text>
    </div>     
  </div>
  <hr />
  @if (Model.data != null)
  {
    <div class="table-responsive">
      <table class="table table-bordered table-striped">
        <thead>
          <tr>
            <th>Amount</th>
            <th>Captured</th>
            <th>Captured_at</th>
            <th>Card.address_city</th>
            <th>Card.address_line1</th>
            <th>Card.address_line2</th>
            <th>Card.address_state</th>
            <th>Card.address_zip</th>
            <th>Card.address_zip_check</th>
            <th>Card.brand</th>
            <th>Card.country</th>
            <th>Card.created</th>
            <th>Card.customer</th>
            <th>Card.cvc_check</th>
            <th>Card.exp_month</th>
            <th>Card.exp_year</th>
            <th>Card.fingerprint</th>
            <th>Card.id</th>
            <th>Card.name</th>
            <th>Card.last4</th>
            <th>Created</th>
            <th>Currency</th>
            <th>Customer</th>
            <th>Description</th>
            <th>Expired_at</th>
            <th>Failure_code</th>
          　<th>Failure_message</th>
            <th>Id</th>
            <th>Livemode</th>
            <th>Metadata</th>
            <th>Paid</th>
            <th>Refunded</th>
            <th>Refund_reason</th>
            <th>Subscription</th>
          </tr>
        </thead>
        <tbody>
          @foreach (var record in Model.data)
          {
            <tr>
              <td>@record.amount</td>
              <td>@record.captured</td>
              <td>@record.captured_at</td>
              <td>@record.card.address_city</td>
              <td>@record.card.address_line1</td>
              <td>@record.card.address_line2</td>
              <td>@record.card.address_state</td>
              <td>@record.card.address_zip</td>
              <td>@record.card.address_zip_check</td>
              <td>@record.card.brand</td>
              <td>@record.card.country</td>
              <td>@record.card.created</td>
              <td>@record.card.customer</td>
              <td>@record.card.cvc_check</td>
              <td>@record.card.exp_month</td>
              <td>@record.card.exp_year</td>
              <td>@record.card.fingerprint</td>
              <td>@record.card.id</td>
              <td>@record.card.name</td>
              <td>@record.card.last4</td>
              <td>@record.created</td>
              <td>@record.currency</td>
              <td>@record.customer</td>
              <td>@record.description</td>
              <td>@record.expired_at</td>
              <td>@record.failure_code</td>
              <td>@record.failure_message</td>
              <td>@record.id</td>
              <td>@record.livemode</td>
              <td>@record.metadata</td>
              <td>@record.paid</td>
              <td>@record.refunded</td>
              <td>@record.refund_reason</td>
              <td>@record.subscription</td>
            </tr>
          }
        </tbody>
      </table>
    </div>
    }
  }
  else
  {
    <div>
      本番秘密鍵とパスワードを入力してください。
    </div>
  }
</div>
```

# おわり

一旦以上です。