0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Django】ECサイトの決済機能・実装編:FormViewで Checkout / Payment / LineItem を保存する

Posted at

はじめに

前回の記事モデルの設計編では、

  • 決済情報を保存する Checkout モデル
  • クレジットカード情報を保存する Payment モデル
  • 「どの商品を何個買ったか」を保存する LineItem モデル
  • それらに対応する OrderForm(フォーム)

を作り、テーブル設計とフォームのバリデーションを用意しました。

この記事では、その続きとして、
FormView を使って実際に DB に保存する処理(決済処理の本体)を実装していきます。


やりたいことの全体像

決済ボタンが押されたときに、次の流れで処理を行うよう実装しました。

  1. フォームから送信された入力値(名前・住所・カード情報など)を受け取る
  2. セッションIDから現在のカートを取得する
  3. カートから合計金額・合計個数・カートアイテム一覧を取得する
  4. Checkout に1回の注文情報を保存する
  5. Payment にその注文のカード情報を保存する
  6. LineItem にそのときのカートの中身を明細として保存する
  7. カートを空にして再度買い物をできるようにする

この一連の処理を、Order という FormView を継承したクラスにまとめて書きました。


FormViewを継承して決済処理を書いていく

まずは、完成形の Order ビューを見てみます。

checkout/views.py
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.db import transaction

from .models import Checkout, Payment, LineItem
from checkout.models import Cart
from .forms import OrderForm
from checkout.utils import _ensure_cart_session


# DBに請求情報とクレジットカード情報を保存するView
class Order(FormView):
    template_name = "checkout/checkout.html"
    form_class = OrderForm
    success_url = reverse_lazy("product:product_list")

    # トランザクション処理にする
    @transaction.atomic
    def form_valid(self, form):

        # form_validは引数にrequestを取らないので、selfから取得する
        session_key = _ensure_cart_session(self.request)
        cart_obj = Cart.objects.get(session_id=session_key)

        # 合計金額・合計数量・カート内の商品一覧を取得する
        total_amount = cart_obj.calculate_total_price()
        total_quantity = cart_obj.calculate_total_quantity()
        cart_items = cart_obj.get_items()

        # formsで定義したバリデーションを通過したデータを取得する
        data = form.cleaned_data

        # 1. フォームから請求情報を取得してCheckoutに保存
        checkout = Checkout.objects.create(
            last_name=data["last_name"],
            first_name=data["first_name"],
            user_name=data["user_name"],
            email=data["email"],
            zip_code=data["zip_code"],
            prefecture=data["prefecture"],
            city=data["city"],
            street_address=data["street_address"],
            building_name=data["building_name"],
            total_amount=total_amount,
            total_quantity=total_quantity,
        )

        # 2. 決済情報をPaymentモデルに保存
        Payment.objects.create(
            checkout=checkout,
            card_holder=data["card_holder"],
            card_number=data["card_number"],      # 本来はDBに保存しない
            expiration_date=data["expiration_date"],
            cvv=data["cvv"],                      # 本来はDBに保存しない
        )

        # 3. カート内の商品情報をLineItemに明細として保存
        for item in cart_items:
            LineItem.objects.create(
                checkout=checkout,
                name=item.product.name,
                price=item.product.price,
                quantity=item.quantity,
                subtotal_amount=item.product.price * item.quantity,
            )

        # 4. カートを削除(中身を空にする)
        cart_obj.clear()

        # 最後に親クラスのform_validを呼び出す(success_urlへリダイレクト)
        return super().form_valid(form)

ここから、FormViewの基本部分 → トランザクション → カート処理の順番で解説していきます。


FormViewの基本設定

まずはクラスの上の方から見ていきます。

class Order(FormView):
    template_name = "checkout/checkout.html"
    form_class = OrderForm
    success_url = reverse_lazy("product:product_list")

template_name = "checkout/checkout.html"

  • 決済ページで使うテンプレートファイルを指定します。
  • 今回は checkout/templates/checkout/checkout.html を使う想定です。

form_class = OrderForm

  • どのフォームクラスを使うかを指定します。
  • 設計編で作った OrderForm(名前・住所・カード情報などが入っているやつ)をここで紐づけています。

success_url = reverse_lazy("product:product_list")

  • フォームの処理(form_valid)が正常に終わったあとにリダイレクトするURLを指定します。
  • reverse_lazy("product:product_list")
    product アプリの urls.pyname="product_list" とつけたURLに変換してくれます。

return super().form_valid(form)

form_valid の最後で呼んでいるこの一文は、

return super().form_valid(form)
  • 「フォームの処理が正常に終わりました」と親クラス(FormView)に教え、その結果として、success_url で指定したURLにリダイレクトされる

という役割を持っています。


トランザクション処理とは?

@transaction.atomic
def form_valid(self, form):
    ...

この @transaction.atomic が、いわゆるトランザクション処理です。

何のために使うのか?

今回の決済処理では、1回のフォーム送信の中で、

  • Checkout を作成(1回の注文)
  • Payment を作成(カード情報)
  • LineItem を複数作成(注文の中身)
  • 最後にカートを削除

という、複数のDB操作を行っています。

ここで問題なのは、

途中までDBに保存されたのに、途中でエラーになって処理が止まると中途半端な状態になってしまう

ということです。

たとえば:

  • Checkout までは保存された
  • でも LineItem の保存中にエラーが出て止まった
  • カートだけ消えて、注文の中身が一部しか保存されていない

みたいな状態は絶対に避けたいです。

transaction.atomic の動き

@transaction.atomic をつけると、

  • 中の処理が全部成功したときだけ まとめてDBに反映されるようになり、途中で例外(エラー)が発生した場合は、その処理の中で行ったDB操作をなかったことにして元に戻すロールバックという処理が行われます。

つまり、

「全部ちゃんとできた」か「何もやっていない」のどちらかに必ずなる

ようにしてくれる仕組みです。

そのおかげで、ユーザーの側から見ると、

  • ちゃんと注文できた状態
  • 何も注文されていない状態

のどちらかしか存在しなくなり、
半端なDB状態が残らずに安全な決済処理 が実現できます。


form_valid の中身を順番に追う

1. カート情報を取得する

session_key = _ensure_cart_session(self.request)
cart_obj = Cart.objects.get(session_id=session_key)
  • form_validrequest を引数に取りませんが、
    クラスベースビューでは self.request にリクエストが入っています。
  • _ensure_cart_session(self.request)
    「セッションIDがなければ作る/あればそれを返す」というヘルパー関数です。
  • そのセッションIDを使って、Cart モデルからカートを1件取得しています。

2. 合計金額・合計個数・カートアイテムを取得する

total_amount = cart_obj.calculate_total_price()
total_quantity = cart_obj.calculate_total_quantity()
cart_items = cart_obj.get_items()

これらは、Cart モデルに定義したモデルメソッドを呼び出しています。
後で、Cart モデル側のコードも載せます。

ここでやっていることはシンプルで、

  • get_items() でカートに入っている CartItem 一覧を取ってくる
  • その結果を使って合計金額・合計数量を計算している

という流れです。

3. フォームのバリデーション済みデータを取得

data = form.cleaned_data
  • OrderForm に定義したバリデーション(必須チェック・文字数チェックなど)を通過したデータだけが cleaned_data に入ります。
  • data["last_name"] のように、辞書のような形で取り出して使えます。

4. Checkout(1回分の注文)を保存

checkout = Checkout.objects.create(
    last_name=data["last_name"],
    first_name=data["first_name"],
    user_name=data["user_name"],
    email=data["email"],
    zip_code=data["zip_code"],
    prefecture=data["prefecture"],
    city=data["city"],
    street_address=data["street_address"],
    building_name=data["building_name"],
    total_amount=total_amount,
    total_quantity=total_quantity,
)

ここでは、フォームから受け取った名前・住所と、
カートから計算した合計金額・合計数量をまとめて Checkout に保存しています。

1件の Checkout が、「1回分の注文のまとまり」を表すイメージです。

5. Payment(カード情報)を保存

Payment.objects.create(
    checkout=checkout,
    card_holder=data["card_holder"],
    card_number=data["card_number"],      # 本来はDBに保存しない
    expiration_date=data["expiration_date"],
    cvv=data["cvv"],                      # 本来はDBに保存しない
)
  • checkout=checkout で、「どの注文のカード情報か」を紐づけています。
  • 繰り返しになりますが、実際のサービスではカード番号やCVVを平文でDBに保存するのはNG です。
    (Stripeなどの外部決済サービスを使うべき部分)

ここではあくまで、「Djangoでどうやって紐づいたレコードを保存するか」を学ぶためのサンプルとして入れています。

6. LineItem(注文の明細)をカートから作成

for item in cart_items:
    LineItem.objects.create(
        checkout=checkout,
        name=item.product.name,
        price=item.product.price,
        quantity=item.quantity,
        subtotal_amount=item.product.price * item.quantity,
    )
  • カート内の cart_items をループしながら、
    各商品ごとに LineItem を1レコードずつ作成しています。ここでは、nameprice をProductからコピーしていきます。

これは、設計編で説明した通り、

「あとで商品名や価格を変更しても、注文したときの情報は変わらないようにしたい」

からです。
LineItem はあくまで「注文時点のスナップショット」という役割になります。

7. カートを空にする

cart_obj.clear()

カートモデルの clear メソッドを呼び出して、カートを削除しています。
これにより、同じセッションで再度商品を追加したときは新しいカートとして扱われます。


Cartモデルのメソッド

最後に、この記事で使っている Cart モデルのメソッドも載せておきます。

checkout/models.py
class Cart(models.Model):
    class Meta:
        db_table = "cart"

    session_id = models.CharField(max_length=40, null=True, blank=True, unique=True)

    # このCartに紐づいているCartItemを、Product情報も一緒に全部取ってくる
    def get_items(self):
        return self.cartitem_set.select_related("product").all()

    # カート内の合計数量を計算するメソッド
    def calculate_total_quantity(self):
        total_quantity = sum(item.quantity for item in self.get_items())
        return total_quantity if total_quantity is not None else 0

    # カートの合計金額を計算するメソッド
    def calculate_total_price(self):
        total_amount = sum(
            item.quantity * item.product.price for item in self.get_items()
        )
        return total_amount if total_amount is not None else 0

    # カート自体を削除するメソッド
    def clear(self):
        self.delete()
  • get_items()
    このカートに紐づく CartItem を、product も一緒に取得します。

  • calculate_total_quantity()
    すべての CartItemquantity を足し合わせます。

  • calculate_total_price()
    quantity × product.price の合計を計算します。

  • clear()
    この Cart レコードを削除(ForeignKeyで紐づいているCartItemも一緒に消える想定)します。

こうしておくことで、Order View からは

cart_obj.calculate_total_price()
cart_obj.calculate_total_quantity()
cart_obj.get_items()
cart_obj.clear()

のように、「カートに関する処理」を全部 Cart モデルに任せて書けるようになっています。


さいごに

今回は、

  • FormView を使って決済処理用の Order ビューを作る方法
  • form_valid の中で Checkout / Payment / LineItem を保存する流れ
  • その際にトランザクション(@transaction.atomic)で「全部成功 or 全部なかったこと」にする考え方
  • Cart モデルのメソッドを使って、合計金額・合計数量・カートアイテムを取得する方法

をまとめました。

これで、

「フォーム送信 → 決済情報をDBに保存 → カートを空にする」

という決済機能の核の処理を完成させることができました。

次の発展編では、

  • 購入完了メールを送る処理
  • Djangoの messages フレームワークを使って「購入ありがとうございます」を画面に表示する処理

などを追加して、より実践的な決済機能に強化していく予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?