はじめに
先日、実務でfincodeを使ったクレジットカード決済機能の実装を担当したので、その内容を共有します。初めてfincodeやLaravelAPI開発を行う方向けの内容となっているので、簡単な内容ではありますが、ご了承ください。
fincodeとは
APIを使ってシステムに決済手段を組み込むことができる、オンライン決済プラットフォームです。
公式チュートリアルでは、以下のように説明されています。
fincodeの立ち位置と役割
fincodeは、「オンライン上で提供されるバーチャルなレジ」のようなものです。
fincodeは、事業を行う皆さんと、決済手段を提供する決済会社( カード決済の場合はカード会社)の間に立ち、両者をつなぐ「ゲートウェイ」の役割を果たします。
2022年にローンチされた、比較的新しい決済サービスです。
APIによる柔軟な実装が可能で、様々な決済手段(クレジットカード決済、コンビニ決済、PayPay、口座振替など)に対応していることが特徴です。
新規ユーザー登録、APIキー確認
fincodeを用いたシステム開発を行う際は、新規ユーザー登録を行い、APIキーを取得する必要があります。
新規ユーザー登録
テスト環境
エンジニアが開発を行う時は、テスト環境のアカウントを用います。実際の決済は行われないので、安心して開発することができます。
本番環境
本番環境は、実際のカード決済・入金処理が行われます。実運用で用います。ログイン時に二要素認証が必要です。また、本番環境申請が完了することで、APIが利用できるようになります。
APIキー確認
テスト環境
ダッシュボードで新規ユーザー作成・ログインを行い、サイドメニューの「API・Webhook」を押下するとAPIキーを確認できます。ここで取得したAPIキー(パブリックキー、シークレットキー)は、後ほど.envに記載します。
本番環境
APIキーの確認方法はテスト環境と同じです。
fincodeの実装パターン
リダイレクト型を使用
fincodeが提供する決済画面を使用する方法です。決済画面を自前で作らなくて良いので、実装コストは最小です。簡易な決済機能で問題ないのであれば、リダイレクト型決済で十分でしょう。後ほど、こちらの方法を用いた実装例を紹介します。
自サイトで実装
リダイレクト型決済を使わず、自サイト内で決済画面・決済機能を実装する方法です。フロントエンド・バックエンドの両方で、決済処理の実行が可能です。
- フロントエンドで決済実行
- バックエンドで決済登録を行い、その後フロントエンドで決済処理を実行する(決済JS)
- バックエンドで決済実行
- 新規カードの場合、フロントエンドでカードトークンを発行し、バックエンドで決済処理を実行する(トークン決済)
- 保存済みカードの場合、顧客idやカードidを用いて、バックエンドで決済処理を実行する
また、決済画面について、UIコンポーネント(fincodeが提供する入力UI)を自サイトの決済画面に埋め込むことで、実装コストを下げることができます。
公式ブログにて、実装例が紹介されているので、参考にしてみると良いかもしれません
リダイレクト型決済実装例
APIリファレンス確認
まずはAPIリファレンスで詳細な内容を確認します。
リファレンスを直接確認した方がわかりやすいですが、念の為以下に転記します。
POST /v1/sessions
Request
{ "transaction": { "pay_type": [ "Card" ], "amount": "1000", "order_id": "o_**********************", "tax": "100", "client_field_1": null, "client_field_2": null, "client_field_3": null }, "card": { "job_code": "CAPTURE", "tds_type": "2", "tds2_type": "2", "td_tenant_name": "s_***********-ab123", "tds2_ch_acc_change": "20240101", "tds2_ch_acc_date": "20220101", "tds2_ch_acc_pw_change": "20230101", "tds2_nb_purchase_account": "9999", "tds2_payment_acc_age": "20231231", "tds2_provision_attempts_day": "999", "tds2_ship_address_usage": "20230930", "tds2_ship_name_ind": "01", "tds2_suspicious_acc_activity": "01", "tds2_txn_activity_day": "999", "tds2_txn_activity_year": "999", "tds2_three_ds_req_auth_data": null, "tds2_three_ds_req_auth_method": "01", "tds2_three_ds_req_auth_timestamp": "202205191234", "tds2_email": "string", "tds2_addr_match": "Y", "tds2_bill_addr_country": "392", "tds2_bill_addr_state": "13", "tds2_bill_addr_city": "渋谷区", "tds2_bill_addr_line_1": "道玄坂1-14-6", "tds2_bill_addr_line_2": "ヒューマックス渋谷ビル", "tds2_bill_addr_line_3": "7F", "tds2_bill_addr_post_code": "150-0043", "tds2_ship_addr_country": "392", "tds2_ship_addr_state": "13", "tds2_ship_addr_city": "渋谷区", "tds2_ship_addr_line_1": "道玄坂1-14-6", "tds2_ship_addr_line_2": "ヒューマックス渋谷ビル", "tds2_ship_addr_line_3": "7F", "tds2_ship_addr_post_code": "150-0043", "tds2_ship_ind": "01", "tds2_delivery_email_address": "email@example.com", "tds2_home_phone_cc": "81", "tds2_home_phone_no": "312345678", "tds2_mobile_phone_cc": "81", "tds2_mobile_phone_no": "9012345678", "tds2_work_phone_cc": "81", "tds2_work_phone_no": "312345678", "tds2_delivery_timeframe": "01", "tds2_pre_order_date": "20231231", "tds2_pre_order_purchase_ind": "01", "tds2_reorder_items_ind": "01", "tds2_recurring_expiry": "20231231", "tds2_recurring_frequency": "99", "tds2_gift_card_amount": "999999", "tds2_gift_card_count": "99", "tds2_gift_card_curr": "392" }, "konbini": { "payment_term_day": "2", "konbini_reception_mail_send_flag": "1" }, "paypay": { "job_code": "CAPTURE", "order_description": "Your Shop上での購入" }, "virtualaccount": { "payment_term_day": "90", "virtualaccount_reception_mail_send_flag": "1", "reference_order_id": "o_**********************" }, "success_url": "https://your-service.example.com/success", "cancel_url": "https://your-service.example.com/cancel", "expire": "2022/02/31 23:59:59.999", "shop_service_name": "Your Service", "guide_mail_send_flag": "1", "receiver_mail": "receiver-email@example.com", "mail_customer_name": "買物 太郎", "thanks_mail_send_flag": "1", "shop_mail_template_id": null }Response(200 リクエストに成功)
{ "id": "lk_**********************", "link_url": "https://secure.test.fincode.jp/v1/links/lk_**********************", "success_url": "https://your-service.example.com/success", "cancel_url": "https://your-service.example.com/cancel", "status": "CREATE", "expire": "2022/02/31 23:59:59.999", "shop_service_name": "Your Service", "guide_mail_send_flag": "1", "receiver_mail": "receiver-email@example.com", "mail_customer_name": "買物 太郎", "thanks_mail_send_flag": "0", "shop_mail_template_id": null, "transaction": { "pay_type": [ "Card" ], "order_id": "o_**********************", "amount": 1000, "tax": 1000, "client_field_1": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore", "client_field_2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore", "client_field_3": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore" }, "card": { "job_code": "CAPTURE", "tds_type": "2", "td_tenant_name": "s_***********-ab123", "tds2_type": "2", "item_code": null }, "konbini": { "konbini_reception_url": "https://secure.test.fincode.jp/v1/links/lk_**********************/konbini", "payment_term_day": 2, "konbini_reception_mail_send_flag": "1" }, "paypay": { "job_code": "CAPTURE", "order_description": "Your Shop上での購入" }, "virtualaccount": { "virtualaccount_reception_url": "https://secure.test.fincode.jp/v1/links/lk_**********************/virtualaccount", "payment_term_day": 90, "virtualaccount_reception_mail_send_flag": "1" }, "bill_id": "string", "created": "2022/05/16 23:59:59.999", "updated": "2022/05/16 23:59:59.999" }Response(400 不正なリクエスト)
{ "errors": [ { "error_code": "E**********", "error_message": "string" } ] }
リクエストについて、transaction(決済共通項目)のamount(決済金額)が必須項目です。
事前準備
.envの設定
ダッシュボードで取得したAPIキーを.envに記載します。ベースURLはなくても良いです。
FINCODE_BASE_URL=https://api.test.fincode.jp # 本番環境は https://api.fincode.jp
FINCODE_PUBLIC_KEY=p_test_*************** # パブリックキー
FINCODE_SECRET_KEY=m_test_*************** # シークレットキー
パブリックキーはフロントエンドで使います。fincodeJS(トークンJS)を用いて、カード情報のトークン化や決済JSの起動に利用します。
シークレットキーはバックエンドで使います。サーバーからfincodeAPIを呼び出す際の認証に用います。
設定キャッシュ
本番環境で設定キャッシュを使用することを想定し、環境変数はenv()で直接参照せず、config()で設定ファイルから取得するようにします。
<?php
return [
'fincode' => [
'base_url' => env('FINCODE_BASE_URL'),
'public_key' => env('FINCODE_PUBLIC_KEY'),
'secret_key' => env('FINCODE_SECRET_KEY'),
],
];
例えば、コントローラーやサービスクラスで、config('services.fincode.secret_key')と書くことで、.envのシークレットキーを取得することができます。
ルーティング
<?php
use App\Http\Controllers\PaymentConfirmController;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
Route::prefix('payment')->name('payment.')->group(function () {
// 決済内容確認画面表示
Route::match(['GET', 'POST'], '/confirm', [PaymentConfirmController::class, 'index'])
->name('confirm')->withoutMiddleware([ValidateCsrfToken::class]);
// 決済URL 作成 APIを実行し、決済画面へリダイレクト
Route::post('/confirm/store', [PaymentConfirmController::class, 'store'])
->name('confirm.store');
// 決済完了画面表示
Route::post('/complete', [PaymentConfirmController::class, 'index'])
->name('complete')->withoutMiddleware([ValidateCsrfToken::class]);
});
GETとPOSTの両方を許可
決済内容確認画面の表示について、GETとPOSTの両方を想定し、match()メソッドを使用しています。GETは通常のケース、POSTはリダイレクト型URLにてキャンセルされた時のケースを想定しています。キャンセル時はPOSTで決済内容確認画面へリダイレクトするので、POSTも許可しなければなりません。
POSTを許可しないと...
fincode決済画面からPOSTでリダイレクトした時、以下のエラーが発生します。
Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
The POST method is not supported for route payment/confirm.
Supported methods: GET, HEAD.
POSTが許可されていないのにPOSTリクエストを送っているため、このようなエラーが発生します。
決済完了画面表示についても、画面表示なのでGETを使いたいところですが、リダイレクト型URLにて決済完了した時にPOSTでリダイレクトするため、post()メソッドを使用しています。
CSRFトークンを除外
fincode決済画面は外部サイトなので、そこからPOSTでリダイレクトした時、CSRFトークンを含みません。CSRFトークンがないとエラーが発生したり、想定外の挙動が起きたりします。そこで、withoutMiddleware()メソッドを使用し、引数にValidateCsrfTokenミドルウェアを指定することで、CSRFトークンを除外できます。CSRFトークン除外については、下記の記事がわかりやすかったです。
コントローラ
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\RedirectResponse;
use Exception;
final class PaymentConfirmController extends Controller
{
/**
* fincodeが提供する決済画面へリダイレクト
*
* @param Request $request
* @return RedirectResponse
*/
public function store(Request $request): RedirectResponse
{
try {
// 決済金額を取得
// $amount = ??????;
// リクエストボディを準備
$requestData = [
'transaction' => [
'pay_type' => 'Card', // 決済手段:カード決済
'amount' => $amount, // 決済金額
],
'card' => [
'job_code' => 'CAPTURE', // 取引種別:即時売上
'tds_type' => '2', // 3Dセキュア認証を利用するか。:3Dセキュア2.0認証を利用する
'tds2_type' => '2', // 3Dセキュア2.0非対応時の挙動設定:エラーをレスポンスし、処理を終了する。
],
'success_url' => route('payment.complete'), // 成功時リダイレクトURL
'cancel_url' => route('payment.confirm'), // キャンセル時リダイレクトURL
];
// 決済URL 作成 APIを実行
$response = Http::baseUrl(config('services.fincode.base_url')) // fincodeのAPIエンドポイントへアクセス
->withToken(config('services.fincode.secret_key')) // Authorization: Bearer <シークレットキー>
->acceptJson() // レスポンスをapplication/jsonで返すように指定
->asJson() // リクエストボディをJSONで送る
->post("/v1/sessions", $requestData) // 決済URL 作成 APIをPOSTで実行
->throw(); // エラーが発生した場合は例外を投げる
// リダイレクト型決済URLへリダイレクト
return redirect()->away($response->json()['link_url']);
} catch (Exception $e) {
// 例外をログに出力
Log::error('fincode redirect failed: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
// 前のページにリダイレクトし、エラーメッセージを表示
return back()->with('error', '決済処理に失敗しました。再度お試しください。');
}
}
}
リクエストボディ
$requestDataで、決済URL 作成 APIのリクエストボディを配列で作成しています。APIリファレンスに沿って、各項目の値を設定しています。
3Dセキュア2.0認証
オンラインカード決済の不正利用防止のための本人認証システムです。今回は、3Dセキュア2.0認証を利用する設定にしています。
リダイレクトURL
決済画面に配置されるボタンのリダイレクト先のことです。
決済URL 作成 APIを実行
LaravelのHTTPクライアントを使用して、fincodeへのAPIを実行しています。HTTPクライアントで使用している各メソッドは、公式LaravelAPIリファレンス
で確認できます。
-
baseUrl():Set the base URL for the pending request.(このリクエスト設定に対して使用する基底URLを指定します。) -
withToken():Specify an authorization token for the request.(このリクエストに使用する認可トークンを指定します。) -
acceptJson():Indicate that JSON should be returned by the server.(サーバーにJSONでレスポンスを返すよう要求します〔Accept: application/jsonを付与〕。) -
asJson():Indicate the request contains JSON.(このリクエストの本文をJSONとして送信することを指定します〔Content-Type: application/json・配列はJSONにエンコード〕。) -
post():Issue a POST request to the given URL.(指定したURLへPOSTリクエストを送信します。) -
throw():Throw an exception if a server or client error occurs.(サーバー/クライアントエラーが発生した場合に例外を投げます。)
外部ドメインへのリダイレクト
外部のページへリダイレクトするため、away()メソッドを使用しています。
自サイトでの決済実装
自サイトで決済処理を実装する(リダイレクト型決済を使用しない)場合、決済機能を柔軟にカスタマイズ可能です。私は、バックエンドで決済を実行する処理を実装しました。その処理のフローチャートを共有します。
雑ですが、以下の仕様を想定しています。
- ゲストユーザーとログインユーザー、共にカード決済が可能
- ゲストユーザーの場合
- fincodeJSで生成したカードトークンでの決済処理を行う
- ログインユーザーの場合
- 初めてカード決済行う場合、顧客登録を行い、顧客idをDBに保存する
- 初めて利用するカードで決済を行う場合、カード登録を行い、カードidをDBに保存する
- 既に保存済みのカードを再度利用する場合、DBに保存された顧客idとカードidを使用して、決済処理を進める
- 3Dセキュア2.0認証を使用する
データ入力・データ出力については、省略しています。使用したAPIは以下の6つです。
-
顧客 登録:POST
/v1/customers -
カード 登録:POST
/v1/customers/{customer_id}/cards -
決済 登録:POST
/v1/payments -
決済 実行:PUT
/v1/payments/{id} -
3Dセキュア 認証実行:PUT
/v1/secure2/{access_id} -
認証後決済 実行:PUT
/v1/payments/{id}/secure
- あくまで一例なので、他にも様々な方法があると思います
- コード量が多いので、すみませんが詳細なソースコード例は省きます
さいごに
初めて実務で、外部APIを使用した実装や決済機能の実装を行ったので、最初はかなり躓きました。公式リファレンスを読み込んだり、Googleで調べたり、AIを頼ったりすることで、なんとか形になりました。自分にとっては難しい内容でしたが、やってみて良かったと感じています。
fincode自体は、すごく使いやすかったです。APIで柔軟に実装ができて、リファレンスも理解しやすかったので、普通にオススメです。Stripeなど決済プラットフォームは色々ありますが、決済機能を実装する際は選択肢の1つとして考えても良いと思います。
本記事に関して、もし間違いがあればすみません。間違いがある場合は、コメントにてご指摘いただけると幸いです。
ここまで読んでいただき、ありがとうございました!
参考URL



