ASP.net MVC 5 で Stripe のカード決済を試してみる

# 経緯
これは [JP_Stripes (Stripe ユーザーグループ）in 松山 キックオフ](http://eventregist.com/e/JP_Stripes_MYJ "JP_Stripes") のLT登壇のためにまとめた記事です。

オンライン決済で話題になっている[Stripe](https://stripe.com/jp "Stripe") 、LT登壇ネタとして、ちょうど仕事の開発でも使っている ASP.net MVC 5 で、噂どおり簡単に実装できるか試してみることにしました。
結果、簡単過ぎて、、、開発する手ごたえがないほど楽でした。（笑）

## 試すこと
会員がログインした後、商品を発注しカード決済するまでの一連の流れを想定して実装してみます。

「会員登録」→「ログイン」→「商品を発注」→「カード決済入力」→「カード決裁結果を表示」

使用するPCの環境は以下です。

* OS : Windows 10 Professional
* 開発環境 : VisualStudio 2017 Professional

# 実装してみる
## 手順
1. [Stripe](https://stripe.com/jp "Stripe") のアカウント登録
+ ASP.net MVC 5 のプロジェクトを、認証は「個別ユーザアカウント」を選択し作成
+ [Stripe.net](https://github.com/stripe/stripe-dotnet "Stripe.net") のクラスライブラリィを設定
+ ASP.net MVC のプロジェクトに課金処理を実装・テスト

以上の手順で進めます。

## Stripe のアカウント登録

以下の Stripe のサイトにアクセスし「アカウント登録」を行いましょう。
https://stripe.com/jp
簡単なので説明は省略します。

アカウント登録が終わったらダッシュボードに進みましょう。
Unnamed Acount が作成されていますが、私は試験用のアカウントを追加しました。

![ダッシュボード](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe03.png "ダッシュボード")

早速売上があるように見えてますが、これはテストデータです。（笑）
テストデータは消せれるようにして欲しいですね。

## ASP.net MVC 5 のプロジェクトを作成

次に、VisualStudio を起動して、ASP.net MVC 5 のプロジェクトを作成しまししょう。

![VisualStudio PJ作成1](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe04.png "VisualStudio PJ作成1")

認証は、今回「個別のユーザアカウント」を選択します。

![VisualStudio PJ作成2](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe05.png "VisualStudio PJ作成2")

以上で、最初から認証機能（ユーザ登録、ログインなど）が実装された、ASP.net MVC のプロジェクトが追加されます。
ASP.net MVC って、本当にプロジェクトを作ってから手軽に開発をスタートできるのがいいですね！

## Stripe.net クラスライブラリィの導入

今回はカード決済の実装に Stripe を使いますので、そのクラスライブラリィを導入します。
コードは以下の Github に公開されているので、何かあった時でもコードを直接参照できるのは安心です。
https://github.com/stripe/stripe-dotnet

NuGet で簡単にインストールできるのもいいですね！
パッケージマネージャーコンソールで、以下を実行するだけでインストールは終わりです。

` PM> Install-Package Stripe.net `

![Stripe Install 1](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe08.png "Stripe Install 1")

Stripe.net.11.9.0 のインストールで、自動で依存関係にある Newtonsoft.Json も 6.0.4 から 9.0.1 アップデートされてますね。

![Stripe Install 2](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe10.png "Stripe Install 2")

## DBマイグレーションの設定（必須ではないが）

Stripe の顧客はメアドが重複しても、そのまま追加されてしまいます。

試験といえども、同じメールアドレスの顧客が重複して登録されるのは避けたいところ。
ローカルの DB で Stripe の顧客IDを管理して、Stripe の顧客が重複するのを避けます。

いちいちDBのテーブルを手動で追加するのも面倒ですので、マイグレーションの設定を追加します。

Stripe の顧客IDを管理するため、Customer エンティティを追加します。

```c#:/Model/Customer.cs
    public class Customer
    {
        public int Id { get; set; }
        public string Email { get; set; }
        public string StripeId { get; set; }
    }
```

Customer エンティティを保管する DB のコンテキスト AppDbContext を追加します。

```c#:/Model/AppDbContext.cs
    public class AppDbContext : DbContext
    {
        public DbSet<Customer> Customers { get; set; }
    }
```

パッケージマネージャーコンソールで、マイグレーションを有効にします。
オプションで DB のコンテキスト名と、自動マイグレーションの指定を追加します。

` PM> Enable-Migrations -ContextTypeName WebApplication1.Models.AppDbContext -EnableAutomaticMigrations  `

マイグレーションを有効にすると /Migrations/Configuration.cs が自動で追加されます。

![Enable-Migrations 2](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe14b.png "Enable-Migrations 2")

続いて、データベースの接続設定を追加します。

DB の保管場所を明示的に指定することで管理しやすくなります。
`<add name="DefaultConnection" ... />` の1つ目は、認証部分のコンテキスト用です。
その下に `<add name="WebApplication1.Models.AppDbContext" ... />` を追加します。 

```xml:/Web.config
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-WebApplication1-20171214041732.mdf;Initial Catalog=aspnet-WebApplication1-20171214041732;Integrated Security=True" providerName="System.Data.SqlClient" />
    <add name="WebApplication1.Models.AppDbContext" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\AppDb.mdf;Initial Catalog=AppDb;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>
```

さらに Global.asax.cs に Database.SetInitializer() を追加して、Web.config で指定した定義で AppDbContext の DB に接続するようにします。

```c#:/Global.asax.cs
using System.Data.Entity;
using WebApplication1.Migrations;
using WebApplication1.Models;

namespace WebApplication1
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            Database.SetInitializer(new MigrateDatabaseToLatestVersion<AppDbContext, Configuration>());
        }
    }
}
```

以上の設定が終わったら、**再度マイグレーションの設定を -Force で上書き**します。

` PM> Enable-Migrations -ContextTypeName WebApplication1.Models.AppDbContext -EnableAutomaticMigrations -Force `

これで AppDbContext にセットされたモデルに変更・追加があっても、自動でマイグレーションしてくれます。

## 実装

Stripe には多くのドキュメントが整備されています。（日本語対応が遅れているのが残念ではありますが。）
stripe docs の以下を参照しながら、ASP.net MVC 5 への実装を進めます。

Using Checkout in an ASP.NET MVC application
https://stripe.com/docs/checkout/aspnet

Creating Charges
https://stripe.com/docs/charges

Detailed Checkout Guide
https://stripe.com/docs/checkout

Retrieve a customer
https://stripe.com/docs/api#retrieve_customer

### 鍵の管理

実装の前に、Stripe のダッシュボードで APIキー（公開鍵、シークレット鍵）を確認します。

![Stripe API](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe24.png "Stripe API")

シークレット鍵がいきなり表示されないように工夫されているのは良いですね！
APIキーは以下のように Web.config などに保管しておくと良いでしょう。
（本当は暗号化などして、もっと厳重に管理した方が良いですが、今回は省略しました。）

```xml:/Web.config
  <appSettings>

（中略）

    <add key="StripeSecretKey" value="sk_test_Key値" />
    <add key="StripePublishableKey" value="pk_test_Key値" />
  </appSettings>
```

シークレットキーの Configuration への設定は Startup.cs などで実施すると良いでしょう。

```c#:/Startup.cs
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);

            // Stripe の初期設定
            StripeConfiguration.SetApiKey(ConfigurationManager.AppSettings["StripeSecretKey"]);
        }
```

### コントローラの実装

コントローラに以下の2つのメソッドを追加します。

+ 商品を発注、カード決裁入力する Order メソッド
+ カード決済と、決裁結果を表示する Charge メソッド

```c#:/Controllers/HomeController.cs
    public class HomeController : Controller
    {
        // カード処理を実装するアプリケーションサービス
        private readonly HomeControlerService service;

        public HomeController()
        {
            this.service = new HomeControlerService();
        }

（中略）

        // 商品を発注、カード決裁入力
        [Authorize]
        public ActionResult Order()
        {
            // カード情報入力画面（checkout.js）に必要な引数をセット（Stripeの公開鍵とメールアドレス）
            ViewBag.PublishableKey = ConfigurationManager.AppSettings["StripePublishableKey"]; 
            ViewBag.Email = User.Identity.Name;
            return View();
        }

        // カード決済と、決裁結果を表示
        [HttpPost]
        [Authorize]
        public ActionResult Charge(string stripeEmail, string stripeToken)
        {
            // クロスドメインでも実行できるように
            Response.Headers["Expires"] = "-1";
            Response.Headers["Access-Control-Allow-Origin"] = "*";

            try
            {
                // 課金処理
                service.Charge(stripeEmail, stripeToken);
                return View();
            }
            catch (Exception ex)
            {
                // 課金エラー処理
                // 本当なら service からスローされたエラーを処理します
                // こんなアバウトなキャッチはだめですよ！(^^;;
                ViewBag.Message = "エラー発生：" + ex.Message;
                return View("Error");
            }
        }
    }
```

### アプリケーションサービスの実装

実際のカード決裁処理は、アプリケーションサービスクラスに実装します。

Stripe の顧客情報はメールアドレスが重複しても追加されてしまいます。
メールアドレスが重複する顧客情報は登録したくないので、ローカルDB で Stripe の顧客IDを管理して重複しないようにします。
（本来は課金結果も保管しますが、今回は省略します。）

```c#:/Model/HomeControlerService.cs
    public class HomeControlerService : IDisposable
    {
        private bool disposed = false;
        private readonly AppDbContext AppDb = new AppDbContext();

        public HomeControlerService()
        {
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposed) return;

            if (disposing)
            {
                if (this.AppDb != null)
                {
                    this.AppDb.Dispose();
                }
            }

            disposed = true;
        }

        // カード決裁処理
        public void Charge(string stripeEmail, string stripeToken)
        {
            var customersService = new StripeCustomerService();
            var chargesService = new StripeChargeService();
            StripeCustomer customer;
            Customer dbCustomer;

            // DBにメアドが存在しなければ、顧客登録を行う
            var dbCustomers = AppDb.Customers.Where(c => c.Email == stripeEmail);
            if (dbCustomers.Count() == 0)
            {
                // Stripe に顧客を追加
                customer = customersService.Create(new StripeCustomerCreateOptions
                {
                    Email = stripeEmail,
                    SourceToken = stripeToken
                });

                // DB に顧客を追加
                dbCustomer = new Customer()
                {
                    Email = stripeEmail,
                    StripeId = customer.Id
                };
                AppDb.Customers.Add(dbCustomer);
                AppDb.SaveChanges();
            }
            else
            {
                // Stripe の顧客を取得
                dbCustomer = dbCustomers.First();
                customer = customersService.Get(dbCustomer.StripeId);
            }

            // 課金を追加
            var charge = chargesService.Create(new StripeChargeCreateOptions
            {
                Amount = 1000,
                Currency = "jpy",
                Description = "Example charge",
                CustomerId = customer.Id
            });

            // ここで課金結果をDBに登録（今回は実装しない）
            // DBに charge.id を保管すれば、Stripe 側の課金情報と紐づけできる
        }
    }
```

最後に、以下の View の追加・修正を行います。

+ 商品の発注画面のリンクを追加し Index ビューを修正
+ 商品を発注、カード決裁入力する Order ビューを追加
+ カード決済と、決裁結果を表示する Charge ビューを追加
+ カード決済のエラーを表示する Error ビューを追加

```html:/Views/Home/Index.cshtml
<div class="row">
@if (Request.IsAuthenticated)
{
    <div class="col-md-4">
        <h2>Stripe Charge 試験（芋けんぴ注文）</h2>
        <p>
            Stripe Charge の試験を、「芋けんぴ注文」画面で行います!
        </p>
        <p>@Html.ActionLink("芋けんぴ注文", "Order", "Home")</p>
    </div>
}
    <div class="col-md-4">
```

```html:/Views/Home/Order.cshtml
@{
    ViewBag.Title = "芋けんぴ注文";
}

<div class="jumbotron">
    <h1>芋けんぴ注文</h1>
</div>

<div class="row">
    <form action="/Home/Charge" method="POST">
        <article>
            <label>芋けんぴ： &yen;1,000</label>
        </article>
        <!-- ボタンを押すと支払をするカード情報を入力するダイアログを表示 -->
        <script src="//checkout.stripe.com/v2/checkout.js"
                class="stripe-button"
                data-key="@ViewBag.PublishableKey"
                data-email="@ViewBag.Email"
                data-locale="auto"
                data-currency="JPY"
                data-description="芋けんぴ代 支払い"
                data-amount="1000">
        </script>
    </form>
</div>
```

```html:/Views/Home/Charge.cshtml
@{
    ViewBag.Title = "芋けんぴ支払い完了";
}

<div class="jumbotron">
    <h1>芋けんぴ支払い完了</h1>
</div>

<div class="row">
    <h2>芋けんぴ代 支払い完了しました！<strong>&yen;1,000</strong></h2>
</div>
```
```html:/Views/Home/Error.cshtml
@{
    ViewBag.Title = "芋けんぴ支払いエラー";
}

<div class="jumbotron">
    <h1>芋けんぴ支払いエラー</h1>
</div>

<div class="row">
    <h2>@ViewBag.Message</h2>
</div>
```

以上で実装は終わりです。

## テスト

実装が終わったら、VisualStudio でデバッグ実行してみます。
デバッグ実行前に Stripe の顧客と、支払いを確認します。

![Stripe 顧客](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe25.png "Stripe 顧客")

![Stripe 支払い](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe26.png "Stripe 支払い")

デバック起動した最初の画面です。

![Home画面](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe21.png "Home画面")

先ずはアカウント（ test@example.com ）を登録します。
プロジェクトを作る際に「個別のアカウント」を選択した場合は、アカウントは電子メールアドレスを使うように初期設定されています。

![アカウント登録画面](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe22.png "アカウント登録画面")

アカウント登録が終わると、ログイン状態で最初の画面に遷移します。
画面右端に「芋けんぴの注文」のリンクが表示されているのが確認できます。

![ログインHome画面](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe27.png "ログインHome画面")

では、実際に注文してカード決裁を行います。

![注文画面](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe28.png "注文画面")

注文画面「Pay with Card」ボタンをクリックすると、カード情報入力ダイアログが表示されます。
ですが、Edge、IE で表示されないことがありました。（現状ではまだ原因は不明です。）
とりあえず、以下のように入力してみます。

+ カード番号：テスト用 4242 4242 4242 4242 
+ 月/年：適当な値
+ CVC：適当な値

![カード情報入力画面](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe29.png "カード情報入力画面")

カード決済が完了すると、以下の画面に遷移します。

![決済完了画面](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe31.png "決済完了画面")

実際に Stripe に顧客、支払いが追加されたか確認します。

![Stripe 顧客](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe32.png "Stripe 顧客")

![Stripe 支払い](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe33.png "Stripe 支払い")

Stripe に無事追加されました。
ローカルDBにも、以下のように顧客（Custmer）が追加されていますね。

![local DB](https://s3-ap-northeast-1.amazonaws.com/soft-village/qiita/stripe/Stripe34b.png "local DB")

## 最後に

実際にアカウント登録、ドキュメントを参照、ASP.net MVC 5 を設定、カード決裁を実装するまで、だいたい半日程度でできました。

とりあえずカード課金を試してみましたが、本命は月・年額会費の課金対応。
以下を参考に、後日追加で試してみるつもりです。

Stripe Subscription （定期支払い）101 - Part 1（Stripe 日本 @y_toku さん）
https://qiita.com/y_toku/items/235b5e7ee00792edcbbf