Files
orion/app/modules/checkout/routes/api/storefront.py
Samir Boulahtit a77a8a3a98
All checks were successful
CI / ruff (push) Successful in 12s
CI / pytest (push) Successful in 50m57s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Successful in 40s
CI / deploy (push) Successful in 51s
feat: multi-module improvements across merchant, store, i18n, and customer systems
- Fix platform-grouped merchant sidebar menu with core items at root level
- Add merchant store management (detail page, create store, team page)
- Fix store settings 500 error by removing dead stripe/API tab
- Move onboarding translations to module-owned locale files
- Fix onboarding banner i18n with server-side rendering + context inheritance
- Refactor login language selectors to use languageSelector() function (LANG-002)
- Move HTTPException handling to global exception handler in merchant routes (API-003)
- Add language selector to all login pages and portal headers
- Fix customer module: drop order stats from customer model, add to orders module
- Fix admin menu config visibility for super admin platform context
- Fix storefront auth and layout issues
- Add missing i18n translations for onboarding steps (en/fr/de/lb)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:48:25 +01:00

228 lines
7.3 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 store from middleware context (StoreContextMiddleware).
Requires customer authentication for order placement.
"""
import logging
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.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.customers.schemas import CustomerContext
from app.modules.messaging.services.email_service import (
EmailService, # MOD-004 - Core email service
)
from app.modules.orders.schemas import OrderCreate, OrderResponse
from app.modules.orders.services import order_service
from app.modules.tenancy.exceptions import StoreNotFoundException
from app.modules.tenancy.models import Store
from middleware.store_context import require_store_context
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: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
Place a new order for current store.
Store 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.
"""
store = getattr(request.state, "store", None)
if not store:
raise StoreNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[CHECKOUT_STOREFRONT] place_order for customer {customer.id}",
extra={
"store_id": store.id,
"store_code": store.subdomain,
"customer_id": customer.id,
},
)
# Create order
order = order_service.create_order(
db=db, store_id=store.id, order_data=order_data
)
db.commit()
logger.info(
f"Order {order.order_number} placed for store {store.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 order stats (owned by orders module)
from app.modules.orders.services.customer_order_service import (
customer_order_service,
)
stats = customer_order_service.record_order(
db=db,
store_id=store.id,
customer_id=customer.id,
total_amount_cents=order.total_amount_cents,
)
logger.debug(
f"Updated customer order stats: total_orders={stats.total_orders}, "
f"total_spent_cents={stats.total_spent_cents}"
)
# 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, store.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}",
},
store_id=store.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,
store: Store = Depends(require_store_context()),
db: Session = Depends(get_db),
) -> CheckoutSessionResponse:
"""
Create a checkout session from cart.
Validates the cart and prepares for checkout.
Store 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 store {store.id}",
extra={
"store_id": store.id,
"session_id": checkout_data.session_id,
},
)
result = checkout_service.create_checkout_session(
db=db,
store_id=store.id,
session_id=checkout_data.session_id,
)
return CheckoutSessionResponse(**result)
@router.post("/checkout/complete", response_model=CheckoutResponse)
def complete_checkout(
checkout_session_id: str,
store: Store = Depends(require_store_context()),
db: Session = Depends(get_db),
) -> CheckoutResponse:
"""
Complete checkout and create order.
Converts the cart to an order and processes payment.
Store 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 store {store.id}",
extra={
"store_id": store.id,
"checkout_session_id": checkout_session_id,
},
)
result = checkout_service.complete_checkout(
db=db,
store_id=store.id,
checkout_session_id=checkout_session_id,
)
db.commit()
return CheckoutResponse(**result)