Files
santeh-ray/orders/services.py
Azimkin a84b9af926
All checks were successful
Build and Push Docker Image / build (push) Successful in 26s
Implement guest buy now checkout
2026-06-15 23:16:10 +02:00

215 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from dataclasses import dataclass
from decimal import Decimal
from typing import Any, Sequence
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.db import transaction
from django.template.loader import render_to_string
from django.utils import formats
from items.models import Item
from orders.models import Cart, CartItem, Order, OrderItem
DELIVERY_PRICE = Decimal("50.00")
FLOOR_LIFT_PRICE = Decimal("45.00")
ZERO_MONEY = Decimal("0.00")
@dataclass(frozen=True)
class CheckoutLine:
item: Item
quantity: int = 1
selected_color: Any = None
id: str = "direct"
def format_currency(value: Decimal) -> str:
return f"{formats.number_format(value, 2)} руб."
def calculate_checkout_totals(checkout_items: Sequence[Any], delivery_method: str) -> dict[str, Decimal]:
products_total = sum(
(checkout_item.item.price * checkout_item.quantity for checkout_item in checkout_items),
start=ZERO_MONEY,
)
discount_total = ZERO_MONEY
delivery_total = DELIVERY_PRICE if delivery_method == Order.DeliveryMethod.COURIER else ZERO_MONEY
floor_lift_total = FLOOR_LIFT_PRICE if delivery_method == Order.DeliveryMethod.COURIER else ZERO_MONEY
total = products_total - discount_total + delivery_total + floor_lift_total
return {
"products_total": products_total,
"discount_total": discount_total,
"delivery_total": delivery_total,
"floor_lift_total": floor_lift_total,
"total": total,
}
def get_or_create_cart(user: Any) -> Cart:
cart, _ = Cart.objects.get_or_create(user=user)
return cart
def get_user_cart_item_ids(user: Any) -> set[int]:
if not getattr(user, "is_authenticated", False):
return set()
return set(
CartItem.objects.filter(cart__user=user).values_list("item_id", flat=True)
)
def get_user_cart(user: Any) -> Cart | None:
if not getattr(user, "is_authenticated", False):
return None
return (
Cart.objects.filter(user=user)
.prefetch_related("items__item__images")
.select_related("user")
.first()
)
def get_user_cart_items(user: Any) -> list[CartItem]:
cart = get_user_cart(user)
if not cart:
return []
return list(
cart.items
.select_related("item__category", "item__subcategory", "selected_color")
.prefetch_related("item__images")
)
def add_item_to_cart(user: Any, item: Item, quantity: int = 1, color=None) -> tuple[CartItem, bool]:
if quantity < 1:
raise ValidationError("Количество товара должно быть не меньше 1.")
if not item.is_available:
raise ValidationError("Недоступный товар нельзя добавить в корзину.")
cart = get_or_create_cart(user)
cart_item, created = CartItem.objects.get_or_create(
cart=cart,
item=item,
defaults={"quantity": quantity, "selected_color": color},
)
if not created and cart_item.selected_color_id != (color.pk if color else None):
cart_item.selected_color = color
cart_item.save(update_fields=["selected_color", "updated_at"])
return cart_item, created
def remove_item_from_cart(user: Any, item_id: int) -> bool:
deleted, _ = CartItem.objects.filter(cart__user=user, item_id=item_id).delete()
return bool(deleted)
def update_cart_item_quantity(user: Any, item_id: int, quantity: int) -> CartItem:
if quantity < 1:
raise ValidationError("Количество товара должно быть не меньше 1.")
cart_item = CartItem.objects.select_related("item").get(cart__user=user, item_id=item_id)
cart_item.quantity = quantity
cart_item.save(update_fields=["quantity", "updated_at"])
return cart_item
@transaction.atomic
def create_order_from_checkout_items(
user: Any | None,
checkout_items: Sequence[Any],
cleaned_data: dict[str, Any],
) -> Order:
if not checkout_items:
raise ValidationError("Нет товаров для оформления заказа.")
order_user = user if getattr(user, "is_authenticated", False) else None
totals = calculate_checkout_totals(checkout_items, cleaned_data["delivery_method"])
order = Order.objects.create(
user=order_user,
customer_name=cleaned_data["customer_name"],
phone=cleaned_data["phone"],
email=cleaned_data["email"],
delivery_method=cleaned_data["delivery_method"],
region=cleaned_data.get("region", ""),
address=cleaned_data.get("address", ""),
payment_method=cleaned_data["payment_method"],
promo_code=cleaned_data.get("promo_code", ""),
products_total=totals["products_total"],
discount_total=totals["discount_total"],
delivery_total=totals["delivery_total"],
floor_lift_total=totals["floor_lift_total"],
total=totals["total"],
status=Order.Status.PAID,
)
OrderItem.objects.bulk_create(
[
OrderItem(
order=order,
item=cart_item.item,
item_name=cart_item.item.name,
item_slug=cart_item.item.slug,
item_price=cart_item.item.price,
quantity=cart_item.quantity,
line_total=cart_item.item.price * cart_item.quantity,
)
for cart_item in checkout_items
]
)
return order
@transaction.atomic
def create_order_from_cart(user: Any, cleaned_data: dict[str, Any]) -> Order:
cart = get_user_cart(user)
if not cart:
raise ValidationError("Корзина пуста.")
cart_items = list(cart.items.select_related("item"))
if not cart_items:
raise ValidationError("Корзина пуста.")
order = create_order_from_checkout_items(user, cart_items, cleaned_data)
cart.items.all().delete()
return order
def create_order_from_item(user: Any | None, item: Item, cleaned_data: dict[str, Any]) -> Order:
if not item.is_available:
raise ValidationError("Недоступный товар нельзя оформить.")
return create_order_from_checkout_items(user, [CheckoutLine(item=item)], cleaned_data)
def send_order_confirmation_email(order: Order) -> None:
order = (
Order.objects.select_related("user")
.prefetch_related("items")
.get(pk=order.pk)
)
context = {
"order": order,
"site_name": getattr(settings, "SITE_NAME", "SantehRay"),
}
subject = render_to_string("orders/emails/order_confirmation_subject.txt", context).strip()
text_body = render_to_string("orders/emails/order_confirmation_email.txt", context)
html_body = render_to_string("orders/emails/order_confirmation_email.html", context)
message = EmailMultiAlternatives(
subject=subject,
body=text_body,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[order.email],
bcc=[settings.ADMINISTRATOR_EMAIL],
)
message.attach_alternative(html_body, "text/html")
message.send()