Files
orion/app/modules/checkout/routes/api/storefront.py
Samir Boulahtit 2755c2f780 refactor(routes): move storefront routes to their modules
Phase 4 of storefront restructure plan - move API routes from legacy
app/api/v1/storefront/ to their respective modules:

- customers: auth, profile, addresses routes combined into storefront.py
- orders: order history viewing routes
- checkout: order placement (place_order endpoint)
- messaging: customer messaging routes

Updated app/api/v1/storefront/__init__.py to import from modules:
- cart_router from app.modules.cart
- catalog_router from app.modules.catalog
- checkout_router from app.modules.checkout
- customers_router from app.modules.customers
- orders_router from app.modules.orders
- messaging_router from app.modules.messaging

Legacy route files in app/api/v1/storefront/ can now be deleted
in Phase 6.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 23:00:01 +01:00

221 lines
7.1 KiB
Python

# app/modules/checkout/routes/api/storefront.py
"""
Checkout Module - Storefront API Routes
Endpoints for checkout and order creation in storefront:
- Place order (convert cart to order)
- Future: checkout session management
Uses vendor from middleware context (VendorContextMiddleware).
Requires customer authentication for order placement.
"""
import logging
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session
from app.api.deps import get_current_customer_api
from app.core.database import get_db
from app.exceptions import VendorNotFoundException
from app.modules.cart.services import cart_service
from app.modules.checkout.schemas import (
CheckoutRequest,
CheckoutResponse,
CheckoutSessionResponse,
)
from app.modules.checkout.services import checkout_service
from app.modules.orders.services import order_service
from app.services.email_service import EmailService
from middleware.vendor_context import require_vendor_context
from models.database.customer import Customer
from models.database.vendor import Vendor
from models.schema.order import OrderCreate, OrderResponse
router = APIRouter()
logger = logging.getLogger(__name__)
# ============================================================================
# ORDER PLACEMENT (converts cart to order)
# ============================================================================
@router.post("/orders", response_model=OrderResponse) # authenticated
def place_order(
request: Request,
order_data: OrderCreate,
customer: Customer = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
Place a new order for current vendor.
Vendor is automatically determined from request context.
Customer must be authenticated to place an order.
Creates an order from the customer's cart.
Request Body:
- Order data including shipping address, payment method, etc.
"""
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[CHECKOUT_STOREFRONT] place_order for customer {customer.id}",
extra={
"vendor_id": vendor.id,
"vendor_code": vendor.subdomain,
"customer_id": customer.id,
},
)
# Create order
order = order_service.create_order(
db=db, vendor_id=vendor.id, order_data=order_data
)
db.commit()
logger.info(
f"Order {order.order_number} placed for vendor {vendor.subdomain}, "
f"total: €{order.total_amount:.2f}",
extra={
"order_id": order.id,
"order_number": order.order_number,
"customer_id": customer.id,
"total_amount": float(order.total_amount),
},
)
# Update customer stats
customer.total_orders = (customer.total_orders or 0) + 1
customer.total_spent = (customer.total_spent or 0) + order.total_amount
customer.last_order_date = datetime.now(UTC)
db.flush()
logger.debug(
f"Updated customer stats: total_orders={customer.total_orders}, "
f"total_spent={customer.total_spent}"
)
# Clear cart (get session_id from request cookies or headers)
session_id = request.cookies.get("cart_session_id") or request.headers.get(
"X-Cart-Session-Id"
)
if session_id:
try:
cart_service.clear_cart(db, vendor.id, session_id)
logger.debug(f"Cleared cart for session {session_id}")
except Exception as e:
logger.warning(f"Failed to clear cart: {e}")
# Send order confirmation email
try:
email_service = EmailService(db)
email_service.send_template(
template_code="order_confirmation",
to_email=customer.email,
to_name=customer.full_name,
language=customer.preferred_language or "en",
variables={
"customer_name": customer.first_name or customer.full_name,
"order_number": order.order_number,
"order_total": f"{order.total_amount:.2f}",
"order_items_count": len(order.items),
"order_date": order.order_date.strftime("%d.%m.%Y")
if order.order_date
else "",
"shipping_address": f"{order.ship_address_line_1}, {order.ship_postal_code} {order.ship_city}",
},
vendor_id=vendor.id,
related_type="order",
related_id=order.id,
)
logger.info(f"Sent order confirmation email to {customer.email}")
except Exception as e:
logger.warning(f"Failed to send order confirmation email: {e}")
return OrderResponse.model_validate(order)
# ============================================================================
# CHECKOUT SESSION (future implementation)
# ============================================================================
@router.post("/checkout/session", response_model=CheckoutSessionResponse)
def create_checkout_session(
checkout_data: CheckoutRequest,
vendor: Vendor = Depends(require_vendor_context()),
db: Session = Depends(get_db),
) -> CheckoutSessionResponse:
"""
Create a checkout session from cart.
Validates the cart and prepares for checkout.
Vendor is automatically determined from request context.
Note: This is a placeholder endpoint for future checkout session workflow.
Request Body:
- session_id: Cart session ID
- shipping_address: Shipping address details
- billing_same_as_shipping: Use shipping for billing (default: true)
- billing_address: Billing address if different
- customer_email: Email for order confirmation
- customer_note: Optional note
"""
logger.info(
f"[CHECKOUT_STOREFRONT] create_checkout_session for vendor {vendor.id}",
extra={
"vendor_id": vendor.id,
"session_id": checkout_data.session_id,
},
)
result = checkout_service.create_checkout_session(
db=db,
vendor_id=vendor.id,
session_id=checkout_data.session_id,
)
return CheckoutSessionResponse(**result)
@router.post("/checkout/complete", response_model=CheckoutResponse)
def complete_checkout(
checkout_session_id: str,
vendor: Vendor = Depends(require_vendor_context()),
db: Session = Depends(get_db),
) -> CheckoutResponse:
"""
Complete checkout and create order.
Converts the cart to an order and processes payment.
Vendor is automatically determined from request context.
Note: This is a placeholder endpoint for future checkout completion workflow.
Query Parameters:
- checkout_session_id: The checkout session ID from create_checkout_session
"""
logger.info(
f"[CHECKOUT_STOREFRONT] complete_checkout for vendor {vendor.id}",
extra={
"vendor_id": vendor.id,
"checkout_session_id": checkout_session_id,
},
)
result = checkout_service.complete_checkout(
db=db,
vendor_id=vendor.id,
checkout_session_id=checkout_session_id,
)
db.commit()
return CheckoutResponse(**result)