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; } } } } {% endblock %} ``` #### Customer Order History (`templates/shop/account/orders.html`) ```html {% extends "shop/base_shop.html" %} {% block content %}

My Orders

{% endblock %} {% block extra_scripts %} {% endblock %} ``` #### Vendor Order Management (`templates/vendor/orders/list.html`) ```html {% extends "vendor/base_vendor.html" %} {% block title %}Order Management{% endblock %} {% block content %}
{% endblock %} {% block extra_scripts %} {% 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`) ```python 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`) ```python 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`) ```python 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`) ```python 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`) ```python @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`) ```html {% extends "shop/base_shop.html" %} {% block content %}
1 Cart Review
2 Shipping
3 Payment
4 Confirmation

Review Your Cart

Subtotal: €

Shipping Address

Payment Method

Order Summary

Subtotal: €

Shipping: €

Tax: €

Total: €

✓ Order Confirmed!

Thank you for your order!

Order Number:

We've sent a confirmation email to your address.

View Order Details
{% endblock %} {% block extra_scripts %}