feat: implement order management and checkout functionality
All checks were successful
Build and Push Docker Image / build (push) Successful in 50s

- Added `Order` and `OrderItem` models with migrations for managing orders and their details
- Created `CheckoutForm` for validating order data and customer information
- Built templates for order confirmation, checkout success, and emails (HTML and plain text)
- Added logic for email notifications upon successful order placement
- Enhanced shopping cart template with checkout panel integration
- Updated styles and JavaScript for streamlined checkout UX
- Refactored cart view to support item summaries and final totals during checkout
This commit is contained in:
2026-05-25 16:45:42 +02:00
parent 438d547fb4
commit 81e30f853d
16 changed files with 1271 additions and 262 deletions

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from orders.models import Cart, CartItem
from orders.models import Cart, CartItem, Order, OrderItem
class CartItemInline(admin.TabularInline):
@@ -20,3 +20,44 @@ class CartItemAdmin(admin.ModelAdmin):
list_display = ("item", "cart", "quantity", "updated_at")
list_select_related = ("cart", "item", "cart__user")
search_fields = ("item__name", "cart__user__username", "cart__user__email")
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
readonly_fields = ("item", "item_name", "item_slug", "item_price", "quantity", "line_total")
can_delete = False
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ("id", "customer_name", "email", "phone", "status", "total", "created_at")
list_filter = ("status", "delivery_method", "payment_method", "created_at")
list_select_related = ("user",)
search_fields = ("id", "customer_name", "email", "phone", "user__username", "user__email")
readonly_fields = (
"user",
"customer_name",
"phone",
"email",
"delivery_method",
"region",
"address",
"payment_method",
"promo_code",
"products_total",
"discount_total",
"delivery_total",
"floor_lift_total",
"total",
"status",
"created_at",
)
inlines = [OrderItemInline]
@admin.register(OrderItem)
class OrderItemAdmin(admin.ModelAdmin):
list_display = ("item_name", "order", "quantity", "item_price", "line_total")
list_select_related = ("order", "item")
search_fields = ("item_name", "order__customer_name", "order__email")

56
orders/forms.py Normal file
View File

@@ -0,0 +1,56 @@
from django import forms
from orders.models import Order
class CheckoutForm(forms.Form):
delivery_method = forms.ChoiceField(
choices=Order.DeliveryMethod.choices,
initial=Order.DeliveryMethod.COURIER,
widget=forms.RadioSelect,
label="Способ доставки",
)
region = forms.CharField(max_length=255, required=False, label="Регион")
address = forms.CharField(max_length=512, required=False, label="Адрес")
payment_method = forms.ChoiceField(
choices=Order.PaymentMethod.choices,
initial=Order.PaymentMethod.CARD,
widget=forms.RadioSelect,
label="Способ оплаты",
)
customer_name = forms.CharField(max_length=255, label="ФИО")
phone = forms.CharField(max_length=32, label="Телефон")
email = forms.EmailField(label="E-mail")
recipient_matches_customer = forms.BooleanField(required=False, initial=True)
promo_code = forms.CharField(max_length=64, required=False, label="Промокод")
terms_accepted = forms.BooleanField(required=True, label="Согласие с условиями")
def clean_email(self):
return self.cleaned_data["email"].strip().lower()
def clean_promo_code(self):
return self.cleaned_data["promo_code"].strip()
def clean_customer_name(self):
return self.cleaned_data["customer_name"].strip()
def clean_phone(self):
return self.cleaned_data["phone"].strip()
def clean_region(self):
return self.cleaned_data["region"].strip()
def clean_address(self):
return self.cleaned_data["address"].strip()
def clean(self):
cleaned_data = super().clean()
delivery_method = cleaned_data.get("delivery_method")
if delivery_method == Order.DeliveryMethod.COURIER:
if not cleaned_data.get("region"):
self.add_error("region", "Укажите регион доставки.")
if not cleaned_data.get("address"):
self.add_error("address", "Укажите адрес доставки.")
return cleaned_data

View File

@@ -0,0 +1,63 @@
# Generated by Django 6.0.5 on 2026-05-21 11:58
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('items', '0003_subcategory_item_subcategory_and_more'),
('orders', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('customer_name', models.CharField(max_length=255, verbose_name='ФИО')),
('phone', models.CharField(max_length=32, verbose_name='Телефон')),
('email', models.EmailField(max_length=254, verbose_name='E-mail')),
('delivery_method', models.CharField(choices=[('courier', 'Курьерская доставка'), ('pickup', 'Самовывоз')], default='courier', max_length=16, verbose_name='Способ доставки')),
('region', models.CharField(blank=True, max_length=255, verbose_name='Регион')),
('address', models.CharField(blank=True, max_length=512, verbose_name='Адрес')),
('payment_method', models.CharField(choices=[('card', 'Банковская карта'), ('credit', 'Кредит'), ('installment', 'Рассрочка')], default='card', max_length=16, verbose_name='Способ оплаты')),
('promo_code', models.CharField(blank=True, max_length=64, verbose_name='Промокод')),
('products_total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Сумма товаров')),
('discount_total', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Скидка')),
('delivery_total', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Доставка')),
('floor_lift_total', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Подъём на этаж')),
('total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Итого к оплате')),
('status', models.CharField(choices=[('paid', 'Оплачен')], default='paid', max_length=16, verbose_name='Статус')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
],
options={
'verbose_name': 'Заказ',
'verbose_name_plural': 'Заказы',
'ordering': ('-created_at', '-id'),
},
),
migrations.CreateModel(
name='OrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item_name', models.CharField(max_length=255, verbose_name='Название товара')),
('item_slug', models.SlugField(blank=True, verbose_name='URL товара')),
('item_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Цена товара')),
('quantity', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='Количество')),
('line_total', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Сумма строки')),
('item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_entries', to='items.item', verbose_name='Товар')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.order', verbose_name='Заказ')),
],
options={
'verbose_name': 'Позиция заказа',
'verbose_name_plural': 'Позиции заказа',
'ordering': ('id',),
},
),
]

View File

@@ -57,3 +57,92 @@ class CartItem(models.Model):
@property
def subtotal(self):
return self.item.price * self.quantity
class Order(models.Model):
class DeliveryMethod(models.TextChoices):
COURIER = "courier", "Курьерская доставка"
PICKUP = "pickup", "Самовывоз"
class PaymentMethod(models.TextChoices):
CARD = "card", "Банковская карта"
CREDIT = "credit", "Кредит"
INSTALLMENT = "installment", "Рассрочка"
class Status(models.TextChoices):
PAID = "paid", "Оплачен"
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="orders",
verbose_name="Пользователь",
)
customer_name = models.CharField(max_length=255, verbose_name="ФИО")
phone = models.CharField(max_length=32, verbose_name="Телефон")
email = models.EmailField(verbose_name="E-mail")
delivery_method = models.CharField(
max_length=16,
choices=DeliveryMethod.choices,
default=DeliveryMethod.COURIER,
verbose_name="Способ доставки",
)
region = models.CharField(max_length=255, blank=True, verbose_name="Регион")
address = models.CharField(max_length=512, blank=True, verbose_name="Адрес")
payment_method = models.CharField(
max_length=16,
choices=PaymentMethod.choices,
default=PaymentMethod.CARD,
verbose_name="Способ оплаты",
)
promo_code = models.CharField(max_length=64, blank=True, verbose_name="Промокод")
products_total = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Сумма товаров")
discount_total = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Скидка")
delivery_total = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Доставка")
floor_lift_total = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Подъём на этаж")
total = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Итого к оплате")
status = models.CharField(
max_length=16,
choices=Status.choices,
default=Status.PAID,
verbose_name="Статус",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
class Meta:
ordering = ("-created_at", "-id")
verbose_name = "Заказ"
verbose_name_plural = "Заказы"
def __str__(self):
return f"Заказ №{self.pk}"
class OrderItem(models.Model):
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name="items",
verbose_name="Заказ",
)
item = models.ForeignKey(
Item,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="order_entries",
verbose_name="Товар",
)
item_name = models.CharField(max_length=255, verbose_name="Название товара")
item_slug = models.SlugField(blank=True, verbose_name="URL товара")
item_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Цена товара")
quantity = models.PositiveIntegerField(validators=[MinValueValidator(1)], verbose_name="Количество")
line_total = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Сумма строки")
class Meta:
ordering = ("id",)
verbose_name = "Позиция заказа"
verbose_name_plural = "Позиции заказа"
def __str__(self):
return f"{self.item_name} x {self.quantity}"

View File

@@ -1,17 +1,45 @@
from decimal import Decimal
from typing import Any
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
from orders.models import Cart, CartItem, Order, OrderItem
DELIVERY_PRICE = Decimal("500.00")
FLOOR_LIFT_PRICE = Decimal("45.00")
ZERO_MONEY = Decimal("0.00")
def format_currency(value: Decimal) -> str:
return f"{formats.number_format(value, 2)} руб."
def calculate_checkout_totals(cart_items: list[CartItem], delivery_method: str) -> dict[str, Decimal]:
products_total = sum(
(cart_item.item.price * cart_item.quantity for cart_item in cart_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
@@ -38,6 +66,16 @@ def get_user_cart(user: Any) -> Cart | None:
)
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").prefetch_related("item__images")
)
def add_item_to_cart(user: Any, item: Item, quantity: int = 1) -> tuple[CartItem, bool]:
if quantity < 1:
raise ValidationError("Количество товара должно быть не меньше 1.")
@@ -67,3 +105,75 @@ def update_cart_item_quantity(user: Any, item_id: int, quantity: int) -> CartIte
cart_item.quantity = quantity
cart_item.save(update_fields=["quantity", "updated_at"])
return cart_item
@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("Корзина пуста.")
totals = calculate_checkout_totals(cart_items, cleaned_data["delivery_method"])
order = Order.objects.create(
user=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 cart_items
]
)
cart.items.all().delete()
return order
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()

View File

@@ -1,16 +1,21 @@
from django.contrib.auth import get_user_model
from django.core import mail
from django.db import IntegrityError
from django.test import TestCase, override_settings
from django.urls import reverse
from items.models import Category, Item, ItemImage, Subcategory
from orders.models import Cart, CartItem
from orders.models import Cart, CartItem, Order, OrderItem
User = get_user_model()
@override_settings(MEDIA_URL="/media/")
@override_settings(
MEDIA_URL="/media/",
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
ADMINISTRATOR_EMAIL="admin@example.com",
)
class CartFlowTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
@@ -40,6 +45,22 @@ class CartFlowTests(TestCase):
is_available=False,
)
def checkout_data(self, **overrides):
data = {
"delivery_method": "courier",
"region": "Минск",
"address": "ул. Тимирязева, 123",
"payment_method": "card",
"customer_name": "Иван Петров",
"phone": "+375296881819",
"email": "buyer@example.com",
"recipient_matches_customer": "on",
"promo_code": "SPRING",
"terms_accepted": "on",
}
data.update(overrides)
return data
def test_cart_item_is_unique_per_cart(self):
cart = Cart.objects.create(user=self.user)
CartItem.objects.create(cart=cart, item=self.item, quantity=1)
@@ -99,8 +120,10 @@ class CartFlowTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Кухонная мойка")
self.assertContains(response, "Оформление заказа")
self.assertContains(response, "1600,00 руб.")
self.assertContains(response, "3200,00 руб.")
self.assertContains(response, "3745,00 руб.")
self.assertContains(response, "/media/items/cart-item.jpg")
def test_cart_page_redirects_guest_to_login(self):
@@ -108,3 +131,101 @@ class CartFlowTests(TestCase):
self.assertEqual(response.status_code, 302)
self.assertIn(reverse("users:login"), response["Location"])
def test_checkout_creates_paid_order_clears_cart_and_sends_email(self):
cart = Cart.objects.create(user=self.user)
CartItem.objects.create(cart=cart, item=self.item, quantity=2)
self.client.force_login(self.user)
response = self.client.post(reverse("orders:checkout"), data=self.checkout_data())
order = Order.objects.get()
self.assertRedirects(response, reverse("orders:success", kwargs={"order_id": order.id}))
self.assertEqual(order.status, Order.Status.PAID)
self.assertEqual(order.products_total, 3200)
self.assertEqual(order.discount_total, 0)
self.assertEqual(order.delivery_total, 500)
self.assertEqual(order.floor_lift_total, 45)
self.assertEqual(order.total, 3745)
self.assertEqual(order.promo_code, "SPRING")
self.assertFalse(CartItem.objects.filter(cart=cart).exists())
order_item = OrderItem.objects.get(order=order)
self.assertEqual(order_item.item_name, "Кухонная мойка")
self.assertEqual(order_item.quantity, 2)
self.assertEqual(order_item.line_total, 3200)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ["buyer@example.com"])
self.assertEqual(mail.outbox[0].bcc, ["admin@example.com"])
self.assertIn(f"заказ №{order.id}", mail.outbox[0].subject.lower())
self.assertIn("Кухонная мойка", mail.outbox[0].body)
def test_checkout_rejects_empty_cart(self):
self.client.force_login(self.user)
response = self.client.post(reverse("orders:checkout"), data=self.checkout_data())
self.assertRedirects(response, reverse("orders:cart"))
self.assertFalse(Order.objects.exists())
self.assertEqual(len(mail.outbox), 0)
def test_checkout_requires_address_for_courier_delivery(self):
cart = Cart.objects.create(user=self.user)
CartItem.objects.create(cart=cart, item=self.item, quantity=1)
self.client.force_login(self.user)
response = self.client.post(
reverse("orders:checkout"),
data=self.checkout_data(region="", address=""),
)
self.assertEqual(response.status_code, 400)
self.assertContains(response, "Укажите регион доставки.", status_code=400)
self.assertContains(response, "Укажите адрес доставки.", status_code=400)
self.assertFalse(Order.objects.exists())
def test_checkout_pickup_does_not_require_address_or_add_services(self):
cart = Cart.objects.create(user=self.user)
CartItem.objects.create(cart=cart, item=self.item, quantity=1)
self.client.force_login(self.user)
response = self.client.post(
reverse("orders:checkout"),
data=self.checkout_data(delivery_method="pickup", region="", address=""),
)
order = Order.objects.get()
self.assertRedirects(response, reverse("orders:success", kwargs={"order_id": order.id}))
self.assertEqual(order.delivery_total, 0)
self.assertEqual(order.floor_lift_total, 0)
self.assertEqual(order.total, 1600)
def test_success_page_is_only_available_to_order_owner(self):
other_user = User.objects.create_user(
username="other-user",
email="other@example.com",
password="StrongPassword123",
is_active=True,
)
order = Order.objects.create(
user=self.user,
customer_name="Иван Петров",
phone="+375296881819",
email="buyer@example.com",
delivery_method=Order.DeliveryMethod.COURIER,
region="Минск",
address="ул. Тимирязева, 123",
payment_method=Order.PaymentMethod.CARD,
products_total=1600,
discount_total=0,
delivery_total=500,
floor_lift_total=45,
total=2145,
status=Order.Status.PAID,
)
self.client.force_login(other_user)
response = self.client.get(reverse("orders:success", kwargs={"order_id": order.id}))
self.assertEqual(response.status_code, 404)

View File

@@ -10,4 +10,6 @@ urlpatterns = [
path("cart/add/", views.add, name="add"),
path("cart/remove/", views.remove, name="remove"),
path("cart/update-quantity/", views.update_quantity, name="update_quantity"),
path("checkout/", views.checkout, name="checkout"),
path("success/<int:order_id>/", views.success, name="success"),
]

View File

@@ -1,5 +1,3 @@
from decimal import Decimal
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
@@ -10,12 +8,16 @@ from django.utils.http import url_has_allowed_host_and_scheme
from django.views.decorators.http import require_POST
from items.models import Item
from orders.models import CartItem
from orders.forms import CheckoutForm
from orders.models import CartItem, Order
from orders.services import (
add_item_to_cart,
calculate_checkout_totals,
create_order_from_cart,
format_currency,
get_user_cart,
get_user_cart_items,
remove_item_from_cart,
send_order_confirmation_email,
update_cart_item_quantity,
)
@@ -33,29 +35,78 @@ def resolve_next_url(request: HttpRequest, fallback: str) -> str:
@login_required
def cart(request: HttpRequest) -> HttpResponse:
cart_items: list[CartItem] = []
total_quantity = 0
total_amount = "0,00 руб."
cart_obj = get_user_cart(request.user)
if cart_obj:
cart_items = list(
cart_obj.items.select_related("item__category", "item__subcategory").prefetch_related("item__images")
)
total_quantity = sum(cart_item.quantity for cart_item in cart_items)
total_amount_value = sum(
(cart_item.item.price * cart_item.quantity for cart_item in cart_items),
start=Decimal("0.00"),
)
total_amount = format_currency(total_amount_value)
cart_items = get_user_cart_items(request.user)
total_quantity = sum(cart_item.quantity for cart_item in cart_items)
totals = calculate_checkout_totals(cart_items, Order.DeliveryMethod.COURIER)
form = CheckoutForm(initial={
"customer_name": request.user.get_full_name() or request.user.get_username(),
"email": request.user.email,
})
return render(request, "orders/cart.html", {
"cart_items": cart_items,
"checkout_form": form,
"total_quantity": total_quantity,
"total_amount": total_amount,
"products_total_value": totals["products_total"],
"products_total": format_currency(totals["products_total"]),
"discount_total": format_currency(totals["discount_total"]),
"delivery_total": format_currency(totals["delivery_total"]),
"floor_lift_total": format_currency(totals["floor_lift_total"]),
"total_amount": format_currency(totals["total"]),
})
@login_required
@require_POST
def checkout(request: HttpRequest) -> HttpResponse:
cart_items = get_user_cart_items(request.user)
if not cart_items:
messages.error(request, "Корзина пуста. Добавьте товары перед оформлением заказа.")
return redirect("orders:cart")
form = CheckoutForm(request.POST)
if not form.is_valid():
total_quantity = sum(cart_item.quantity for cart_item in cart_items)
delivery_method = request.POST.get("delivery_method") or Order.DeliveryMethod.COURIER
totals = calculate_checkout_totals(cart_items, delivery_method)
return render(request, "orders/cart.html", {
"cart_items": cart_items,
"checkout_form": form,
"total_quantity": total_quantity,
"products_total_value": totals["products_total"],
"products_total": format_currency(totals["products_total"]),
"discount_total": format_currency(totals["discount_total"]),
"delivery_total": format_currency(totals["delivery_total"]),
"floor_lift_total": format_currency(totals["floor_lift_total"]),
"total_amount": format_currency(totals["total"]),
}, status=400)
try:
order = create_order_from_cart(request.user, form.cleaned_data)
except ValidationError as error:
messages.error(request, error.message)
return redirect("orders:cart")
try:
send_order_confirmation_email(order)
except Exception:
messages.error(request, "Заказ создан, но письмо с деталями не удалось отправить. Мы свяжемся с вами.")
else:
messages.success(request, "Оплата прошла успешно. Детали заказа отправлены на почту.")
return redirect("orders:success", order_id=order.id)
@login_required
def success(request: HttpRequest, order_id: int) -> HttpResponse:
order = get_object_or_404(
Order.objects.prefetch_related("items"),
pk=order_id,
user=request.user,
)
return render(request, "orders/success.html", {"order": order})
@login_required
@require_POST
def add(request: HttpRequest) -> HttpResponse: