Skip to content

Commit

Permalink
Merge pull request #12 from mohamad-liyaghi/orders
Browse files Browse the repository at this point in the history
Base Orders
  • Loading branch information
mohamad-liyaghi authored Aug 8, 2024
2 parents 97701f0 + 2e51c10 commit 64d27c4
Show file tree
Hide file tree
Showing 24 changed files with 475 additions and 6 deletions.
Empty file added apps/orders/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions apps/orders/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.contrib import admin
from .models import Order, OrderItem


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = (
"id",
"user",
"restaurant",
"status",
"created_at",
"updated_at",
"total_price",
)
list_filter = ("status", "restaurant")
search_fields = ("user__email", "restaurant__name")
date_hierarchy = "created_at"

def get_queryset(self, request):
return super().get_queryset(request).select_related("user", "restaurant")


@admin.register(OrderItem)
class OrderItemAdmin(admin.ModelAdmin):
list_display = (
"id",
"order",
"product",
"quantity",
"price",
"created_at",
"updated_at",
)
list_filter = ("order", "product")
search_fields = ("order__user__email", "product__name")
date_hierarchy = "created_at"

def get_queryset(self, request):
return super().get_queryset(request).select_related("order", "product")
6 changes: 6 additions & 0 deletions apps/orders/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class OrdersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "orders"
9 changes: 9 additions & 0 deletions apps/orders/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.db import models


class OrderStatus(models.TextChoices):
PENDING_PAYMENT = "pp", "Pending Payment"
PROCESSING = "pr", "Processing"
SHIPPED = "sh", "Shipped"
DELIVERED = "de", "Delivered"
CANCELLED = "ca", "Cancelled"
2 changes: 2 additions & 0 deletions apps/orders/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class EmptyCartException(Exception):
pass
97 changes: 97 additions & 0 deletions apps/orders/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Generated by Django 5.0.7 on 2024-08-08 08:39

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
("products", "0003_product_is_deleted"),
("restaurants", "0003_restaurant_is_soft_deleted"),
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",
),
),
(
"uuid",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
(
"status",
models.CharField(
choices=[
("pp", "Pending Payment"),
("pr", "Processing"),
("sh", "Shipped"),
("de", "Delivered"),
("ca", "Cancelled"),
],
default="pp",
max_length=2,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("total", models.DecimalField(decimal_places=2, max_digits=10)),
(
"restaurant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="restaurants.restaurant",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="OrderItem",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("quantity", models.PositiveIntegerField()),
("price", models.DecimalField(decimal_places=2, max_digits=10)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"order",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="orders.order"),
),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="products.product",
),
),
],
),
]
17 changes: 17 additions & 0 deletions apps/orders/migrations/0002_rename_total_order_total_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.7 on 2024-08-08 08:40

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("orders", "0001_initial"),
]

operations = [
migrations.RenameField(
model_name="order",
old_name="total",
new_name="total_price",
),
]
17 changes: 17 additions & 0 deletions apps/orders/migrations/0003_order_is_removed_from_balance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.7 on 2024-08-08 09:17

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("orders", "0002_rename_total_order_total_price"),
]

operations = [
migrations.AddField(
model_name="order",
name="is_removed_from_balance",
field=models.BooleanField(default=False),
),
]
Empty file.
85 changes: 85 additions & 0 deletions apps/orders/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from django.db import models, transaction
from django.core.cache import cache
from decouple import config
from django.conf import settings
from django.db.models import QuerySet, F
from uuid import uuid4
from orders.exceptions import EmptyCartException
from orders.enums import OrderStatus
from restaurants.models import Restaurant
from products.models import Product


class Order(models.Model):
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
status = models.CharField(max_length=2, choices=OrderStatus.choices, default=OrderStatus.PENDING_PAYMENT)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
total_price = models.DecimalField(max_digits=10, decimal_places=2)
is_removed_from_balance = models.BooleanField(default=False)

def __str__(self):
return f"{self.id} - {self.user.email} - {self.restaurant.name} - {self.status}"

def save(self, *args, **kwargs):
if self.status == OrderStatus.PROCESSING and not self.is_removed_from_balance:
self.user.balance -= self.total_price
self.user.save()
return super().save(*args, **kwargs)

@classmethod
def create_order(cls, user: settings.AUTH_USER_MODEL, cart: dict) -> QuerySet:
if not cart:
raise EmptyCartException

restaurant_ids = set(item["restaurant_id"] for item in cart.values())
orders = {
restaurant_id: cls(user=user, restaurant_id=restaurant_id, total_price=0)
for restaurant_id in restaurant_ids
}

# Bulk create Order instances
Order.objects.bulk_create(orders.values())

# Prepare for bulk creation of OrderItem instances
order_items = []
product_ids = [item["product_id"] for item in cart.values()]
products = {product.id: product for product in Product.objects.filter(id__in=product_ids)}

for item in cart.values():
product = products[item["product_id"]]
order = orders[item["restaurant_id"]]
order_item = OrderItem(
order=order,
product=product,
quantity=item["quantity"],
price=product.price,
)
order.total_price += order_item.price * order_item.quantity
order_items.append(order_item)

with transaction.atomic():
OrderItem.objects.bulk_create(order_items)
for order in orders.values():
order.save()

# Clear the cache for the user's cart
cache.delete_many(
[config("CART_CACHE_KEY").format(user_id=user.id, product_id=product_id) for product_id in product_ids]
)

return Order.objects.filter(id__in=[order.id for order in orders.values()])


class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return f"{self.id} - {self.order.id} - {self.product.name} - {self.quantity}"
Empty file added apps/orders/serializers.py
Empty file.
Empty file added apps/orders/tests/__init__.py
Empty file.
93 changes: 93 additions & 0 deletions apps/orders/tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import pytest
from orders.models import Order, OrderItem
from orders.enums import OrderStatus


@pytest.fixture(scope="session")
def pending_order(django_db_setup, django_db_blocker, user, available_food_product) -> Order:
with django_db_blocker.unblock():
order = Order.objects.create(
user=user,
status=OrderStatus.PENDING_PAYMENT,
restaurant=available_food_product.restaurant,
total_price=0,
)
OrderItem.objects.create(
order=order,
product=available_food_product,
quantity=1,
price=available_food_product.price,
)
yield order


@pytest.fixture(scope="session")
def processing_order(django_db_setup, django_db_blocker, user, available_food_product) -> Order:
with django_db_blocker.unblock():
order = Order.objects.create(
user=user,
status=OrderStatus.PROCESSING,
restaurant=available_food_product.restaurant,
total_price=0,
)
OrderItem.objects.create(
order=order,
product=available_food_product,
quantity=1,
price=available_food_product.price,
)
yield order


@pytest.fixture(scope="session")
def shipped_order(django_db_setup, django_db_blocker, user, available_food_product) -> Order:
with django_db_blocker.unblock():
order = Order.objects.create(
user=user,
status=OrderStatus.SHIPPED,
restaurant=available_food_product.restaurant,
total_price=0,
)
OrderItem.objects.create(
order=order,
product=available_food_product,
quantity=1,
price=available_food_product.price,
)
yield order


@pytest.fixture(scope="session")
def delivered_order(django_db_setup, django_db_blocker, user, available_food_product) -> Order:
with django_db_blocker.unblock():
order = Order.objects.create(
user=user,
status=OrderStatus.DELIVERED,
restaurant=available_food_product.restaurant,
total_price=0,
)
OrderItem.objects.create(
order=order,
product=available_food_product,
quantity=1,
price=available_food_product.price,
)
yield order


@pytest.fixture(scope="session")
def cancelled_order(django_db_setup, django_db_blocker, user, available_food_product) -> Order:
with django_db_blocker.unblock():
order = Order.objects.create(
user=user,
status=OrderStatus.CANCELLED,
restaurant=available_food_product.restaurant,
total_price=0,
)
OrderItem.objects.create(
order=order,
product=available_food_product,
quantity=1,
price=available_food_product.price,
)
yield order
Loading

0 comments on commit 64d27c4

Please sign in to comment.