feat(slice-4): honest delivery failure — send-before-commit + 502 delivery_failed (INV-9; closes #7)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,9 +18,16 @@ from datetime import datetime, timedelta, timezone
|
||||
import psycopg
|
||||
|
||||
from app.platform import config
|
||||
from app.platform.mailer import Mailer
|
||||
from app.platform.mailer import Mailer, MailerError
|
||||
|
||||
from .errors import CodeExhausted, CodeExpired, CodeMismatch, InvalidEmail, ResendCooldown
|
||||
from .errors import (
|
||||
CodeExhausted,
|
||||
CodeExpired,
|
||||
CodeMismatch,
|
||||
DeliveryFailed,
|
||||
InvalidEmail,
|
||||
ResendCooldown,
|
||||
)
|
||||
from .models import Account
|
||||
|
||||
# INV-3 constants — the one-time-code policy.
|
||||
@@ -82,17 +89,23 @@ def request_code(conn: psycopg.Connection, mailer: Mailer, email: str) -> None:
|
||||
"INSERT INTO auth_code (email, code_hash, expires_at) VALUES (%s, %s, %s)",
|
||||
(email, _hash_code(code), now + CODE_TTL),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
mailer.send(
|
||||
to=email,
|
||||
subject=f"Your ecomm code: {code}",
|
||||
body=(
|
||||
f"Your ecomm one-time code is {code}.\n"
|
||||
f"It is valid for {int(CODE_TTL.total_seconds() // 60)} minutes.\n"
|
||||
"If you didn't request this, ignore this message."
|
||||
),
|
||||
)
|
||||
# Send BEFORE commit (ecomm#7): a refused delivery rolls the row back, so no orphan
|
||||
# code blocks the 60s cooldown and the caller can honestly retry at once (INV-9).
|
||||
try:
|
||||
mailer.send(
|
||||
to=email,
|
||||
subject=f"Your ecomm code: {code}",
|
||||
body=(
|
||||
f"Your ecomm one-time code is {code}.\n"
|
||||
f"It is valid for {int(CODE_TTL.total_seconds() // 60)} minutes.\n"
|
||||
"If you didn't request this, ignore this message."
|
||||
),
|
||||
)
|
||||
except MailerError as exc:
|
||||
conn.rollback()
|
||||
raise DeliveryFailed(str(exc)) from exc
|
||||
conn.commit()
|
||||
|
||||
|
||||
def verify(conn: psycopg.Connection, email: str, code: str) -> tuple[Account, bool]:
|
||||
|
||||
Reference in New Issue
Block a user