# app/services/order_service.py """ Order service for order management. This module provides: - Order creation from cart - Order status management - Order retrieval and filtering """ import logging import random import string from datetime import datetime, timezone from typing import List, Optional, Tuple from sqlalchemy import and_, or_ from sqlalchemy.orm import Session from app.exceptions import (CustomerNotFoundException, InsufficientInventoryException, OrderNotFoundException, ValidationException) from models.database.customer import Customer, CustomerAddress from models.database.order import Order, OrderItem from models.database.product import Product from models.schema.order import OrderAddressCreate, OrderCreate, OrderUpdate logger = logging.getLogger(__name__) class OrderService: """Service for order operations.""" def _generate_order_number(self, db: Session, vendor_id: int) -> str: """ Generate unique order number. Format: ORD-{VENDOR_ID}-{TIMESTAMP}-{RANDOM} Example: ORD-1-20250110-A1B2C3 """ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d") random_suffix = "".join( random.choices(string.ascii_uppercase + string.digits, k=6) ) order_number = f"ORD-{vendor_id}-{timestamp}-{random_suffix}" # Ensure uniqueness while db.query(Order).filter(Order.order_number == order_number).first(): random_suffix = "".join( random.choices(string.ascii_uppercase + string.digits, k=6) ) order_number = f"ORD-{vendor_id}-{timestamp}-{random_suffix}" return order_number def _create_customer_address( self, db: Session, vendor_id: int, customer_id: int, address_data: OrderAddressCreate, address_type: str, ) -> CustomerAddress: """Create a customer address for order.""" address = CustomerAddress( vendor_id=vendor_id, customer_id=customer_id, address_type=address_type, first_name=address_data.first_name, last_name=address_data.last_name, company=address_data.company, address_line_1=address_data.address_line_1, address_line_2=address_data.address_line_2, city=address_data.city, postal_code=address_data.postal_code, country=address_data.country, is_default=False, ) db.add(address) db.flush() # Get ID without committing return address def create_order( self, db: Session, vendor_id: int, order_data: OrderCreate ) -> Order: """ Create a new order. Args: db: Database session vendor_id: Vendor ID order_data: Order creation data Returns: Created Order object Raises: ValidationException: If order data is invalid InsufficientInventoryException: If not enough inventory """ try: # Validate customer exists if provided customer_id = order_data.customer_id if customer_id: customer = ( db.query(Customer) .filter( and_( Customer.id == customer_id, Customer.vendor_id == vendor_id ) ) .first() ) if not customer: raise CustomerNotFoundException(str(customer_id)) else: # Guest checkout - create guest customer # TODO: Implement guest customer creation raise ValidationException("Guest checkout not yet implemented") # Create shipping address shipping_address = self._create_customer_address( db=db, vendor_id=vendor_id, customer_id=customer_id, address_data=order_data.shipping_address, address_type="shipping", ) # Create billing address (use shipping if not provided) if order_data.billing_address: billing_address = self._create_customer_address( db=db, vendor_id=vendor_id, customer_id=customer_id, address_data=order_data.billing_address, address_type="billing", ) else: billing_address = shipping_address # Calculate order totals subtotal = 0.0 order_items_data = [] for item_data in order_data.items: # Get product product = ( db.query(Product) .filter( and_( Product.id == item_data.product_id, Product.vendor_id == vendor_id, Product.is_active == True, ) ) .first() ) if not product: raise ValidationException( f"Product {item_data.product_id} not found" ) # Check inventory if product.available_inventory < item_data.quantity: raise InsufficientInventoryException( product_id=product.id, requested=item_data.quantity, available=product.available_inventory, ) # Calculate item total unit_price = product.sale_price if product.sale_price else product.price if not unit_price: raise ValidationException(f"Product {product.id} has no price") item_total = unit_price * item_data.quantity subtotal += item_total order_items_data.append( { "product_id": product.id, "product_name": product.marketplace_product.title, "product_sku": product.product_id, "quantity": item_data.quantity, "unit_price": unit_price, "total_price": item_total, } ) # Calculate tax and shipping (simple implementation) tax_amount = 0.0 # TODO: Implement tax calculation shipping_amount = 5.99 if subtotal < 50 else 0.0 # Free shipping over €50 discount_amount = 0.0 # TODO: Implement discounts total_amount = subtotal + tax_amount + shipping_amount - discount_amount # Generate order number order_number = self._generate_order_number(db, vendor_id) # Create order order = Order( vendor_id=vendor_id, customer_id=customer_id, order_number=order_number, status="pending", subtotal=subtotal, tax_amount=tax_amount, shipping_amount=shipping_amount, discount_amount=discount_amount, total_amount=total_amount, currency="EUR", shipping_address_id=shipping_address.id, billing_address_id=billing_address.id, shipping_method=order_data.shipping_method, customer_notes=order_data.customer_notes, ) db.add(order) db.flush() # Get order ID # Create order items for item_data in order_items_data: order_item = OrderItem(order_id=order.id, **item_data) db.add(order_item) db.commit() db.refresh(order) logger.info( f"Order {order.order_number} created for vendor {vendor_id}, " f"total: €{total_amount:.2f}" ) return order except ( ValidationException, InsufficientInventoryException, CustomerNotFoundException, ): db.rollback() raise except Exception as e: db.rollback() logger.error(f"Error creating order: {str(e)}") raise ValidationException(f"Failed to create order: {str(e)}") def get_order(self, db: Session, vendor_id: int, order_id: int) -> Order: """Get order by ID.""" order = ( db.query(Order) .filter(and_(Order.id == order_id, Order.vendor_id == vendor_id)) .first() ) if not order: raise OrderNotFoundException(str(order_id)) return order def get_vendor_orders( self, db: Session, vendor_id: int, skip: int = 0, limit: int = 100, status: Optional[str] = None, customer_id: Optional[int] = None, ) -> Tuple[List[Order], int]: """ Get orders for vendor with filtering. Args: db: Database session vendor_id: Vendor ID skip: Pagination offset limit: Pagination limit status: Filter by status customer_id: Filter by customer Returns: Tuple of (orders, total_count) """ query = db.query(Order).filter(Order.vendor_id == vendor_id) if status: query = query.filter(Order.status == status) if customer_id: query = query.filter(Order.customer_id == customer_id) # Order by most recent first query = query.order_by(Order.created_at.desc()) total = query.count() orders = query.offset(skip).limit(limit).all() return orders, total def get_customer_orders( self, db: Session, vendor_id: int, customer_id: int, skip: int = 0, limit: int = 100, ) -> Tuple[List[Order], int]: """Get orders for a specific customer.""" return self.get_vendor_orders( db=db, vendor_id=vendor_id, skip=skip, limit=limit, customer_id=customer_id ) def update_order_status( self, db: Session, vendor_id: int, order_id: int, order_update: OrderUpdate ) -> Order: """ Update order status and tracking information. Args: db: Database session vendor_id: Vendor ID order_id: Order ID order_update: Update data Returns: Updated Order object """ try: order = self.get_order(db, vendor_id, order_id) # Update status with timestamps if order_update.status: order.status = order_update.status # Update timestamp based on status now = datetime.now(timezone.utc) if order_update.status == "shipped" and not order.shipped_at: order.shipped_at = now elif order_update.status == "delivered" and not order.delivered_at: order.delivered_at = now elif order_update.status == "cancelled" and not order.cancelled_at: order.cancelled_at = now # Update tracking number if order_update.tracking_number: order.tracking_number = order_update.tracking_number # Update internal notes if order_update.internal_notes: order.internal_notes = order_update.internal_notes order.updated_at = datetime.now(timezone.utc) db.commit() db.refresh(order) logger.info(f"Order {order.order_number} updated: status={order.status}") return order except OrderNotFoundException: db.rollback() raise except Exception as e: db.rollback() logger.error(f"Error updating order: {str(e)}") raise ValidationException(f"Failed to update order: {str(e)}") # Create service instance order_service = OrderService()