7-5:クレジットカード決済機能を追加する

2019/05/29

概要

注文を受け付けることができるようになったので、クレジットカード決済機能を実装して、決済を完了させます。
オンライン決済サービスもいろいろ種類がありますが、今回は開発者向けの決済サービスで比較的導入が簡単なPAY.JPを利用して実装していきます。

フォルダ階層

完成イメージ

クレジットカード情報入力

完了

pay.jp

外部参考サイト

PAY.JPの公式サイト

全体の手順

手順は以下のとおりです。

  1. PAY.JPの登録
  2. クレジット情報入力画面作成
  3. トークンの発行
  4. 決済処理

オンライン決済のルール

オンライン決済を実装するには、「改正割賦販売法」という法律を守らなければいけません。
「改正割賦販売法」とはクレジット取引等を対象に事業者が守るべきルールを定める法律のことです。
2018年6月に改正割賦販売法が施行され、EC事業者にもセキュリティ対策が求められるようになりました。
EC事業者はクレジットカード情報の適切な管理をしなければいけません。
対応方法は主に2つ。

  1. PCI DSSに準拠する
  2. カード情報の非保持化

PCI DSSとは、カード情報を安全に取り扱うことを目的として策定されたカード業界のセキュリティ基準のことで、PCI SSC(国際カードブランド5社が共同で設立)によって運用・管理されています。
PCI DSSに準拠するには、12の要件に基づき、最大約400の要求事項をクリアする必要があり、システム投資や人件費など大きなコストがかかる可能性があり、あまり現実的ではありません。

そこで、EC業者はカード情報の非保持化を行うことが多いです。
カード情報の非保持化とはECサイト内でクレジットカード情報を持たない決済の方法です。
非保持化の方法も2つあります。

  1. トークン型(Javascript)
  2. リンクタイプ型(サイト遷移)

トークン型は、Javascriptを利用して、直接クレジットカード情報を決済サービスプロバイダーに送ることでサイト内でカード情報を持たない方法です。
リンクタイプはクレジットカード情報を入力する画面自体を決済サービスプロバイダーのサイトに遷移して行う方法です。
今回は、トークン型で決済を行っていきます。

トークン型の仕組み

ユーザーが入力したクレジットカード情報はJavascriptで決済サービスプロバイダー(PAY.JP)に送られます。
決済サービスプロバイダーでクレジットカード情報のチェックをして、トークンをECサイトに返します。
ECサイトではクレジットカード情報ではなく、このトークンを決済処理をリクエストするときに送ることで決済処理を行う仕組みです。

※イメージ(実際とは少し異なります)

PAY.JPの登録

アカウント作成

PAY.JPの公式サイトからアカウントを登録します。
メールアドレスとパスワードでアカウントを作成すると、登録したアドレスにメールが届くので認証をクリックして登録を完了します。

APIキーの確認

作成したアカウントでログインします。
ログインするとダッシュボードが表示されます。
決済が完了すると、ここに売上がリアルタイムで反映されます。
とりあえず、決済機能実装に必要なAPIキーを確認します。
左メニューのAPIをクリックするとキーが発行されています。
本番環境は別途本番申請が必要ですが、テストはすぐに利用可能です。
テスト秘密鍵とテスト公開鍵が表示されているのが確認できたら一旦OKです。

クレジット情報入力画面作成

現状、カートから購入手続きボタンをクリックすると、ご購入者情報入力→確認→受付となっています。

今回はクレジットカード情報を入力してもらいたいので、ご購入者情報入力の次にクレジットカード情報を入力する画面のpay_card.phpを作成します。

pay_card.php作成

フォーム作成

pay.phpをコピーしてpay_card.phpを作成します。
title・h3タグを「決済カード情報」に変更します。
パンくずリストは TOP>商品一覧>カート>ご購入者情報>決済カード情報になるように設定します。
formの中身をクレジット決済に必要な情報にします。
クレジットカード決済のトークンを作成するために必要な情報は

  • カード番号
  • CVC(セキュリティコード)
  • 有効期限

です。
上記の情報を使ってトークンを作成します。
名前はトークン作成でチェックされていませんが、通常名前も入力するものなので名前も一応入力フォームを作りました。
トークン作成ができなかった場合、エラー内容を表示するためにwrapper-titleのh3タグの下にspanタグでerrorクラスを作成しています。

pay_card.php

<!DOCTYPE html>
<html>

<head>
    <!-- Global site tag (gtag.js) - Google Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=UA-13xxxxxxxxx"></script>
    <script>
        window.dataLayer = window.dataLayer || [];
        function gtag() { dataLayer.push(arguments); }
        gtag('js', new Date());

        gtag('config', 'UA-13xxxxxxxxx');
    </script>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>決済カード情報|SQUARE, inc.</title>

    <link rel="icon" href="favicon.ico">

    <!-- css -->
    <link rel="stylesheet" href="styles.css">
    <link rel="stylesheet" href="responsive.css">

    <!-- icon -->
    <link href="https://use.fontawesome.com/releases/v5.0.6/css/all.css" rel="stylesheet">
</head>

<body>
    <header>
        <div class="container">
            <div class="header-logo">
                <h1><a href="index.php"><img src="img/square_logo.png" id="logo"></a></h1>
            </div>

            <!-- ハンバーガーメニューボタン -->
            <div class="toggle">
                <div>
                    <span></span>
                    <span></span>
                    <span></span>
                </div>
            </div>
            <div class="cart">
                <a href="cart.php"><i class="fas fa-shopping-cart"></i></a>
            </div>

            <nav class="sp-menu menu">
                <ul>
                    <li><a href="index.php#service">サービス</a></li>
                    <li><a href="shop.php">商品一覧</a></li>
                    <li><a href="index.php#news">お知らせ</a></li>
                    <li><a href="index.php#about">会社概要</a></li>
                    <li><a href="ブログのURL">ブログ</a></li>
                    <li><a href="register.html">会員登録</a></li>
                </ul>
            </nav>

            <nav class="pc-menu menu-left menu">
                <ul>
                    <li><a href="index.php#service">サービス</a></li>
                    <li><a href="shop.php">商品一覧</a></li>
                    <li><a href="index.php#news">お知らせ</a></li>
                    <li><a href="index.php#about">会社概要</a></li>
                    <li><a href="ブログのURL">ブログ</a></li>
                </ul>
            </nav>
            <nav class="pc-menu menu-right menu">
                <ul>
                    <li><a href="cart.php"><i class="fas fa-shopping-cart"></i></a></li>
                    <li><a href="register.html">会員登録</a></li>
                </ul>
            </nav>
        </div>
    </header>
    <main>
        <div class="breadcrumbs">
            <div class="container">
                <ul>
                    <li><a href="index.php">TOP</a></li>
                    <li><a href="shop.php">商品一覧</a></li>
                    <li><a href="cart.php">カート</a></li>
                    <li><a href="pay.php">ご購入者情報</a></li>
                    <li>決済カード情報</li>
                </ul>
            </div>
        </div>
        <div class="wrapper last-wrapper">
            <div class="container">
                <div class="wrapper-title">
                    <h3>決済カード情報</h3>
                    <span class="error"></span>
                </div>
                <form class="pay-form" action="pay_conf.php" method="POST">
                    <div class="form-group">
                        <p class="form-title">カード番号 *</p>
                        <input type="text" id="card-number" required>
                    </div>
                    <div class="form-group">
                        <p class="form-title">セキュリティーコード *</p>
                        <input type="text" id="cvc" class="sm-form" required>
                    </div>
                    <div class="form-group">
                        <p class="form-title">カード有効期限 *</p>
                        <label>月</label>
                        <input type="text" id="exp_month" placeholder="7" class="sm-form" required>
                        <label>年</label>
                        <input type="text" id="exp_year" placeholder="2020" class="sm-form" required>
                    </div>
                    <div class="form-group">
                        <p class="form-title">カード名義 *</p>
                        <input type="text" id="card-name" placeholder="TARO YAMADA">
                    </div>
                    <button type="button" class="btn btn-blue">確認する</button>
                </form>
            </div>
        </div>
    </main>
    <footer>
        <div class="container">
            <p>Copyright @ 2018 SQUARE, inc.</p>
        </div>
    </footer>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script>
        $(function () {
            // ハンバーガーメニューの動作
            $('.toggle').click(function () {
                $("header").toggleClass('open');
                $(".sp-menu").slideToggle(500);
            });
        });        
    </script>
</body>

</html>

表示してみます。

pay.phpのフォーム送信先変更

pay.phpからpay_card.phpに飛ぶことになるので、フォームの送信先を変更します。
pay.php

-   <form class="pay-form" action="pay_conf.php" method="POST">
+   <form class="pay-form" action="pay_card.php" method="POST">

購入者情報受け取りとhidden設定

pay.phpで入力して送られてきた購入者情報をphpで受け取ります。
pay_conf.phpのphpをコピーしてpay_card.phpに貼り付けます。
pay_card.php

<?php
    // 値の受け取り
    $name = isset($_POST['name'])? htmlspecialchars($_POST['name'],ENT_QUOTES,'utf-8'):'';
    $email = isset($_POST['email'])? htmlspecialchars($_POST['email'],ENT_QUOTES,'utf-8'):'';
    $tel = isset($_POST['tel'])? htmlspecialchars($_POST['tel'],ENT_QUOTES,'utf-8'):'';
    $postcode = isset($_POST['postcode'])? htmlspecialchars($_POST['postcode'],ENT_QUOTES,'utf-8'):'';
    $address = isset($_POST['address'])? htmlspecialchars($_POST['address'],ENT_QUOTES,'utf-8'):'';
?>
<!DOCTYPE html>

このページでは購入者情報を表示させる必要はないので、受け取ったデータをform内にinputタグのtype=hiddenで記述しておきます。
pay_card.php

 <form class="pay-form" action="pay_conf.php" method="POST">
+   <input type="hidden" name="name" value="<?php echo $name; ?>">
+   <input type="hidden" name="email" value="<?php echo $email; ?>">
+   <input type="hidden" name="tel" value="<?php echo $tel; ?>">
+   <input type="hidden" name="postcode" value="<?php echo $postcode; ?>">
+   <input type="hidden" name="address" value="<?php echo $address; ?>">
    <div class="form-group">
        <p class="form-title">カード番号 *</p>
        <input type="text" name="card-number" required>
    </div>

トークン発行

payjp.js読み込み

まず最初に <script> タグからpayjp.jsを読み込み、PAY.JPのページで確認したAPIキーのテスト公開鍵をセットします。
pay_card.php

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
+   <script type="text/javascript" src="https://js.pay.jp/"></script>
+   <script type="text/javascript">Payjp.setPublicKey('pk_test_c9axxxxxxxxxxxxxxxxx');</script>

値の送信

Javascriptで確認するボタンが押されたことを認識できるように確認ボタンにconfirmというクラスを追加しました。
pay_card.php

-   <button type="button" class="btn btn-blue">確認する</button>
+   <button type="button" class="btn btn-blue confirm">確認する</button>

確認するボタンが押された時にトークンを作成する処理を書きます。

それぞれの値を取得

フォームに入力された値を取得します。
また、エラーの表示はページを読み込んだ最初と、確認ボタンをクリックした時点では空にしておきます。
pay_card.php

    <script>
        $(function(){

+           $(".error").empty(); //最初はエラーは空

            // ハンバーガーメニューの動作
            $('.toggle').click(function(){
                $("header").toggleClass('open');
                $(".sp-menu").slideToggle(500);
            });

           // クレジットカード動作
+           $(".confirm").click(function() {
+           
+               $(".error").empty(); //最初はエラーは空
+               
+               //それぞれの値を取得
+               var number = $("#card-number").val();
+               var cvc = $("#cvc").val();
+               var exp_month = $("#exp_month").val();
+               var exp_year = $("#exp_year").val();     
+               
+           });

        });

    </script>

変数に値を格納

取得した値をcardという変数に格納します。
pay_card.php

    // クレジットカード動作
    $(".confirm").click(function() {

         $(".error").empty(); //最初はエラーは空

        //それぞれの値を取得
        var number = $("#card-number").val();
        var cvc = $("#cvc").val();
        var exp_month = $("#exp_month").val();
        var exp_year = $("#exp_year").val();

+       //すべての値をcardに格納
+       var card = {
+           number: number,
+           cvc: cvc,
+           exp_month: exp_month,
+           exp_year: exp_year
+       };
    });

トークン作成

トークン作成は Payjp.createToken というメソッドを使って行います。
Pay.jp.createTokenに作成した変数cardを入れて実行します。
レスポンスをresponseで受け取ります。
エラーの場合は、htmlのerror表示部にresponse.error.messageを表示させます。
エラーではない場合は、response.idで取得できるトークンをtokenという変数で受け取ります。
取得したトークンをフォームに追加します。
pay_card.php

// クレジットカード動作
$(".confirm").click(function() {

        $(".error").empty(); //最初はエラーは空

        //それぞれの値を取得
        var number = $("#card-number").val();
        var cvc = $("#cvc").val();
        var exp_month = $("#exp_month").val();
        var exp_year = $("#exp_year").val();

       //すべての値をcardに格納
        var card = {
            number: number,
            cvc: cvc,
            exp_month: exp_month,
            exp_year: exp_year
        };

+       //トークン発行
+       Payjp.createToken(card, function(s, response) {
+
+           if (response.error) {
+               // エラーの場合、エラー内容を表示
+               $(".error").append(response.error.message);
+               return false;
+           }else {
+               //OKの場合、response.idでトークンを取得
+               var token = response.id;
+               //取得したトークンをformに追加
+               $(".pay-form").append($('<input type="hidden" name="payjp_token" />').val(token));
+           }
+       });
    });

確認

一旦ここで動作確認をしてみます。
テスト環境ではテストカードでのみ決済が可能なので、PAY.JPのテストカード一覧を確認してください
cvcは123、有効期限は12/20が正常で通過します。
デベロッパーツールを開いて商品一覧から進んで、正常系のカード情報を入力して「確認する」ボタンをクリックします。
フォーム内にhiddenでpayjp_tokenが追加されているか確認します。

エラー系のカード番号でもテストしてみます。

エラー内容が赤字で表示されていればOKです。

送信

tokenが取得できたので、pay_conf.phpに送信します。
pay_card.php

    //トークン発行
    Payjp.createToken(card, function(s, response) {

        if (response.error) {
            // エラーの場合、エラー内容を表示
            $(".error").append(response.error.message);
            return false;
        }else {
            //OKの場合、response.idでトークンを取得
            var token = response.id;
            //取得したトークンをformに追加
            $(".pay-form").append($('<input type="hidden" name="payjp_token" />').val(token));
+           //支払い実行ページに送信
+           $(".pay-form").submit();
        }
    });

確認

正常系カードで実行して、pay_conf.phpに遷移できればOKです。

token受け渡し

pay_card.phpから送られてきtokenもpay_conf.phpで受け取ります。
pay_conf.php

 <?php
    // 値の受け取り
    $name = isset($_POST['name'])? htmlspecialchars($_POST['name'],ENT_QUOTES,'utf-8'):'';
    $email = isset($_POST['email'])? htmlspecialchars($_POST['email'],ENT_QUOTES,'utf-8'):'';
    $tel = isset($_POST['tel'])? htmlspecialchars($_POST['tel'],ENT_QUOTES,'utf-8'):'';
    $postcode = isset($_POST['postcode'])? htmlspecialchars($_POST['postcode'],ENT_QUOTES,'utf-8'):'';
    $address = isset($_POST['address'])? htmlspecialchars($_POST['address'],ENT_QUOTES,'utf-8'):'';
+   $payjp_token = isset($_POST['payjp_token'])? htmlspecialchars($_POST['payjp_token'],ENT_QUOTES,'utf-8'):'';

?>

受け取ったトークンをhiddenでpay_end.phpに送ります。
pay_conf.php

    <form class="pay-form" action="pay_end.php" method="POST">
+       <input type="hidden" name="payjp_token" value="<?php echo $payjp_token; ?>">
        <div class="form-group" >
            <p class="form-title">お名前 *</p>
            <p><?php echo $name; ?></p>
            <input type="hidden" name="name" value="<?php echo $name; ?>">
        </div>

パンくずリストも修正しておきます。
TOP>商品一覧>カート>ご購入者情報>クレジットカード情報>ご購入者情報確認に変更します
pay_conf.php

    <div class="breadcrumbs">
        <div class="container">
            <ul>
                <li><a href="index.php">TOP</a></li>
                <li><a href="shop.php">商品一覧</a></li>
                <li><a href="cart.php">カート</a></li>
-               <li>ご購入者情報確認</li>
+               <li><a href="pay.php">ご購入者情報</a></li>
+               <li><a href="pay_card.php">クレジットカード情報</a></li>
+               <li>ご購入者情報確認</li>
            </ul>
        </div>
    </div>

決済処理

pay_end.phpに決済処理を実装します。

トークン受け取り

pay_conf.phpから送られてきたトークンを取得します。
pay_end.php

 <?php
    // 値の受け取り
    $name = isset($_POST['name'])? htmlspecialchars($_POST['name'],ENT_QUOTES,'utf-8'):'';
    $email = isset($_POST['email'])? htmlspecialchars($_POST['email'],ENT_QUOTES,'utf-8'):'';
    $tel = isset($_POST['tel'])? htmlspecialchars($_POST['tel'],ENT_QUOTES,'utf-8'):'';
    $postcode = isset($_POST['postcode'])? htmlspecialchars($_POST['postcode'],ENT_QUOTES,'utf-8'):'';
    $address = isset($_POST['address'])? htmlspecialchars($_POST['address'],ENT_QUOTES,'utf-8'):'';
+   $payjp_token = isset($_POST['payjp_token'])? htmlspecialchars($_POST['payjp_token'],ENT_QUOTES,'utf-8'):'';

pay.jp PHPライブラリの利用

pay_end.phpではPAY.JPで用意されているphp用ライブラリを利用するのでコンソールでrequire payjp/payjp-phpを実行します。

composerのインストールチェック

composerを使ってrequireしたいのでcomposerコマンドが使えるか確認します。
console

$ composer -v

結果が下記のように返ってくればcomposerコマンドが使えるのでこのまま進みます。
console

   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.6.3 2018-01-31 16:28:17

Usage:
  command [options] [arguments]

Options:
  -h, --help                     Display this help message
  -q, --quiet                    Do not output any message
  -V, --version                  Display this application version
      --ansi                     Force ANSI output
      --no-ansi                  Disable ANSI output
  -n, --no-interaction           Do not ask any interactive question
      --profile                  Display timing and memory usage information
      --no-plugins               Whether to disable plugins.
  -d, --working-dir=WORKING-DIR  If specified, use the given directory as working directory.
  -v|vv|vvv, --verbose           Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  about                Shows the short information about Composer.
  archive              Creates an archive of this composer package.
  browse               Opens the package's repository URL or homepage in your browser.
  check-platform-reqs  Check that platform requirements are satisfied.
  clear-cache          Clears composer's internal package cache.
  clearcache           Clears composer's internal package cache.
  config               Sets config options.
  create-project       Creates new project from a package into given directory.
  depends              Shows which packages cause the given package to be installed.
  diagnose             Diagnoses the system to identify common errors.
  dump-autoload        Dumps the autoloader.
  dumpautoload         Dumps the autoloader.
  exec                 Executes a vendored binary/script.
  global               Allows running commands in the global composer dir ($COMPOSER_HOME).
  help                 Displays help for a command
  home                 Opens the package's repository URL or homepage in your browser.
  info                 Shows information about packages.
  init                 Creates a basic composer.json file in current directory.
  install              Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json.
  licenses             Shows information about licenses of dependencies.
  list                 Lists commands
  outdated             Shows a list of installed packages that have updates available, including their latest version.
  prohibits            Shows which packages prevent the given package from being installed.
  remove               Removes a package from the require or require-dev.
  require              Adds required packages to your composer.json and installs them.
  run-script           Runs the scripts defined in composer.json.
  search               Searches for packages.
  self-update          Updates composer.phar to the latest version.
  selfupdate           Updates composer.phar to the latest version.
  show                 Shows information about packages.
  status               Shows a list of locally modified packages.
  suggests             Shows package suggestions.
  update               Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file.
  upgrade              Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file.
  validate             Validates a composer.json and composer.lock.
  why                  Shows which packages cause the given package to be installed.
  why-not              Shows which packages prevent the given package from being installed.

結果が下記のようにnot foundで返ってきたらcomposerをインストールする必要があるのでローカル開発環境でhomebrewとcomposerをインストールするを参照してcomposerを使えるようにしてから次へ進みます。
console

-bash: composer: command not found

composerを使ってライブラリを追加

console

$ cd corporate-site
$ composer require "payjp/payjp-php"

しばらくすると、下記のような文言が表示されます。
console

Using version ^1.0 for payjp/payjp-php
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing payjp/payjp-php (1.0.4): Loading from cache
Writing lock file
Generating autoload files

フォルダ内に、composer.jsonファイル・composer.lockファイル、vendorフォルダが生成されていればインストール成功です。

pay_end.phpで必要なpayjpファイルを読み込みます。
そして、setApiKeyでPAY.JPで確認したテスト秘密鍵を設定します。
※端的に手順を説明するため、POSTのデータが空の場合などのエラーチェックを割愛しています。
pay_end.php

 <?php
+   require_once './vendor/payjp/payjp-php/init.php';
+   \Payjp\Payjp::setApiKey("sk_test_21a7xxxxxxxxxxxxxxxxxxxx");

    // 値の受け取り
    $name = isset($_POST['name'])? htmlspecialchars($_POST['name'],ENT_QUOTES,'utf-8'):'';
    $email = isset($_POST['email'])? htmlspecialchars($_POST['email'],ENT_QUOTES,'utf-8'):'';
    $tel = isset($_POST['tel'])? htmlspecialchars($_POST['tel'],ENT_QUOTES,'utf-8'):'';
    $postcode = isset($_POST['postcode'])? htmlspecialchars($_POST['postcode'],ENT_QUOTES,'utf-8'):'';
    $address = isset($_POST['address'])? htmlspecialchars($_POST['address'],ENT_QUOTES,'utf-8'):'';
    $payjp_token = isset($_POST['payjp_token'])? htmlspecialchars($_POST['payjp_token'],ENT_QUOTES,'utf-8'):'';

決済処理の設定

トークン、各パラメータを設定して決済処理を実行します。
決済処理を行うためにはCharge::create()を実行します。
処理に必要なパラメータも、項目によって異なるので詳しいリファレンスはpay.jp公式リファレンスを参考にしてください。
今回は最低限必要なデータで決済処理を進めます。
必要なパラメータは

  • トークン
  • 金額(決済合計金額)
  • 通貨
    です。

トークンと金額はpay_conf.phpから送られてくるデータを利用します。
通貨は、$currency = 'jpy';を設定します。
pay_end.php

    session_start();
    $products = isset($_SESSION['products'])? $_SESSION['products']:[];
    $total = 0;
    foreach($products as $key => $product){
       $subtotal = (int)$product['price']*(int)$product['count'];
       $total += $subtotal;
    }

+   $currency = 'jpy';

+   $res = \Payjp\Charge::create(array(
+               "card" => $payjp_token,
+               "amount" => (int)$total,
+               "currency" => "jpy"
+       ));

決済処理結果を表示するようにします。
公式リファレンスによると、決済エラーの場合、レスポンスにerrorが入ります。
なので、if文で$resのレスポンスにerrorがあるかをチェックして記述します。
errorがある場合は、$resultに「決済処理に失敗しました。」を入れ、$result_titleに「決済失敗」をいれます。
その他の場合は、$resultに「ご購入ありがとうございます。」、$result_titleに「購入完了」を入れます。
pay_end.php

    $res = \Payjp\Charge::create(array(
                "card" => $payjp_token,
                "amount" => (int)$total,
                "currency" => "jpy"
        ));

+   if($res['error']){
+       $result = '決済処理に失敗しました。';
+       $result_title = '決済失敗';
+   }else{
+       $result = 'ご購入ありがとうございます。';
+       $result_title = '購入完了';
+   }

HTMLで表示させます。
$resultをいままでスタティックで「ご購入ありがとうございます。」と表示させていた部分に入れて、動的に処理結果が表示させるようにしました。
$result_titleはtitleタグとwrapper-titleのh3タグで「購入完了」と表示させていた部分に入れて処理結果に応じて動的に表示させるようにします。

pay_end.php

    <head>
        <!-- Global site tag (gtag.js) - Google Analytics -->
        <script async src="https://www.googletagmanager.com/gtag/js?id=UA-13xxxxxxxxx"></script>
        <script>
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());

            gtag('config', 'UA-13xxxxxxxxx');
        </script>

        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">

+       <title><?php echo $result_title; ?>|SQUARE, inc.</title>
-       <title>購入完了|SQUARE, inc.</title>

        <link rel="icon" href="favicon.ico">

        <!-- css -->
        <link rel="stylesheet" href="styles.css">
        <link rel="stylesheet" href="responsive.css">
        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">

    </head>

    ~~~省略~~~
     <div class="breadcrumbs">
          <div class="container">
             <ul>
                 <li><a href="index.php">TOP</a></li>
                 <li><a href="cart.php">カート</a></li>
-                 <li>購入完了</li>
+                 <li><?php echo $result_title; ?></li>
            </ul>
        </div>
     </div>
    <div class="wrapper last-wrapper">
        <div class="container">
            <div class="wrapper-title">
-              <h3>購入完了</h3>
+              <h3><?php echo $result_title; ?></h3>
            </div>
            <div class="thanks">
+              <h4><?php echo $result; ?></h4>
-              <h4>ご購入ありがとうございました。</h4>
            </div>  
            <button class="btn btn-gray" onclick="location.href='./index.php'">トップページに戻る</button>
        </div>
    </div>

確認

カートから進んで正常系テストカードで決済をしてみます。
「ご購入ありがとうございます。」まで表示できればOKです。

正常に決済が完了した場合、PAY.JPのダッシュボードに決済内容が反映されます。
決済処理前は、売上金額は0でした。
決済後確認すると、売上金額にデータが反映されています。

これで一通りのクレジット決済機能は実装できました。

コード

https://github.com/bluecode-io/web-basic/tree/basic7-5