はじめに
ECサイトで購入ボタンをクリックすると、
設定したメールに購入完了メールが送られるという実装中に沼った話です。
-
上記のURLを参考にしましたが、私の環境下で起こった話です
別の環境下によっては問題なく実装できる場合もあります -
結論があっさりしており
確定的な原因を特定できたわけではありません
上記2点ご了承ください
仕様
請求先住所に設定した以下の内容をテンプレートを使ってメールを送信する
- ユーザ名
- 注文番号
- 商品ごとの商品名、個数、小計
- 購入金額合計
購入処理までの機能フロー
-
購入ボタンクリック
-
請求先住所、支払い方法フォーム作成
-
カートIDを取得
-
カートID内の商品を取得
-
カート内に商品がない場合はカートページへ戻る
-
バリデーションエラー時にカート内商品を保持する処理
- 空の辞書を用意
- カート内商品をループで回す
- カート内商品の商品名を変数に格納
- 商品名から4つ取得する → 小計、商品ID、商品価格、商品個数
-
バリデーションエラーになったらカートページへ戻す(以降 ValidationError→VE)
- 請求先住所のVE
- 支払い情報のVE
- カート内の商品情報
- 購入予定の合計金額
- 購入予定の商品の種類
-
トランザクション開始(以降 フォームチェック→FC)
- FCした請求先住所をDBに保存
- FCした支払い情報をDBに保存
- 注文情報をDBに保存
- 請求先住所 ID
- 支払い情報 ID
- OrderItemテーブルで商品情報を管理
- 注文ID(外部キー)
- 商品名
- 商品価格
- 商品個数
- 商品ごとの小計
- 合計購入金額
- メール送信処理
- 取得したカートを削除
- サクセスメッセージを作成
- リダイレクトで商品一覧ページへ遷移する
-
-
商品一覧ページでサクセスメッセージが出力される
- カートが削除されているため、カート内の商品個数が0になっている
DB設計
cart (カート)
カラム名(論理) | カラム名(物理) | 型 | NOT NULL | その他制約 | 説明 |
---|---|---|---|---|---|
ID | id | serial | True | PRIMARY KEY | |
セッションID | session_id | char(32) | True |
order (注文)
カラム名(論理) | カラム名(物理) | 型 | NOT NULL | その他制約 | 説明 |
---|---|---|---|---|---|
ID | id | serial | True | PRIMARY KEY | |
請求先住所 | billing_address_id | integer | True | 請求先住所の外部キー | |
支払い情報 | payment_id | integer | True | 支払い情報の外部キー |
billing_address (請求先住所)
カラム名(論理) | カラム名(物理) | 型 | NOT NULL | その他制約 | 説明 |
---|---|---|---|---|---|
ID | id | serial | True | PRIMARY KEY | |
姓 | last_name | char(20) | True | ||
名 | first_name | char(20) | True | ||
ユーザー名 | username | char(20) | True | unique | |
メールアドレス | char(30) | False | |||
国 | country | char(100) | True | ||
州/県 | state_prefecture | char(100) | True | ||
郵便番号 | zip | char(7) | True | 今回のみ7桁の郵便番号 | |
住所1 | address1 | char(255) | True | 市区町村+番地 | |
住所2 | address2 | char(255) | False | マンション、アパート名+部屋番号等 | |
請求先=配送先 | same_address | boolean | False | default=False | |
配送先住所を保存 | save_address | boolean | False | default=False |
payment (支払い)
カラム名(論理) | カラム名(物理) | 型 | NOT NULL | その他制約 | 説明 |
---|---|---|---|---|---|
ID | id | serial | True | PRIMARY KEY | |
カード名義 | card_holder | char(32) | True | ||
カード番号(ハッシュ化) | card_number_hash | char(19) | True | 4桁-4桁-4桁-4桁 間のハイフン含める | |
カード下4桁 | card_last4 | char(4) | True | ||
有効期限 | expiration_date | char(5) | True | MM/YY 形式 |
order_item (購入商品)
カラム名(論理) | カラム名(物理) | 型 | NOT NULL | その他制約 | 説明 |
---|---|---|---|---|---|
ID | id | serial | True | PRIMARY KEY | |
注文ID | order_id | integer | True | 注文情報の外部キー | |
商品名 | name | char(30) | True | 購入時の商品名 | |
価格 | price | integer | True | 購入時の商品の価格 | |
個数 | quantity | integer | True | 購入時の商品個数 | |
商品ごとの小計 | subtotal | integer | True | MM/YY 形式 |
SendGridをHeroku経由で使えるように設定する
以下の手順でSendGridを設定していきました。
SendGridのアドオン追加
以下のコマンドからHeroku経由でSendGridをアドオンする
heroku addons:create sendgrid:starter
情報を入力する
- 今回は Compny Name の部分に個人利用であることを記載
- Webサイトは X のURLを記載
これで Get Started! をクリックするとダッシュボードが立ち上がる
以下のコマンドでも立ち上げ可能
heroku addons:open sendgrid
APIキーの作成
以下の画像のように
- Settings→API Keyにアクセス
- Create API Keyをクリック
- Full Accessを選択して Create & Viewをクリック
API Keyが出力されます。コピーしておきましょう。
Doneをクリックすると二度と出力されません。
その後、API Keyを作成できる画面に戻りまして
先ほど作成したAPI Keyが出力されています。
Senderを設定する
- Settings→Sender Authenticationにアクセス
- Verify a Single Senderをクリック
- メールアドレス等を設定して Create ボタンをクリック
- 認証メールが届いて認証を通す
gmail, yahoo, icloudだと警告が出ますが問題なく作成できます。
上記で設定したアドレスに認証メールが届くので、
「Verify Single Sender」をクリックして、認証が通ると下記画面が表示される
環境変数の設定
APIキーの発行後、Herokuの環境変数に設定する。
今回はコマンドから設定していきます。
(-a以降はなくてもOK)
heroku config:set SENDGRID_API_KEY=[作成したAPIキー] -a [アプリ名]
メール実装
Django公式からAnymailを使用するのが良さげなので
こちらを導入します。
まずはAnymail
をインストールします。
pip install django-anymail
pip install django-environ
pip freeze >> requirement.txt
setttings.py
に下記設定を追記します。
...
INSTALLED_APPS = [
# ...
"anymail",
# ...
]
...
# SendGridを使用
EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend"
ANYMAIL = {
...
# SendGridのAPIキー
"SENDGRID_API_KEY": env("SENDGRID_API_KEY")
}
# メールの送信サーバー(SMTPサーバー)
EMAIL_HOST = env.str("EMAIL_HOST")
# SMTP のポート番号
EMAIL_PORT = env.int("EMAIL_PORT")
# TLS の使用
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS")
# SendGrid使用する場合は「apikey」
EMAIL_HOST_USER = env.str("EMAIL_HOST_USER")
# SMTP 認証のパスワード
# SendGrid の場合、SMTP 経由でメールを送信するときは API キーをパスワードとして使用する
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD")
# 送信元メールアドレス
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL")
# < >←は不要
SENDGRID_API_KEY=<My API Key>
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST=smtp.sendgrid.net
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=apikey
EMAIL_HOST_PASSWORD=<API Key>
DEFAULT_FROM_EMAIL=<My Email>
ここからviews.py
とmodels.py
を記載
class OrderView(View):
def post(self, request):
# 請求先住所フォームと支払いフォームを生成
billing_address_form = BillingAddressForm(request.POST)
payment_form = PaymentForm(request.POST)
# カートIDを取得、ない場合エラーを返してリダイレクト
cart = Cart.get_or_create_cart(self.request)
cart_products = cart.cart_products.select_related("product")
if not cart_products:
messages.error(request, "カート内に商品がありません")
return render(request, "cart/cart_page.html", {
"billing_address_form": billing_address_form,
"payment_form": payment_form,
})
# エラーになった際にカート内の商品情報を保持すための処理
product_data = {}
for cart_product in cart_products:
product_name = cart_product.product.name
product_data[product_name] = {
"subtotal": cart_product.sub_total_price(),
"id": cart_product.id,
"price": cart_product.product.price,
"quantity": cart_product.quantity,
}
# どちらかがエラーの場合、エラーメッセージと共に元の画面へリダイレクト
if not billing_address_form.is_valid() or not payment_form.is_valid():
messages.error(request, "入力に間違いがあります")
return render(request, "cart/cart_page.html", {
"billing_address_form": billing_address_form,
"payment_form": payment_form,
"product_data": product_data,
"total_cart_price": cart.total_price,
"total_type_products": len(product_data),
})
# トランザクション内でDB処理を一括実行
with transaction.atomic():
# フォームチェックした請求先住所をDBに保存
billing_address = billing_address_form.save()
# フォームチェックした支払い方法をDBに保存
payment = payment_form.save()
# 注文情報をDBに保存
order = Order.objects.create(
billing_address=billing_address,
payment=payment
)
# カート内の商品をOrderItemテーブルに保存
for product_name, product_info in product_data.items():
order_item = OrderItem.objects.create(
order=order,
name=product_name,
price=product_info["price"],
quantity=product_info["quantity"],
subtotal=product_info["subtotal"]
)
# ここから本題
# メールを設定していたらメール送信
try:
if billing_address.email:
order.send_email(
username=billing_address.username,
order_id=order.id,
order_items=product_data,
total_price=sum(item["subtotal"] for item in product_data.values()),
email=billing_address.email
)
print("メール送信完了")
except:
print(("メール送信できませんでした")
# カートを削除
Cart.objects.filter(id=cart.id).delete()
messages.success(request, "ご購入ありがとうございます")
return redirect(reverse("products:index"))
from django.core.mail import send_mail
...
def send_email(self, username, order_id, order_items, total_price, email):
# テンプレート側で使えるように必要な情報をcontextに追加
context = {
"username": username,
"order_id": order_id,
"order_items": order_items,
"total_price": total_price
}
subject = "【購入確定メール】野球魂"
text_content = render_to_string("email/order_confirmation.txt", context) # テキスト版
html_content = render_to_string("email/order_confirmation.html", context) # HTML版
email = send_mail(
subject=subject,
message=text_content,
from_email=env.str("DEFAULT_FROM_EMAIL"),
recipient_list=[email],
html_message=html_content,
fail_silently=False,
)
return True
これでカートに商品を追加して
購入ボタンをクリックして
「メール送信完了」のデバック出力とサクセスメッセージが出力されました!!
私はここで詰まりました。
ずっとSendGrid側のActivityがProcessedのままでした
原因を考える
結論だけ知りたい方は飛ばしてください
- 環境変数の確認
- 全て確認済み
- print()でデバック
- 1行ずつ確認してもエラーになる部分がありませんでした
- dockerを再起動
- 再起動後、改めて購入ボタンをクリックしてもメールが送られませんでした
- APIキーの再生成
- 再生成も意味なし
- gmailを不使用にする
- yahooメール, icloudメールを使用しても意味なし
- pip install django-anymailを再インストール
- 意味なし
結論
アカウントを新しく作成し直して新しいAPI Keyを作成しました。
そうするとメールが送信できました。
嬉しくて何回も送ってしまいました!!!!
憶測ですが、Heroku経由からSendGridを作成したことが
バグにつながったのではないかと考えております。