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サイトの決済機能・設計編:Checkout / Payment / LineItem と OrderForm

Posted at

はじめに

今回は、ECサイトの決済機能を作ったので、復習もかねて実装内容をまとめておきます。

やりたいことはざっくり言うとこの2つです。

  • 決済フォームから送られてきた 名前・住所・カード情報 を受け取る
  • それを データベースに保存 して、「誰が・何を・いくつ・いくらで買ったか?」がわかるようにする

そのために、まずはデータベースの設計(テーブル設計)から考えていきます。


DB設計をする

今回作るテーブルは次の3つです。

  1. チェックアウト情報(Checkout)
    名前・住所・メールアドレス・合計金額など、「注文そのもの」の情報

  2. クレジットカード情報(Payment)
    カード名義人・カード番号・有効期限などの情報
    ※実際のサービスではカード番号をDBにそのまま保存するのはNGですが、ここでは学習用サンプルとして保存しています。

  3. 注文ごとの商品明細(LineItem)
    「どの商品を」「いくらで」「何個買ったか」を1行ずつ記録するテーブル

この3つを組み合わせることで、

  • Checkout:注文の「ヘッダー」
  • LineItem:注文の「中身(明細)」
  • Payment:注文に紐づく「カード情報」

というイメージで管理していきます。


決済情報(名前・住所など)を保存するモデル

まずは、注文者の名前・住所・合計金額などを保存する Checkout モデルです。

class Checkout(models.Model):
    class Meta:
        db_table = "checkout"
        # 新しい注文が上にくるように並べる
        ordering = ["-created_at"]

    last_name = models.CharField("", max_length=30)
    first_name = models.CharField("", max_length=30)
    user_name = models.CharField("ユーザー名", max_length=30)
    email = models.EmailField("メールアドレス")
    zip_code = models.CharField("郵便番号", max_length=8)
    prefecture = models.CharField("都道府県", max_length=30)
    city = models.CharField("市区町村", max_length=30)
    street_address = models.CharField("丁目・番地・号", max_length=30)
    building_name = models.CharField("建物名・部屋番号", max_length=30, blank=True)

    total_amount = models.IntegerField("合計金額", default=0)
    total_quantity = models.IntegerField("合計数量", default=0)
    created_at = models.DateTimeField("注文日時", auto_now_add=True, null=True)
  • 1件の Checkout が「1回分の注文」を表します。
  • total_amounttotal_quantity には、後でカートの中身から計算した合計金額・合計個数を入れます。

クレジットカード情報を保存するモデル

次に、カード情報を保存する Payment モデルです。

# クレジットカード情報モデル
class Payment(models.Model):
    class Meta:
        db_table = "payment"

    checkout = models.ForeignKey(
        Checkout, verbose_name="請求情報", on_delete=models.CASCADE
    )
    card_holder = models.CharField("カード名義人", max_length=50)
    card_number = models.CharField("カード番号", max_length=19)
    expiration_date = models.CharField("有効期限", max_length=10)
    cvv = models.CharField("セキュリティコード", max_length=3)
  • checkoutCheckout への外部キーで、「どの注文のカード情報か」を表します。
  • 現実のサービスでは、カード番号/CVVなどは外部の決済サービス(Stripeなど)に任せて、自前のDBには保存はしません。
  • 今回はあくまで学習用として「こういう形で紐づけるんだな」とイメージするための例として定義しています。

決済情報と「そのとき買った商品」を紐づけるモデル

次は、1回の注文の中で「どの商品を」「いくらで」「何個買ったか」を記録する LineItem モデルです。

# 注文商品の明細モデル
class LineItem(models.Model):
    class Meta:
        db_table = "line_item"

    checkout = models.ForeignKey(
        Checkout, verbose_name="注文情報", on_delete=models.CASCADE
    )
    name = models.CharField("商品名", max_length=100)
    price = models.IntegerField("価格", default=0)
    quantity = models.IntegerField("数量", default=1)
    subtotal_amount = models.IntegerField("小計", default=0)

なぜ Product のフィールドをコピーしているのか?

ここでは

  • name
  • price
  • quantity
  • subtotal_amount

など、Productモデルにもありそうな項目を、あえてもう一度フィールドとして作っています。

「じゃあ最初から ProductCartItem に外部キーを張ればいいんじゃないの?」
と思うかもしれませんが、そうしないのには理由があります。

CartItem をFKで結ばない理由

もし LineItemCartItemProductそのまま外部キーで紐づいているだけだと、

  • あとで商品名や価格を変更したとき
    → 過去の注文データまで「新しい名前・新しい価格」で見えてしまう

という問題が起きます。

でも、本当に知りたいのは

注文当時の 商品名・価格・数量」

ですよね。

そのため、

  • 決済が完了したタイミングで
  • CartItem / Product から そのときの値をコピー
  • LineItem として「スナップショット(その時点のコピー)」を保存する

という設計にしています。

簡単にいうと、

LineItem は「その注文時点の履歴」を残すためのテーブル

というイメージです。


フォームでバリデーションを設定する(FormView用)

次に、FormView から使うための**フォームの定義(バリデーション)**を作っていきます。

forms.py を作成し、決済情報+カード情報を受け取る OrderForm を定義します。

checkout/forms.py
from django import forms


class OrderForm(forms.Form):
    # 決済情報(名前・住所など)
    last_name = forms.CharField(max_length=50)
    first_name = forms.CharField(max_length=50)
    user_name = forms.CharField(max_length=50)
    email = forms.EmailField(max_length=50)
    zip_code = forms.CharField(max_length=50)
    prefecture = forms.CharField(max_length=50)
    city = forms.CharField(max_length=50)
    street_address = forms.CharField(max_length=50)
    building_name = forms.CharField(max_length=50)

    # クレジットカード情報
    card_holder = forms.CharField(max_length=100)
    card_number = forms.CharField(max_length=50, min_length=16)
    expiration_date = forms.CharField(max_length=20)
    cvv = forms.CharField(max_length=4, min_length=3)

ここでは、最低限のバリデーションだけを付けています。

  • CharField, EmailField などの型

  • max_length / min_length で文字数のチェック

    • 例:カード番号 → 16桁以上にしたいので min_length=16

実際のサービスでは、もっと厳しいチェックを入れますが、まずは「Formを通して入力値を受け取る」段階まで作ることを優先しています。

テンプレート側との対応

ここで定義したフィールド名は、テンプレートの <input>name 属性と対応させます。

<input type="text" name="last_name" />

のように書いておくと、

  • フォームから送信された last_name の値が
  • OrderFormlast_name フィールドに入り
  • Djangoが自動でバリデーション(必須・文字数など)をしてくれる

という流れになります。

こんな感じで、

  • モデル設計で「どんな情報を、どんなテーブルに分けるか」
  • フォームで「どんな入力を受け取って、どうチェックするか」

まで整理したので、この後 FormView側で「Checkout / Payment / LineItem をまとめて作成する処理」 を書いていこうかと思います。

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?