generated from westfarn/web_django_template
Add customer accounts, shipment tracking, and purchase reviews.
CI / test (pull_request) Successful in 35s
CI / test (pull_request) Successful in 35s
Shoppers can register, save shipping details, and view order history while cards stay on Stripe. EasyPost tracker updates (including numbers from Pirate Ship) and 1–5 star reviews are limited to buyers. The contact form now only asks for email and a message.
This commit is contained in:
+90
-11
@@ -10,11 +10,11 @@ from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Sum
|
||||
from django.db.models import F, Q, Sum
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import StoredFile
|
||||
from shop.models import Order, OrderItem, Product, ProductColor, ProductImage
|
||||
from shop.models import Order, OrderItem, Product, ProductColor, ProductImage, ProductReview
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -254,6 +254,7 @@ def create_order_from_cart(
|
||||
customer_name: str = "",
|
||||
shipping_address: dict | None = None,
|
||||
notes: str = "",
|
||||
user=None,
|
||||
) -> Order:
|
||||
lines = cart_lines(session)
|
||||
if not lines:
|
||||
@@ -267,6 +268,7 @@ def create_order_from_cart(
|
||||
with transaction.atomic():
|
||||
order = Order.objects.create(
|
||||
number=next_order_number(),
|
||||
user=user if getattr(user, "is_authenticated", False) else None,
|
||||
email=email,
|
||||
customer_name=(customer_name or "").strip(),
|
||||
status=Order.Status.DRAFT,
|
||||
@@ -295,6 +297,42 @@ def create_order_from_cart(
|
||||
return order
|
||||
|
||||
|
||||
def ensure_stripe_customer(user) -> str:
|
||||
"""Create or reuse a Stripe Customer. Card data never leaves Stripe."""
|
||||
if not user or not getattr(user, "is_authenticated", False):
|
||||
return ""
|
||||
from accounts.services import get_customer_profile
|
||||
|
||||
profile = get_customer_profile(user)
|
||||
if profile.stripe_customer_id:
|
||||
return profile.stripe_customer_id
|
||||
stripe = _stripe()
|
||||
customer = stripe.Customer.create(
|
||||
email=(user.email or user.username or "") or None,
|
||||
name=(user.get_full_name() or "") or None,
|
||||
metadata={"user_id": str(user.pk)},
|
||||
)
|
||||
customer_id = getattr(customer, "id", None) or customer.get("id") or ""
|
||||
if not customer_id:
|
||||
raise ShopError("Stripe did not return a customer id.")
|
||||
profile.stripe_customer_id = customer_id
|
||||
profile.save(update_fields=["stripe_customer_id", "updated_at"])
|
||||
return customer_id
|
||||
|
||||
|
||||
def remember_stripe_customer(order: Order, customer_id: str) -> None:
|
||||
customer_id = (customer_id or "").strip()
|
||||
if not customer_id or not order.user_id:
|
||||
return
|
||||
from accounts.services import get_customer_profile
|
||||
|
||||
profile = get_customer_profile(order.user)
|
||||
if profile.stripe_customer_id:
|
||||
return
|
||||
profile.stripe_customer_id = customer_id
|
||||
profile.save(update_fields=["stripe_customer_id", "updated_at"])
|
||||
|
||||
|
||||
def create_checkout_session(order: Order, *, success_url: str, cancel_url: str) -> str:
|
||||
stripe = _stripe()
|
||||
line_items = [
|
||||
@@ -312,14 +350,27 @@ def create_checkout_session(order: Order, *, success_url: str, cancel_url: str)
|
||||
]
|
||||
if not line_items:
|
||||
raise ShopError("Order has no items.")
|
||||
session = stripe.checkout.Session.create(
|
||||
mode="payment",
|
||||
customer_email=order.email or None,
|
||||
line_items=line_items,
|
||||
metadata={"shop_order_id": str(order.pk), "order_number": order.number},
|
||||
success_url=success_url,
|
||||
cancel_url=cancel_url,
|
||||
)
|
||||
params = {
|
||||
"mode": "payment",
|
||||
"line_items": line_items,
|
||||
"metadata": {"shop_order_id": str(order.pk), "order_number": order.number},
|
||||
"success_url": success_url,
|
||||
"cancel_url": cancel_url,
|
||||
}
|
||||
customer_id = ""
|
||||
if order.user_id:
|
||||
try:
|
||||
customer_id = ensure_stripe_customer(order.user)
|
||||
except ShopError:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("stripe customer create failed for order %s", order.number)
|
||||
if customer_id:
|
||||
params["customer"] = customer_id
|
||||
params["payment_intent_data"] = {"setup_future_usage": "on_session"}
|
||||
else:
|
||||
params["customer_email"] = order.email or None
|
||||
session = stripe.checkout.Session.create(**params)
|
||||
order.stripe_checkout_session_id = session.id
|
||||
order.hosted_checkout_url = session.url or ""
|
||||
order.status = Order.Status.OPEN
|
||||
@@ -353,7 +404,9 @@ def _notify_pos(order: Order) -> None:
|
||||
enqueue_online_sale(order)
|
||||
|
||||
|
||||
def mark_paid(order: Order, *, stripe_id: str = "") -> None:
|
||||
def mark_paid(
|
||||
order: Order, *, stripe_id: str = "", stripe_customer_id: str = ""
|
||||
) -> None:
|
||||
if order.status == Order.Status.PAID:
|
||||
return
|
||||
with transaction.atomic():
|
||||
@@ -365,6 +418,7 @@ def mark_paid(order: Order, *, stripe_id: str = "") -> None:
|
||||
locked.paid_at = timezone.now()
|
||||
locked.save(update_fields=["status", "paid_at", "updated_at"])
|
||||
order.refresh_from_db()
|
||||
remember_stripe_customer(order, stripe_customer_id)
|
||||
try:
|
||||
send_order_email(order)
|
||||
except Exception:
|
||||
@@ -614,3 +668,28 @@ def product_media_payload(product: Product, colors: list[ProductColor]) -> dict:
|
||||
"available": available_qty(product, color),
|
||||
}
|
||||
return {"shared": shared, "stl": product.stl_url, "colors": color_data}
|
||||
|
||||
|
||||
_REVIEWABLE_STATUSES = (Order.Status.PAID, Order.Status.FULFILLED)
|
||||
|
||||
|
||||
def qualifying_order_for_review(user, product: Product) -> Order | None:
|
||||
if not user or not getattr(user, "is_authenticated", False):
|
||||
return None
|
||||
email = (user.email or user.username or "").strip()
|
||||
qs = (
|
||||
Order.objects.filter(
|
||||
items__product=product,
|
||||
status__in=_REVIEWABLE_STATUSES,
|
||||
)
|
||||
.filter(Q(user=user) | Q(email__iexact=email))
|
||||
.distinct()
|
||||
.order_by("-paid_at", "-created_at")
|
||||
)
|
||||
return qs.first()
|
||||
|
||||
|
||||
def user_has_reviewed(user, product: Product) -> bool:
|
||||
if not user or not getattr(user, "is_authenticated", False):
|
||||
return False
|
||||
return ProductReview.objects.filter(user=user, product=product).exists()
|
||||
|
||||
Reference in New Issue
Block a user