105 lines
3.6 KiB
Python
105 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
|
|
from sqlalchemy import select, update
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
from app.core.exceptions import BadRequestError, NotFoundError
|
|
from app.models.order import Order, OrderStatusEnum
|
|
from app.models.order_item import OrderItem
|
|
from app.models.product import Product
|
|
from app.models.user import User
|
|
from app.schemas.order import OrderCreate
|
|
|
|
audit_logger = logging.getLogger("app.audit")
|
|
MONEY_QUANTIZER = Decimal("0.01")
|
|
|
|
|
|
def _to_money(value: Decimal) -> Decimal:
|
|
return value.quantize(MONEY_QUANTIZER, rounding=ROUND_HALF_UP)
|
|
|
|
|
|
class OrderService:
|
|
def __init__(self, db: Session) -> None:
|
|
self.db = db
|
|
|
|
def create_order(self, user: User, payload: OrderCreate) -> Order:
|
|
product_ids = [item.product_id for item in payload.items]
|
|
products = self.db.scalars(
|
|
select(Product).where(Product.id.in_(product_ids), Product.is_active.is_(True))
|
|
).all()
|
|
|
|
if len(products) != len(product_ids):
|
|
raise NotFoundError("One or more requested products are unavailable")
|
|
|
|
product_map = {product.id: product for product in products}
|
|
order = Order(user_id=user.id, total_amount=Decimal("0.00"), status=OrderStatusEnum.pending)
|
|
|
|
try:
|
|
self.db.add(order)
|
|
self.db.flush()
|
|
|
|
total_amount = Decimal("0.00")
|
|
for item in payload.items:
|
|
product = product_map[item.product_id]
|
|
update_result = self.db.execute(
|
|
update(Product)
|
|
.where(
|
|
Product.id == product.id,
|
|
Product.is_active.is_(True),
|
|
Product.stock >= item.quantity,
|
|
)
|
|
.values(stock=Product.stock - item.quantity)
|
|
)
|
|
if update_result.rowcount != 1:
|
|
raise BadRequestError(
|
|
f"Insufficient stock for product '{product.name}'"
|
|
)
|
|
unit_price = _to_money(Decimal(product.price))
|
|
subtotal = _to_money(unit_price * item.quantity)
|
|
order_item = OrderItem(
|
|
order_id=order.id,
|
|
product_id=product.id,
|
|
quantity=item.quantity,
|
|
unit_price=unit_price,
|
|
subtotal=subtotal,
|
|
)
|
|
self.db.add(order_item)
|
|
total_amount += subtotal
|
|
|
|
order.total_amount = _to_money(total_amount)
|
|
self.db.commit()
|
|
except Exception:
|
|
self.db.rollback()
|
|
raise
|
|
|
|
audit_logger.info(
|
|
"order_created order_id=%s user_id=%s total_amount=%s",
|
|
order.id,
|
|
user.id,
|
|
str(order.total_amount),
|
|
)
|
|
return self.get_order_for_user(order.id, user)
|
|
|
|
def get_orders_for_user(self, user: User) -> list[Order]:
|
|
statement = (
|
|
select(Order)
|
|
.where(Order.user_id == user.id)
|
|
.options(selectinload(Order.items).selectinload(OrderItem.product))
|
|
.order_by(Order.created_at.desc())
|
|
)
|
|
return list(self.db.scalars(statement).all())
|
|
|
|
def get_order_for_user(self, order_id: int, user: User) -> Order:
|
|
statement = (
|
|
select(Order)
|
|
.where(Order.id == order_id, Order.user_id == user.id)
|
|
.options(selectinload(Order.items).selectinload(OrderItem.product))
|
|
)
|
|
order = self.db.scalar(statement)
|
|
if order is None:
|
|
raise NotFoundError("Order not found")
|
|
return order
|