Files
orion/docs/project-roadmap/slice5_doc.md

53 KiB
Raw Blame History

    async placeOrder() {
        this.placing = true;
        try {
            const order = await apiClient.post(
                `/api/v1/public/vendors/${window.vendorId}/orders`,
                {
                    shipping_address_id: this.selectedShippingAddress,
                    billing_address_id: this.selectedBillingAddress,
                    shipping_method: 'standard',
                    payment_method: this.paymentMethod,
                    customer_notes: ''
                }
            );
            
            this.orderNumber = order.order_number;
            this.currentStep = 4;
            
            // Clear cart from localStorage
            clearCart();
        } catch (error) {
            showNotification(error.message || 'Failed to place order', 'error');
        } finally {
            this.placing = false;
        }
    }
}

} </script> {% endblock %}


#### Customer Order History (`templates/shop/account/orders.html`)

```html
{% extends "shop/base_shop.html" %}

{% block content %}
<div x-data="orderHistory()" x-init="loadOrders()">
    <div class="account-page">
        <h1>My Orders</h1>
        
        <template x-if="orders.length === 0 && !loading">
            <div class="empty-state">
                <p>You haven't placed any orders yet.</p>
                <a href="/shop/products" class="btn btn-primary">Start Shopping</a>
            </div>
        </template>
        
        <template x-if="orders.length > 0">
            <div class="orders-list">
                <template x-for="order in orders" :key="order.id">
                    <div class="order-card">
                        <div class="order-header">
                            <div class="order-info">
                                <h3 x-text="order.order_number"></h3>
                                <span class="order-date" x-text="formatDate(order.created_at)"></span>
                            </div>
                            <span 
                                class="badge"
                                :class="{
                                    'badge-warning': order.status === 'pending' || order.status === 'processing',
                                    'badge-info': order.status === 'confirmed' || order.status === 'shipped',
                                    'badge-success': order.status === 'delivered',
                                    'badge-danger': order.status === 'cancelled'
                                }"
                                x-text="order.status"
                            ></span>
                        </div>
                        
                        <div class="order-items">
                            <template x-for="item in order.items" :key="item.id">
                                <div class="order-item">
                                    <img :src="item.product_image" :alt="item.product_title">
                                    <div class="item-details">
                                        <p x-text="item.product_title"></p>
                                        <p class="text-muted">Qty: <span x-text="item.quantity"></span></p>
                                    </div>
                                    <p class="item-price">€<span x-text="item.total_price.toFixed(2)"></span></p>
                                </div>
                            </template>
                        </div>
                        
                        <div class="order-footer">
                            <p class="order-total">
                                Total: €<span x-text="order.total_amount.toFixed(2)"></span>
                            </p>
                            <a :href="`/shop/account/orders/${order.id}`" class="btn btn-sm">
                                View Details
                            </a>
                        </div>
                    </div>
                </template>
            </div>
        </template>
        
        <!-- Pagination -->
        <div x-show="hasMore" class="text-center mt-3">
            <button @click="loadMore()" class="btn btn-secondary" :disabled="loading">
                <span x-show="!loading">Load More</span>
                <span x-show="loading" class="loading-spinner"></span>
            </button>
        </div>
    </div>
</div>

<script>
    window.vendorId = {{ vendor.id }};
</script>
{% endblock %}

{% block extra_scripts %}
<script>
function orderHistory() {
    return {
        orders: [],
        loading: false,
        page: 0,
        hasMore: true,
        
        async loadOrders() {
            this.loading = true;
            try {
                const newOrders = await apiClient.get(
                    `/api/v1/public/vendors/${window.vendorId}/orders/my-orders?skip=${this.page * 20}&limit=20`
                );
                
                this.orders = [...this.orders, ...newOrders];
                this.hasMore = newOrders.length === 20;
            } catch (error) {
                showNotification('Failed to load orders', 'error');
            } finally {
                this.loading = false;
            }
        },
        
        async loadMore() {
            this.page++;
            await this.loadOrders();
        },
        
        formatDate(dateString) {
            return new Date(dateString).toLocaleDateString();
        }
    }
}
</script>
{% endblock %}

Vendor Order Management (templates/vendor/orders/list.html)

{% extends "vendor/base_vendor.html" %}

{% block title %}Order Management{% endblock %}

{% block content %}
<div x-data="orderManagement()" x-init="loadOrders()">
    <!-- Page Header -->
    <div class="page-header">
        <h1>Orders</h1>
        <div class="header-actions">
            <button @click="exportOrders()" class="btn btn-secondary">
                Export Orders
            </button>
        </div>
    </div>
    
    <!-- Filters -->
    <div class="filters-bar">
        <div class="filter-group">
            <label>Status</label>
            <select x-model="filters.status" @change="loadOrders()">
                <option value="">All Statuses</option>
                <option value="pending">Pending</option>
                <option value="confirmed">Confirmed</option>
                <option value="processing">Processing</option>
                <option value="shipped">Shipped</option>
                <option value="delivered">Delivered</option>
                <option value="cancelled">Cancelled</option>
            </select>
        </div>
        
        <div class="filter-group">
            <label>Search</label>
            <input 
                type="text" 
                x-model="filters.search"
                @input.debounce.500ms="loadOrders()"
                placeholder="Order #, customer name..."
                class="form-control"
            >
        </div>
        
        <div class="filter-group">
            <label>Date From</label>
            <input 
                type="date" 
                x-model="filters.date_from"
                @change="loadOrders()"
                class="form-control"
            >
        </div>
        
        <div class="filter-group">
            <label>Date To</label>
            <input 
                type="date" 
                x-model="filters.date_to"
                @change="loadOrders()"
                class="form-control"
            >
        </div>
    </div>
    
    <!-- Orders Table -->
    <div class="card">
        <template x-if="orders.length === 0 && !loading">
            <p class="text-center text-muted">No orders found</p>
        </template>
        
        <template x-if="orders.length > 0">
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Order #</th>
                        <th>Customer</th>
                        <th>Date</th>
                        <th>Items</th>
                        <th>Total</th>
                        <th>Status</th>
                        <th>Payment</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    <template x-for="order in orders" :key="order.id">
                        <tr>
                            <td>
                                <strong x-text="order.order_number"></strong>
                            </td>
                            <td>
                                <div x-text="order.customer.first_name + ' ' + order.customer.last_name"></div>
                                <small class="text-muted" x-text="order.customer.email"></small>
                            </td>
                            <td x-text="formatDate(order.created_at)"></td>
                            <td x-text="order.items.length"></td>
                            <td><span x-text="order.total_amount.toFixed(2)"></span></td>
                            <td>
                                <span 
                                    class="badge"
                                    :class="{
                                        'badge-warning': order.status === 'pending',
                                        'badge-info': order.status === 'processing' || order.status === 'confirmed',
                                        'badge-primary': order.status === 'shipped',
                                        'badge-success': order.status === 'delivered',
                                        'badge-danger': order.status === 'cancelled'
                                    }"
                                    x-text="order.status"
                                ></span>
                            </td>
                            <td>
                                <span 
                                    class="badge"
                                    :class="{
                                        'badge-warning': order.payment_status === 'pending',
                                        'badge-success': order.payment_status === 'paid',
                                        'badge-danger': order.payment_status === 'failed'
                                    }"
                                    x-text="order.payment_status"
                                ></span>
                            </td>
                            <td>
                                <button 
                                    @click="viewOrder(order.id)" 
                                    class="btn btn-sm"
                                >
                                    View
                                </button>
                                <button 
                                    @click="showStatusModal(order)" 
                                    class="btn btn-sm btn-secondary"
                                >
                                    Update Status
                                </button>
                            </td>
                        </tr>
                    </template>
                </tbody>
            </table>
        </template>
    </div>
    
    <!-- Status Update Modal -->
    <div x-show="statusModal.show" class="modal-overlay" @click.self="statusModal.show = false">
        <div class="modal">
            <div class="modal-header">
                <h3>Update Order Status</h3>
                <button @click="statusModal.show = false" class="modal-close">×</button>
            </div>
            <div class="modal-body">
                <form @submit.prevent="updateStatus()">
                    <div class="form-group">
                        <label>Order Number</label>
                        <input 
                            type="text" 
                            :value="statusModal.order?.order_number"
                            class="form-control"
                            readonly
                        >
                    </div>
                    
                    <div class="form-group">
                        <label>Current Status</label>
                        <input 
                            type="text" 
                            :value="statusModal.order?.status"
                            class="form-control"
                            readonly
                        >
                    </div>
                    
                    <div class="form-group">
                        <label>New Status</label>
                        <select x-model="statusModal.newStatus" class="form-control" required>
                            <option value="confirmed">Confirmed</option>
                            <option value="processing">Processing</option>
                            <option value="shipped">Shipped</option>
                            <option value="delivered">Delivered</option>
                            <option value="cancelled">Cancelled</option>
                        </select>
                    </div>
                    
                    <div class="form-group" x-show="statusModal.newStatus === 'shipped'">
                        <label>Tracking Number</label>
                        <input 
                            type="text" 
                            x-model="statusModal.trackingNumber"
                            class="form-control"
                            placeholder="Enter tracking number"
                        >
                    </div>
                    
                    <div class="form-group">
                        <label>Notes (Optional)</label>
                        <textarea 
                            x-model="statusModal.notes"
                            class="form-control"
                            rows="3"
                        ></textarea>
                    </div>
                    
                    <div class="modal-footer">
                        <button type="button" @click="statusModal.show = false" class="btn btn-secondary">
                            Cancel
                        </button>
                        <button type="submit" class="btn btn-primary" :disabled="statusModal.updating">
                            <span x-show="!statusModal.updating">Update Status</span>
                            <span x-show="statusModal.updating" class="loading-spinner"></span>
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_scripts %}
<script>
function orderManagement() {
    return {
        orders: [],
        loading: false,
        filters: {
            status: '',
            search: '',
            date_from: '',
            date_to: ''
        },
        statusModal: {
            show: false,
            order: null,
            newStatus: '',
            trackingNumber: '',
            notes: '',
            updating: false
        },
        
        async loadOrders() {
            this.loading = true;
            try {
                const params = new URLSearchParams();
                if (this.filters.status) params.append('status', this.filters.status);
                if (this.filters.search) params.append('search', this.filters.search);
                if (this.filters.date_from) params.append('date_from', this.filters.date_from);
                if (this.filters.date_to) params.append('date_to', this.filters.date_to);
                
                this.orders = await apiClient.get(`/api/v1/vendor/orders?${params}`);
            } catch (error) {
                showNotification('Failed to load orders', 'error');
            } finally {
                this.loading = false;
            }
        },
        
        viewOrder(orderId) {
            window.location.href = `/vendor/orders/${orderId}`;
        },
        
        showStatusModal(order) {
            this.statusModal.show = true;
            this.statusModal.order = order;
            this.statusModal.newStatus = order.status;
            this.statusModal.trackingNumber = order.tracking_number || '';
            this.statusModal.notes = '';
        },
        
        async updateStatus() {
            this.statusModal.updating = true;
            try {
                await apiClient.put(
                    `/api/v1/vendor/orders/${this.statusModal.order.id}/status`,
                    {
                        status: this.statusModal.newStatus,
                        tracking_number: this.statusModal.trackingNumber,
                        notes: this.statusModal.notes
                    }
                );
                
                showNotification('Order status updated', 'success');
                this.statusModal.show = false;
                await this.loadOrders();
            } catch (error) {
                showNotification(error.message || 'Failed to update status', 'error');
            } finally {
                this.statusModal.updating = false;
            }
        },
        
        async exportOrders() {
            // Implement CSV export
            showNotification('Export feature coming soon', 'info');
        },
        
        formatDate(dateString) {
            return new Date(dateString).toLocaleDateString();
        }
    }
}
</script>
{% endblock %}

Testing Checklist

Backend Tests

Order Creation

  • Order created successfully from cart
  • Order number generated uniquely
  • Order items created with product snapshots
  • Inventory reserved correctly
  • Customer stats updated (total_orders, total_spent)
  • Cart cleared after order placement
  • Order status history recorded
  • Shipping/billing addresses validated
  • Tax calculated correctly
  • Shipping cost calculated correctly

Order Management

  • Customer can view their order history
  • Customer can view order details
  • Vendor can view all orders
  • Order filtering works (status, date, search)
  • Order search works (order number, customer name)
  • Vendor can update order status
  • Status history tracked correctly
  • Tracking number can be added

Payment Integration

  • Payment method validation
  • Payment status tracking
  • Stripe integration ready (placeholder)

Notifications

  • Order confirmation email sent
  • Status update email sent
  • Email contains correct order details

Frontend Tests

Checkout Flow

  • Multi-step checkout works
  • Cart review displays correctly
  • Address selection works
  • New address creation works
  • Payment method selection works
  • Order summary calculates correctly
  • Order placement succeeds
  • Confirmation page displays
  • Loading states work
  • Error handling works

Customer Order History

  • Order list displays correctly
  • Order cards show correct information
  • Status badges display correctly
  • Order details link works
  • Pagination works
  • Empty state displays

Vendor Order Management

  • Order table displays correctly
  • Filters work (status, date, search)
  • Order details modal works
  • Status update modal works
  • Status updates successfully
  • Real-time status updates
  • Export functionality (when implemented)

Integration Tests

Complete Order Flow

  • Customer adds products to cart
  • Customer proceeds to checkout
  • Customer completes checkout
  • Order appears in customer history
  • Order appears in vendor orders
  • Vendor updates order status
  • Customer sees status update
  • Emails sent at each step

Vendor Isolation

  • Orders scoped to correct vendor
  • Customers can only see their orders
  • Vendors can only manage their orders
  • Cross-vendor access prevented

Data Integrity

  • Inventory correctly reserved/updated
  • Product snapshots preserved
  • Customer stats accurate
  • Order totals calculate correctly
  • Status history maintained

📝 Additional Features (Optional)

Payment Integration

  • Stripe payment element integration
  • Payment intent creation
  • Payment confirmation handling
  • Refund processing

Email Notifications

  • Order confirmation template
  • Order shipped notification
  • Order delivered notification
  • Order cancelled notification

Advanced Features

  • Order notes/communication
  • Partial refunds
  • Order tracking page
  • Invoice generation
  • Bulk order status updates
  • Order analytics dashboard

🚀 Deployment Checklist

Environment Setup

  • Email service configured (SendGrid, Mailgun, etc.)
  • Stripe API keys configured (test and live)
  • Order number prefix configured
  • Tax rates configured by country
  • Shipping rates configured

Testing

  • Complete checkout flow tested
  • Payment processing tested (test mode)
  • Email notifications tested
  • Order management tested
  • Mobile checkout tested

Production Readiness

  • Stripe live mode enabled
  • Email templates reviewed
  • Error logging enabled
  • Order monitoring set up
  • Backup strategy for orders

🎯 Acceptance Criteria

Slice 5 is complete when:

  1. Customer Checkout Works

    • Multi-step checkout functional
    • Address management works
    • Orders can be placed successfully
    • Confirmation displayed
  2. Customer Order Management Works

    • Order history displays
    • Order details accessible
    • Order tracking works
  3. Vendor Order Management Works

    • All orders visible
    • Filtering and search work
    • Status updates work
    • Order details accessible
  4. System Integration Complete

    • Inventory updates correctly
    • Emails sent automatically
    • Data integrity maintained
    • Vendor isolation enforced
  5. Production Ready

    • All tests pass
    • Payment integration ready
    • Email system configured
    • Error handling robust

🎉 Platform Complete!

After completing Slice 5, your multi-tenant ecommerce platform is production-ready with:

Multi-tenant foundation - Complete vendor isolation
Admin management - Vendor creation and management
Marketplace integration - CSV product imports
Product catalog - Vendor product management
Customer shopping - Browse, search, cart
Order processing - Complete checkout and fulfillment

Next Steps for Production

  1. Security Audit

    • Review authentication
    • Test vendor isolation
    • Verify payment security
    • Check data encryption
  2. Performance Optimization

    • Database indexing
    • Query optimization
    • Caching strategy
    • CDN setup
  3. Monitoring & Analytics

    • Set up error tracking (Sentry)
    • Configure analytics
    • Set up uptime monitoring
    • Create admin dashboards
  4. Documentation

    • User guides for vendors
    • Admin documentation
    • API documentation
    • Deployment guide
  5. Launch Preparation

    • Beta testing with real vendors
    • Load testing
    • Backup procedures
    • Support system

Slice 5 Status: 📋 Not Started
Dependencies: Slices 1, 2, 3, & 4 must be complete
Estimated Duration: 5 days
Platform Status After Completion: 🚀 Production Ready!# Slice 5: Order Processing

Complete Checkout and Order Management

Status: 📋 NOT STARTED
Timeline: Week 5 (5 days)
Prerequisites: Slices 1, 2, 3, & 4 complete

🎯 Slice Objectives

Complete the ecommerce transaction workflow with checkout, order placement, and order management for both customers and vendors.

User Stories

  • As a Customer, I can proceed to checkout with my cart
  • As a Customer, I can select shipping address
  • As a Customer, I can place orders
  • As a Customer, I can view my order history
  • As a Customer, I can view order details and status
  • As a Vendor Owner, I can view all customer orders
  • As a Vendor Owner, I can manage orders (update status, view details)
  • As a Vendor Owner, I can filter and search orders
  • Order confirmation emails are sent automatically

Success Criteria

  • Customers can complete multi-step checkout process
  • Address selection/creation works
  • Orders are created with proper vendor isolation
  • Inventory is reserved/updated on order placement
  • Customers can view their order history
  • Customers can track order status
  • Vendors can view all their orders
  • Vendors can update order status
  • Order status changes trigger notifications
  • Order confirmation emails sent
  • Payment integration ready (placeholder for Stripe)

📋 Backend Implementation

Database Models

Order Model (models/database/order.py)

class Order(Base, TimestampMixin):
    """Customer orders"""
    __tablename__ = "orders"
    
    id = Column(Integer, primary_key=True, index=True)
    vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
    customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
    
    # Order identification
    order_number = Column(String, unique=True, nullable=False, index=True)
    
    # Order status
    status = Column(
        String, 
        nullable=False, 
        default="pending"
    )  # pending, confirmed, processing, shipped, delivered, cancelled
    
    # Amounts
    subtotal = Column(Numeric(10, 2), nullable=False)
    shipping_cost = Column(Numeric(10, 2), default=0)
    tax_amount = Column(Numeric(10, 2), default=0)
    discount_amount = Column(Numeric(10, 2), default=0)
    total_amount = Column(Numeric(10, 2), nullable=False)
    currency = Column(String(3), default="EUR")
    
    # Addresses (snapshot at time of order)
    shipping_address_id = Column(Integer, ForeignKey("customer_addresses.id"))
    billing_address_id = Column(Integer, ForeignKey("customer_addresses.id"))
    
    # Shipping
    shipping_method = Column(String)
    tracking_number = Column(String)
    shipped_at = Column(DateTime, nullable=True)
    delivered_at = Column(DateTime, nullable=True)
    
    # Payment
    payment_status = Column(String, default="pending")  # pending, paid, failed, refunded
    payment_method = Column(String)  # stripe, paypal, etc.
    payment_reference = Column(String)  # External payment ID
    paid_at = Column(DateTime, nullable=True)
    
    # Customer notes
    customer_notes = Column(Text)
    internal_notes = Column(Text)  # Vendor-only notes
    
    # Metadata
    ip_address = Column(String)
    user_agent = Column(String)
    
    # Relationships
    vendor = relationship("Vendor", back_populates="orders")
    customer = relationship("Customer", back_populates="orders")
    items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
    shipping_address = relationship("CustomerAddress", foreign_keys=[shipping_address_id])
    billing_address = relationship("CustomerAddress", foreign_keys=[billing_address_id])
    status_history = relationship("OrderStatusHistory", back_populates="order")
    
    # Indexes
    __table_args__ = (
        Index('ix_order_vendor_customer', 'vendor_id', 'customer_id'),
        Index('ix_order_status', 'vendor_id', 'status'),
        Index('ix_order_date', 'vendor_id', 'created_at'),
    )

class OrderItem(Base, TimestampMixin):
    """Items in an order"""
    __tablename__ = "order_items"
    
    id = Column(Integer, primary_key=True, index=True)
    order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
    product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
    
    # Product snapshot at time of order
    product_sku = Column(String, nullable=False)
    product_title = Column(String, nullable=False)
    product_image = Column(String)
    
    # Pricing
    quantity = Column(Integer, nullable=False)
    unit_price = Column(Numeric(10, 2), nullable=False)
    total_price = Column(Numeric(10, 2), nullable=False)
    
    # Tax
    tax_rate = Column(Numeric(5, 2), default=0)
    tax_amount = Column(Numeric(10, 2), default=0)
    
    # Relationships
    order = relationship("Order", back_populates="items")
    product = relationship("Product", back_populates="order_items")

class OrderStatusHistory(Base, TimestampMixin):
    """Track order status changes"""
    __tablename__ = "order_status_history"
    
    id = Column(Integer, primary_key=True, index=True)
    order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
    
    # Status change
    from_status = Column(String)
    to_status = Column(String, nullable=False)
    
    # Who made the change
    changed_by_type = Column(String)  # 'customer', 'vendor', 'system'
    changed_by_id = Column(Integer)
    
    # Notes
    notes = Column(Text)
    
    # Relationships
    order = relationship("Order", back_populates="status_history")

Pydantic Schemas

Order Schemas (models/schema/order.py)

from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
from decimal import Decimal

class OrderItemCreate(BaseModel):
    """Order item from cart"""
    product_id: int
    quantity: int
    unit_price: Decimal

class OrderCreate(BaseModel):
    """Create new order"""
    shipping_address_id: int
    billing_address_id: int
    shipping_method: Optional[str] = "standard"
    payment_method: str = "stripe"
    customer_notes: Optional[str] = None

class OrderItemResponse(BaseModel):
    """Order item details"""
    id: int
    product_id: int
    product_sku: str
    product_title: str
    product_image: Optional[str]
    quantity: int
    unit_price: float
    total_price: float
    tax_amount: float
    
    class Config:
        from_attributes = True

class OrderResponse(BaseModel):
    """Order details"""
    id: int
    vendor_id: int
    customer_id: int
    order_number: str
    status: str
    subtotal: float
    shipping_cost: float
    tax_amount: float
    discount_amount: float
    total_amount: float
    currency: str
    payment_status: str
    payment_method: Optional[str]
    tracking_number: Optional[str]
    customer_notes: Optional[str]
    created_at: datetime
    updated_at: datetime
    items: List[OrderItemResponse]
    
    class Config:
        from_attributes = True

class OrderStatusUpdate(BaseModel):
    """Update order status"""
    status: str = Field(..., regex="^(pending|confirmed|processing|shipped|delivered|cancelled)$")
    tracking_number: Optional[str] = None
    notes: Optional[str] = None

class OrderListFilters(BaseModel):
    """Filters for order list"""
    status: Optional[str] = None
    customer_id: Optional[int] = None
    date_from: Optional[datetime] = None
    date_to: Optional[datetime] = None
    search: Optional[str] = None  # Order number or customer name

Service Layer

Order Service (app/services/order_service.py)

from typing import List, Optional
from sqlalchemy.orm import Session
from datetime import datetime
from decimal import Decimal

class OrderService:
    """Handle order operations"""
    
    async def create_order_from_cart(
        self,
        vendor_id: int,
        customer_id: int,
        cart_id: int,
        order_data: OrderCreate,
        db: Session
    ) -> Order:
        """Create order from shopping cart"""
        
        # Get cart with items
        cart = db.query(Cart).filter(
            Cart.id == cart_id,
            Cart.vendor_id == vendor_id
        ).first()
        
        if not cart or not cart.items:
            raise CartEmptyError("Cart is empty")
        
        # Verify addresses belong to customer
        shipping_addr = db.query(CustomerAddress).filter(
            CustomerAddress.id == order_data.shipping_address_id,
            CustomerAddress.customer_id == customer_id
        ).first()
        
        billing_addr = db.query(CustomerAddress).filter(
            CustomerAddress.id == order_data.billing_address_id,
            CustomerAddress.customer_id == customer_id
        ).first()
        
        if not shipping_addr or not billing_addr:
            raise AddressNotFoundError()
        
        # Calculate totals
        subtotal = sum(item.line_total for item in cart.items)
        shipping_cost = self._calculate_shipping(order_data.shipping_method)
        tax_amount = self._calculate_tax(subtotal, shipping_addr.country)
        total_amount = subtotal + shipping_cost + tax_amount
        
        # Generate order number
        order_number = self._generate_order_number(vendor_id, db)
        
        # Create order
        order = Order(
            vendor_id=vendor_id,
            customer_id=customer_id,
            order_number=order_number,
            status="pending",
            subtotal=subtotal,
            shipping_cost=shipping_cost,
            tax_amount=tax_amount,
            discount_amount=0,
            total_amount=total_amount,
            currency="EUR",
            shipping_address_id=order_data.shipping_address_id,
            billing_address_id=order_data.billing_address_id,
            shipping_method=order_data.shipping_method,
            payment_method=order_data.payment_method,
            payment_status="pending",
            customer_notes=order_data.customer_notes
        )
        
        db.add(order)
        db.flush()  # Get order ID
        
        # Create order items from cart
        for cart_item in cart.items:
            # Verify product still available
            product = db.query(Product).get(cart_item.product_id)
            if not product or not product.is_active:
                continue
            
            # Check inventory
            if product.track_inventory and product.stock_quantity < cart_item.quantity:
                raise InsufficientInventoryError(f"Not enough stock for {product.title}")
            
            # Create order item
            order_item = OrderItem(
                order_id=order.id,
                product_id=cart_item.product_id,
                product_sku=product.sku,
                product_title=product.title,
                product_image=product.featured_image,
                quantity=cart_item.quantity,
                unit_price=cart_item.unit_price,
                total_price=cart_item.line_total,
                tax_rate=self._get_tax_rate(shipping_addr.country),
                tax_amount=cart_item.line_total * self._get_tax_rate(shipping_addr.country) / 100
            )
            db.add(order_item)
            
            # Reserve inventory
            if product.track_inventory:
                await self._reserve_inventory(product, cart_item.quantity, order.id, db)
        
        # Record initial status
        status_history = OrderStatusHistory(
            order_id=order.id,
            from_status=None,
            to_status="pending",
            changed_by_type="customer",
            changed_by_id=customer_id,
            notes="Order created"
        )
        db.add(status_history)
        
        # Clear cart
        db.query(CartItem).filter(CartItem.cart_id == cart_id).delete()
        
        # Update customer stats
        customer = db.query(Customer).get(customer_id)
        customer.total_orders += 1
        customer.total_spent += float(total_amount)
        
        db.commit()
        db.refresh(order)
        
        # Send order confirmation email
        await self._send_order_confirmation(order, db)
        
        return order
    
    def get_customer_orders(
        self,
        vendor_id: int,
        customer_id: int,
        db: Session,
        skip: int = 0,
        limit: int = 20
    ) -> List[Order]:
        """Get customer's order history"""
        
        return db.query(Order).filter(
            Order.vendor_id == vendor_id,
            Order.customer_id == customer_id
        ).order_by(
            Order.created_at.desc()
        ).offset(skip).limit(limit).all()
    
    def get_vendor_orders(
        self,
        vendor_id: int,
        db: Session,
        filters: OrderListFilters,
        skip: int = 0,
        limit: int = 50
    ) -> List[Order]:
        """Get vendor's orders with filters"""
        
        query = db.query(Order).filter(Order.vendor_id == vendor_id)
        
        if filters.status:
            query = query.filter(Order.status == filters.status)
        
        if filters.customer_id:
            query = query.filter(Order.customer_id == filters.customer_id)
        
        if filters.date_from:
            query = query.filter(Order.created_at >= filters.date_from)
        
        if filters.date_to:
            query = query.filter(Order.created_at <= filters.date_to)
        
        if filters.search:
            query = query.filter(
                or_(
                    Order.order_number.ilike(f"%{filters.search}%"),
                    Order.customer.has(
                        or_(
                            Customer.first_name.ilike(f"%{filters.search}%"),
                            Customer.last_name.ilike(f"%{filters.search}%"),
                            Customer.email.ilike(f"%{filters.search}%")
                        )
                    )
                )
            )
        
        return query.order_by(
            Order.created_at.desc()
        ).offset(skip).limit(limit).all()
    
    async def update_order_status(
        self,
        vendor_id: int,
        order_id: int,
        status_update: OrderStatusUpdate,
        changed_by_id: int,
        db: Session
    ) -> Order:
        """Update order status"""
        
        order = db.query(Order).filter(
            Order.id == order_id,
            Order.vendor_id == vendor_id
        ).first()
        
        if not order:
            raise OrderNotFoundError()
        
        old_status = order.status
        new_status = status_update.status
        
        # Update order
        order.status = new_status
        
        if status_update.tracking_number:
            order.tracking_number = status_update.tracking_number
        
        if new_status == "shipped":
            order.shipped_at = datetime.utcnow()
        elif new_status == "delivered":
            order.delivered_at = datetime.utcnow()
        
        # Record status change
        status_history = OrderStatusHistory(
            order_id=order.id,
            from_status=old_status,
            to_status=new_status,
            changed_by_type="vendor",
            changed_by_id=changed_by_id,
            notes=status_update.notes
        )
        db.add(status_history)
        
        db.commit()
        db.refresh(order)
        
        # Send notification to customer
        await self._send_status_update_email(order, old_status, new_status, db)
        
        return order
    
    def _generate_order_number(self, vendor_id: int, db: Session) -> str:
        """Generate unique order number"""
        from models.database.vendor import Vendor
        
        vendor = db.query(Vendor).get(vendor_id)
        today = datetime.utcnow().strftime("%Y%m%d")
        
        # Count orders today
        today_start = datetime.utcnow().replace(hour=0, minute=0, second=0)
        count = db.query(Order).filter(
            Order.vendor_id == vendor_id,
            Order.created_at >= today_start
        ).count()
        
        return f"{vendor.vendor_code}-{today}-{count+1:05d}"
    
    def _calculate_shipping(self, method: str) -> Decimal:
        """Calculate shipping cost"""
        shipping_rates = {
            "standard": Decimal("5.00"),
            "express": Decimal("15.00"),
            "free": Decimal("0.00")
        }
        return shipping_rates.get(method, Decimal("5.00"))
    
    def _calculate_tax(self, subtotal: Decimal, country: str) -> Decimal:
        """Calculate tax amount"""
        tax_rate = self._get_tax_rate(country)
        return subtotal * tax_rate / 100
    
    def _get_tax_rate(self, country: str) -> Decimal:
        """Get tax rate by country"""
        tax_rates = {
            "LU": Decimal("17.00"),  # Luxembourg VAT
            "FR": Decimal("20.00"),  # France VAT
            "DE": Decimal("19.00"),  # Germany VAT
            "BE": Decimal("21.00"),  # Belgium VAT
        }
        return tax_rates.get(country, Decimal("0.00"))
    
    async def _reserve_inventory(
        self,
        product: Product,
        quantity: int,
        order_id: int,
        db: Session
    ):
        """Reserve inventory for order"""
        
        inventory = db.query(Inventory).filter(
            Inventory.product_id == product.id
        ).first()
        
        if inventory:
            inventory.available_quantity -= quantity
            inventory.reserved_quantity += quantity
            
            # Record movement
            movement = InventoryMovement(
                inventory_id=inventory.id,
                movement_type="reserved",
                quantity_change=-quantity,
                reference_type="order",
                reference_id=order_id,
                notes=f"Reserved for order #{order_id}"
            )
            db.add(movement)
        
        # Update product stock count
        product.stock_quantity -= quantity
    
    async def _send_order_confirmation(self, order: Order, db: Session):
        """Send order confirmation email to customer"""
        # Implement email sending
        # This will be part of notification service
        pass
    
    async def _send_status_update_email(
        self,
        order: Order,
        old_status: str,
        new_status: str,
        db: Session
    ):
        """Send order status update email"""
        # Implement email sending
        pass

API Endpoints

Customer Order Endpoints (app/api/v1/public/vendors/orders.py)

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

router = APIRouter()

@router.post("", response_model=OrderResponse, status_code=201)
async def create_order(
    vendor_id: int,
    order_data: OrderCreate,
    current_customer: Customer = Depends(get_current_customer),
    db: Session = Depends(get_db)
):
    """Place order from cart"""
    
    # Get customer's cart
    cart = db.query(Cart).filter(
        Cart.vendor_id == vendor_id,
        Cart.customer_id == current_customer.id
    ).first()
    
    if not cart:
        raise HTTPException(status_code=404, detail="Cart not found")
    
    service = OrderService()
    order = await service.create_order_from_cart(
        vendor_id,
        current_customer.id,
        cart.id,
        order_data,
        db
    )
    
    return order

@router.get("/my-orders", response_model=List[OrderResponse])
async def get_my_orders(
    vendor_id: int,
    skip: int = 0,
    limit: int = 20,
    current_customer: Customer = Depends(get_current_customer),
    db: Session = Depends(get_db)
):
    """Get customer's order history"""
    
    service = OrderService()
    orders = service.get_customer_orders(
        vendor_id,
        current_customer.id,
        db,
        skip,
        limit
    )
    
    return orders

@router.get("/my-orders/{order_id}", response_model=OrderResponse)
async def get_my_order(
    vendor_id: int,
    order_id: int,
    current_customer: Customer = Depends(get_current_customer),
    db: Session = Depends(get_db)
):
    """Get specific order details"""
    
    order = db.query(Order).filter(
        Order.id == order_id,
        Order.vendor_id == vendor_id,
        Order.customer_id == current_customer.id
    ).first()
    
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    
    return order

Vendor Order Management Endpoints (app/api/v1/vendor/orders.py)

@router.get("", response_model=List[OrderResponse])
async def get_vendor_orders(
    status: Optional[str] = None,
    customer_id: Optional[int] = None,
    date_from: Optional[datetime] = None,
    date_to: Optional[datetime] = None,
    search: Optional[str] = None,
    skip: int = 0,
    limit: int = 50,
    current_user: User = Depends(get_current_vendor_user),
    vendor: Vendor = Depends(get_current_vendor),
    db: Session = Depends(get_db)
):
    """Get vendor's orders with filters"""
    
    filters = OrderListFilters(
        status=status,
        customer_id=customer_id,
        date_from=date_from,
        date_to=date_to,
        search=search
    )
    
    service = OrderService()
    orders = service.get_vendor_orders(vendor.id, db, filters, skip, limit)
    
    return orders

@router.get("/{order_id}", response_model=OrderResponse)
async def get_order_details(
    order_id: int,
    current_user: User = Depends(get_current_vendor_user),
    vendor: Vendor = Depends(get_current_vendor),
    db: Session = Depends(get_db)
):
    """Get order details"""
    
    order = db.query(Order).filter(
        Order.id == order_id,
        Order.vendor_id == vendor.id
    ).first()
    
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    
    return order

@router.put("/{order_id}/status", response_model=OrderResponse)
async def update_order_status(
    order_id: int,
    status_update: OrderStatusUpdate,
    current_user: User = Depends(get_current_vendor_user),
    vendor: Vendor = Depends(get_current_vendor),
    db: Session = Depends(get_db)
):
    """Update order status"""
    
    service = OrderService()
    order = await service.update_order_status(
        vendor.id,
        order_id,
        status_update,
        current_user.id,
        db
    )
    
    return order

🎨 Frontend Implementation

Templates

Checkout Page (templates/shop/checkout.html)

{% extends "shop/base_shop.html" %}

{% block content %}
<div x-data="checkout()" x-init="init()">
    <!-- Progress Steps -->
    <div class="checkout-steps">
        <div class="step" :class="{'active': currentStep >= 1, 'completed': currentStep > 1}">
            <span class="step-number">1</span>
            <span class="step-label">Cart Review</span>
        </div>
        <div class="step" :class="{'active': currentStep >= 2, 'completed': currentStep > 2}">
            <span class="step-number">2</span>
            <span class="step-label">Shipping</span>
        </div>
        <div class="step" :class="{'active': currentStep >= 3, 'completed': currentStep > 3}">
            <span class="step-number">3</span>
            <span class="step-label">Payment</span>
        </div>
        <div class="step" :class="{'active': currentStep === 4}">
            <span class="step-number">4</span>
            <span class="step-label">Confirmation</span>
        </div>
    </div>
    
    <!-- Step 1: Cart Review -->
    <div x-show="currentStep === 1" class="checkout-section">
        <h2>Review Your Cart</h2>
        <div class="cart-items">
            <template x-for="item in cart.items" :key="item.id">
                <div class="cart-item">
                    <img :src="item.product_image" :alt="item.product_title">
                    <div class="item-details">
                        <h3 x-text="item.product_title"></h3>
                        <p>Quantity: <span x-text="item.quantity"></span></p>
                        <p class="price"><span x-text="item.line_total.toFixed(2)"></span></p>
                    </div>
                </div>
            </template>
        </div>
        
        <div class="cart-summary">
            <p>Subtotal: €<span x-text="cart.subtotal.toFixed(2)"></span></p>
            <button @click="nextStep()" class="btn btn-primary btn-lg">
                Continue to Shipping
            </button>
        </div>
    </div>
    
    <!-- Step 2: Shipping Address -->
    <div x-show="currentStep === 2" class="checkout-section">
        <h2>Shipping Address</h2>
        
        <!-- Existing Addresses -->
        <template x-if="addresses.length > 0">
            <div class="address-selection">
                <template x-for="address in addresses" :key="address.id">
                    <div 
                        class="address-card"
                        :class="{'selected': selectedShippingAddress === address.id}"
                        @click="selectedShippingAddress = address.id"
                    >
                        <input 
                            type="radio" 
                            :value="address.id"
                            x-model="selectedShippingAddress"
                        >
                        <div class="address-info">
                            <strong x-text="`${address.first_name} ${address.last_name}`"></strong>
                            <p x-text="address.address_line1"></p>
                            <p x-text="`${address.postal_code} ${address.city}`"></p>
                        </div>
                    </div>
                </template>
            </div>
        </template>
        
        <button @click="showAddressForm = true" class="btn btn-secondary">
            + Add New Address
        </button>
        
        <!-- New Address Form -->
        <div x-show="showAddressForm" class="address-form">
            <!-- Address form fields -->
        </div>
        
        <div class="checkout-actions">
            <button @click="prevStep()" class="btn btn-secondary">Back</button>
            <button 
                @click="nextStep()" 
                class="btn btn-primary"
                :disabled="!selectedShippingAddress"
            >
                Continue to Payment
            </button>
        </div>
    </div>
    
    <!-- Step 3: Payment -->
    <div x-show="currentStep === 3" class="checkout-section">
        <h2>Payment Method</h2>
        
        <div class="payment-methods">
            <div class="payment-option">
                <input type="radio" value="stripe" x-model="paymentMethod">
                <label>Credit Card (Stripe)</label>
            </div>
        </div>
        
        <!-- Stripe Payment Element -->
        <div id="payment-element">
            <!-- Stripe.js will inject payment form here -->
        </div>
        
        <div class="order-summary">
            <h3>Order Summary</h3>
            <p>Subtotal: €<span x-text="cart.subtotal.toFixed(2)"></span></p>
            <p>Shipping: €<span x-text="shippingCost.toFixed(2)"></span></p>
            <p>Tax: €<span x-text="taxAmount.toFixed(2)"></span></p>
            <p class="total">Total: €<span x-text="totalAmount.toFixed(2)"></span></p>
        </div>
        
        <div class="checkout-actions">
            <button @click="prevStep()" class="btn btn-secondary">Back</button>
            <button 
                @click="placeOrder()" 
                class="btn btn-primary btn-lg"
                :disabled="placing"
            >
                <span x-show="!placing">Place Order</span>
                <span x-show="placing" class="loading-spinner"></span>
            </button>
        </div>
    </div>
    
    <!-- Step 4: Confirmation -->
    <div x-show="currentStep === 4" class="checkout-section">
        <div class="order-confirmation">
            <h2>✓ Order Confirmed!</h2>
            <p>Thank you for your order!</p>
            <p>Order Number: <strong x-text="orderNumber"></strong></p>
            <p>We've sent a confirmation email to your address.</p>
            
            <a href="/shop/account/orders" class="btn btn-primary">
                View Order Details
            </a>
        </div>
    </div>
</div>

<script>
    window.vendorId = {{ vendor.id }};
    window.customerId = {{ customer.id if customer else 'null' }};
</script>
{% endblock %}

{% block extra_scripts %}
<script src="https://js.stripe.com/v3/"></script>
<script>
function checkout() {
    return {
        currentStep: 1,
        cart: {},
        addresses: [],
        selectedShippingAddress: null,
        selectedBillingAddress: null,
        paymentMethod: 'stripe',
        showAddressForm: false,
        placing: false,
        orderNumber: null,
        
        // Calculated
        shippingCost: 5.00,
        taxRate: 0.17,
        
        get taxAmount() {
            return this.cart.subtotal * this.taxRate;
        },
        
        get totalAmount() {
            return this.cart.subtotal + this.shippingCost + this.taxAmount;
        },
        
        async init() {
            await this.loadCart();
            await this.loadAddresses();
        },
        
        async loadCart() {
            try {
                const sessionId = getSessionId();
                this.cart = await apiClient.get(
                    `/api/v1/public/vendors/${window.vendorId}/cart/${sessionId}`
                );
            } catch (error) {
                showNotification('Failed to load cart', 'error');
            }
        },
        
        async loadAddresses() {
            try {
                this.addresses = await apiClient.get(
                    `/api/v1/public/vendors/${window.vendorId}/customers/addresses`
                );
                
                // Select default address
                const defaultAddr = this.addresses.find(a => a.is_default);
                if (defaultAddr) {
                    this.selectedShippingAddress = defaultAddr.id;
                    this.selectedBillingAddress = defaultAddr.id;
                }
            } catch (error) {
                console.error('Failed to load addresses', error);
            }
        },
        
        nextStep() {
            if (this.currentStep < 4) {
                this.currentStep++;
            }
        },
        
        prevStep() {
            if (this.currentStep > 1) {
                this.currentStep--;
            }
        },
        
        async placeOrder