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