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()