RailsでのSquare webクレジット決済のサンプル

SquareWebPaymentsSDKを用いたカード決済サイトを構築する一般的な方法は以下のドキュメントに記載されています。
- [Web Payments SDK Overview](https://developer.squareup.com/docs/web-payments/overview)
- [Web Payments SDK Documentation](https://developer.squareup.com/reference/sdks/web/payments): javascriptベースの決済フォーム作成に関するドキュメント
- [Square API Technical Reference](https://developer.squareup.com/reference/square)
- [square/web-payments-quickstart](https://github.com/square/web-payments-quickstart) : 今回のサンプルプロジェクトはこれをベースに作成しました。
- [API Explorer](https://developer.squareup.com/reference/square/payments-api/list-payments)が秀悦です。これで引数試しながらコードを書き上げるのが良いと思います。

## 前提
- 店舗でのキャッシュレス決済プラットフォーム[Square](https://squareup.com/jp/jahttps://squareup.com/jp/ja)のアカウントを持っている
- [Square APIについて](https://qiita.com/goofmint/items/a8ba21e82d252e4436ae)知っている
- サンプルプロジェクトを実行する前に、[開発者向けダッシュボード](https://developer.squareup.com/apps)で、APIアクセスにに必要なApplicationID, AccessTokenを発行しておく。ここでLocationIDも確認できます。
- ruby on rails の基礎知識（railsコマンド、ディレクトリ構造、model,view,controllerの関係など）を有している。
- [https://github.com/yasunao/sq_rails_web_payment_example](https://github.com/yasunao/sq_rails_web_payment_example)にソースコードを置いておきました。

## 画面のサンプル
![new.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30721/ded142bf-d1e3-2a11-d431-1cb9d6577e88.png)

## 追加した主なGems
|GEM|目的|
|:--|:--|
| [square.rb](https://github.com/square/square-ruby-sdk)| システムにSquarePaymentsを組み込むのに必要。Catalog, Customers, Employees, Inventory, Labor, Locationを含むAPIアクセスが可能になります。  |
|[dotenv-rails](https://github.com/bkeepers/dotenv)|.envファイルにAPIアクセスにに必要なApplicationID, AccessToken、LocationIDを記録しておきます。詳しくは[初心者向け！gem 'dotenv-rails'の使い方](https://qiita.com/__Wata16__/items/d2d3aa21623f8c026d5d)を参照。	

## Controller、Viewの作成
```
$ rails g controller patients index new create show
```

## Controllerのコードのポイント
- get_client: SquareAPIへのコールは、全てこのClientオブジェクトを通して発行します。APIコールの詳細は[https://github.com/square/square-ruby-sdk](https://github.com/square/square-ruby-sdk)を参照して学習してください。
- list_paymet: サイトの決済履歴を得ます。シンプルですね。
- get_payment: 決済の結果を得ます。やはりシンプルです
- create_payment: 実際に決済を作成します。引数のnonce, priceは、newアクションのviewのフォームより得たパラメータです。[square/web-payments-quickstart](https://github.com/square/web-payments-quickstart)のように、Viewで決済を完了する実装モデルもあるのでしょうが、今回は、コントローラ上で決済プロセスを実行しました。

```rb
class PaymentsController < ApplicationController

  # GET /payments or /payments.json
  def index
    @notice=params[:notice]
    @payments = list_payment
  end

  # GET /payments/1 or /payments/1.json
  def show
    @payment = params[:payment].nil? ? get_payment(params[:id]).body.payment : params[:payment]
    @notice=params[:notice]
  end

  # GET /payments/new
  def new
    @notice=params[:notice]
    @price=params[:price]
  end

  # POST /payments or /payments.json
  def create
    @payment=create_payment(params[:nonce],params[:price].to_i)
    respond_to do |format|
      if @payment.success?
        format.html { redirect_to payment_path(@payment.body.payment[:id], payment: @payment.body.payment, notice: "Payment was successfully created." )}
        #format.json { render :show, status: :created, location: @payment }
      else
        format.html { redirect_to action: :new, 'data-turbolinks': false,price: params[:price],notice: "Payment was unprocessable" }
        #format.json { render json: @payment.errors, status: :unprocessable_entity }
      end
    end
  end

  private
  def get_square_client
    access_token=ENV['SQUARE_ACCESS_TOKEN']
    location_id=ENV['LOCATION_ID']
    case Rails.env
    when"production"
      environment="production"
    else
      environment='sandbox'
    end
    client = Square::Client.new(
      access_token: access_token,
      environment: environment
    )
    return client
  end
  def list_payment
    client=self.get_square_client
    payments=client.payments.list_payments(
      sort_order: "DESC",
      location_id: ENV['LOCATION_ID']
    )
    payments= payments.body.try(:payments).nil? ? [] :payments.body.payments
    return payments #upto 100 paymetns will return
  end
  def create_payment(nonce, price)
    client=self.get_square_client
    location_id=ENV['LOCATION_ID']
    result = client.payments.create_payment(
      body: {
        source_id: nonce,
        idempotency_key: SecureRandom.uuid(),
        amount_money: {
          amount: price,
          currency: "JPY"
        },
        location_id: location_id
      }
    )
    return result
  end
  def get_payment(id)
    client=self.get_square_client
    result = client.payments.get_payment(
      payment_id: id
    )
  end
end
```
## Viewのポイント
Web Payments SDKを用いたカード決済において特筆すべきviewはnew.html.erbの以下の3行のみ
```
<div id="card-container"></div>
<div id="payment-status-container"></div>
<%= f.hidden_field "card-nonce", name: "nonce" %>
```

後述するapp/javascripts/payments.jsで、card-containerタグ内にiframeのクレジットカード番号の入力フォームを表示します。Bootstrapベースになっています。ここで表示されているテキストボックスはすべてSquare側のiframe内で提供されており、Webサービス提供側からは触れないようになっています。ユーザからの見た目はWebサービス側そのままですし、デザインの自由度も高いのですがセキュリティ的に安全な仕組みになっています。

カード番号を入力してカード番号を決済ボタンを押すと、まずカード番号や有効期限、CVVといった情報がSquareのサーバに送られます。その結果、一時的に決済に使えるトークン（nonce）が発行されます。このトークンと決済金額を合わせて決済ができますが、これは一度しか使えません。（以上の情報は引用しました。[クレジットカード番号を保有せずクレジットカード番号を保存するCard on Fileの使い方](https://qiita.com/goofmint/items/4a1f335cb70ecd00242c)）

決済ボタンを押した時に、一時的に決済に使えるトークン(nonce）をhidden_fieldに格納し、createアクションにパラメータを送信するわけです。

```erb
## new.html.erb
<%case Rails.env%>
<%when 'production'%>
	<%= javascript_include_tag "https://web.squarecdn.com/v1/square.js" , 'data-turbolinks-track': 'reload' %>
<%when 'development'%>
	<%= javascript_include_tag "https://sandbox.web.squarecdn.com/v1/square.js" , 'data-turbolinks-track': 'reload' %>
<%end%>

<div class="container">
	<h1>New Payment</h1>
	<p id="notice"><%= @notice %></p>
	<div class="col-md-8 order-md-1">
	  	<%= form_with(id: "payment-form",url: payments_path, method: :post, local: true) do |f| %>
			<div class="mb-3">
				<label for="card" class="form-label">Credit Card</label>
				<div id="card-container"></div>
				<div id="payment-status-container"></div>
				<%= f.hidden_field "card-nonce", name: "nonce" %>
			</div>
			<div class="mb-3">
				<label for="price" class="form-label">Price</label>
				<%= f.text_field "price",class: "form-control"%>
			</div>
	      	<%= f.submit "Pay" ,id: "card-button",class: "btn btn-primary"%>
	    <% end %>
	</div>
	<%= link_to 'Back', payments_path %>
</div>
```
## app/javascripts/payments.js
ほぼ、[square/web-payments-quickstart](https://github.com/square/web-payments-quickstart)をコピーしたものです。強いて言えば以下の3点が特徴でしょうか
- APIアクセスに必要なパラメータをprocess.env.APPLICATION_IDのように環境変数より取得している。herokuにデプロイする場合は、[Herokuで環境変数を設定する方法](https://qiita.com/mzmz__02/items/64db94b8fc67ee0a9068)を参考にしましょう。
- document.getElementById('card-nonce').value = token
- document.getElementById('payment-form').submit()

```js
##app/javascripts/payments.js
const appId = process.env.APPLICATION_ID;
const locationId = process.env.LOCATION_ID;

async function initializeCard(payments) {
  const card = await payments.card();
  await card.attach('#card-container');
  return card;
}

async function createPayment(token) {
  const body = JSON.stringify({
    locationId,
    sourceId: token,
  });

  const paymentResponse = await fetch('/payment', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body,
  });

  if (paymentResponse.ok) {
    return paymentResponse.json();
  }

  const errorBody = await paymentResponse.text();
  throw new Error(errorBody);
}

async function tokenize(paymentMethod) {
  const tokenResult = await paymentMethod.tokenize();
  if (tokenResult.status === 'OK') {
    return tokenResult.token;
	
  } else {
    let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
    if (tokenResult.errors) {
      errorMessage += ` and errors: ${JSON.stringify(
        tokenResult.errors
      )}`;
    }
    //throw new Error(errorMessage);
	return errorMessage
  }
}

document.addEventListener('DOMContentLoaded', async function () {
  if (!window.Square) {
    return;
  }
  let payments;
  try {
    payments = window.Square.payments(appId, locationId);
  } catch(e) {
    const statusContainer = document.getElementById(
      'payment-status-container'
    );
    statusContainer.className = 'missing-credentials';
    statusContainer.style.visibility = 'visible';
  	console.error(e);
	
    return;
  }

  let card;
  try {
    card = await initializeCard(payments);
  } catch (e) {
    console.error('Initializing Card failed', e);
    return;
  }

  // Checkpoint 2.
  async function handlePaymentMethodSubmission(event, paymentMethod) {
    event.preventDefault();
    try {
        // disable the submit button as we await tokenization and make a payment request.
        cardButton.disabled = true;
        const token = await tokenize(paymentMethod);
		// nonceの値をhiddenの中に入れます
		document.getElementById('card-nonce').value = token;
    } catch (e) {
	  console.error(e.message);
      cardButton.disabled = false;
    }
  }

  const cardButton = document.getElementById('card-button');
  cardButton.addEventListener('click', async function (event) {
    await handlePaymentMethodSubmission(event, card);
	// 本来のフォームを送信します
	document.getElementById('payment-form').submit();
  });
});
```

## 考察
以前はちょっとしたWebサービスに決済機能を盛り込むのはセキュリティの面でハードルが高いと感じていました。しかしSquareAPIおよびSquareWebPaymentSDKが使えることで、容易に決済ロジックを組み込むことができるようになりました。
## 謝辞
Qiitaコミュニティにおけるエンジニアリングに関する知識を記録・共有のおかげで、これまでたくさんの問題を解決することができました。少しばかりですが、Ruby,Rials, SquareAPI関連のコミュニティに貢献できれば幸いです。