diff --git a/app/api/v1/admin/monitoring.py b/app/api/v1/admin/monitoring.py
new file mode 100644
index 00000000..35f4f24b
--- /dev/null
+++ b/app/api/v1/admin/monitoring.py
@@ -0,0 +1 @@
+# Platform monitoring and alerts
diff --git a/app/api/v1/public/vendors/payments.py b/app/api/v1/public/vendors/payments.py
new file mode 100644
index 00000000..d23e8e75
--- /dev/null
+++ b/app/api/v1/public/vendors/payments.py
@@ -0,0 +1 @@
+# Payment processing
diff --git a/app/api/v1/public/vendors/search.py b/app/api/v1/public/vendors/search.py
new file mode 100644
index 00000000..0e14768d
--- /dev/null
+++ b/app/api/v1/public/vendors/search.py
@@ -0,0 +1 @@
+# Product search functionality
diff --git a/app/api/v1/public/vendors/shop.py b/app/api/v1/public/vendors/shop.py
new file mode 100644
index 00000000..c753d234
--- /dev/null
+++ b/app/api/v1/public/vendors/shop.py
@@ -0,0 +1 @@
+# Public shop info
diff --git a/app/api/v1/shared/health.py b/app/api/v1/shared/health.py
new file mode 100644
index 00000000..069c2bac
--- /dev/null
+++ b/app/api/v1/shared/health.py
@@ -0,0 +1 @@
+# Health checks
diff --git a/app/api/v1/shared/uploads.py b/app/api/v1/shared/uploads.py
new file mode 100644
index 00000000..9a4e1aa0
--- /dev/null
+++ b/app/api/v1/shared/uploads.py
@@ -0,0 +1 @@
+# File upload handling
diff --git a/app/api/v1/shared/webhooks.py b/app/api/v1/shared/webhooks.py
new file mode 100644
index 00000000..acfcff9e
--- /dev/null
+++ b/app/api/v1/shared/webhooks.py
@@ -0,0 +1 @@
+# External webhooks (Stripe, etc.
diff --git a/app/api/v1/vendor/customers.py b/app/api/v1/vendor/customers.py
new file mode 100644
index 00000000..e0c6d1ea
--- /dev/null
+++ b/app/api/v1/vendor/customers.py
@@ -0,0 +1 @@
+# Vendor customer management
diff --git a/app/api/v1/vendor/media.py b/app/api/v1/vendor/media.py
new file mode 100644
index 00000000..e6751751
--- /dev/null
+++ b/app/api/v1/vendor/media.py
@@ -0,0 +1 @@
+# File and media management
diff --git a/app/api/v1/vendor/notifications.py b/app/api/v1/vendor/notifications.py
new file mode 100644
index 00000000..2154ca57
--- /dev/null
+++ b/app/api/v1/vendor/notifications.py
@@ -0,0 +1 @@
+# Notification management
diff --git a/app/api/v1/vendor/payments.py b/app/api/v1/vendor/payments.py
new file mode 100644
index 00000000..2136b212
--- /dev/null
+++ b/app/api/v1/vendor/payments.py
@@ -0,0 +1 @@
+# Payment configuration and processing
diff --git a/app/api/v1/vendor/settings.py b/app/api/v1/vendor/settings.py
new file mode 100644
index 00000000..9d16efa5
--- /dev/null
+++ b/app/api/v1/vendor/settings.py
@@ -0,0 +1 @@
+# Vendor settings and configuration
diff --git a/app/api/v1/vendor/teams.py b/app/api/v1/vendor/teams.py
new file mode 100644
index 00000000..b8dcf518
--- /dev/null
+++ b/app/api/v1/vendor/teams.py
@@ -0,0 +1 @@
+# Team member management
diff --git a/app/exceptions/backup.py b/app/exceptions/backup.py
new file mode 100644
index 00000000..f8e0394d
--- /dev/null
+++ b/app/exceptions/backup.py
@@ -0,0 +1 @@
+# Backup/recovery exceptions
diff --git a/app/exceptions/marketplace.py b/app/exceptions/marketplace.py
new file mode 100644
index 00000000..1a92b6ab
--- /dev/null
+++ b/app/exceptions/marketplace.py
@@ -0,0 +1 @@
+# Import/marketplace exceptions
diff --git a/app/exceptions/media.py b/app/exceptions/media.py
new file mode 100644
index 00000000..a56df71b
--- /dev/null
+++ b/app/exceptions/media.py
@@ -0,0 +1 @@
+# Media/file management exceptions
diff --git a/app/exceptions/monitoring.py b/app/exceptions/monitoring.py
new file mode 100644
index 00000000..70f33c86
--- /dev/null
+++ b/app/exceptions/monitoring.py
@@ -0,0 +1 @@
+# Monitoring exceptions
diff --git a/app/exceptions/notification.py b/app/exceptions/notification.py
new file mode 100644
index 00000000..c1772546
--- /dev/null
+++ b/app/exceptions/notification.py
@@ -0,0 +1 @@
+# Notification exceptions
diff --git a/app/exceptions/payment.py b/app/exceptions/payment.py
new file mode 100644
index 00000000..c3fbe15b
--- /dev/null
+++ b/app/exceptions/payment.py
@@ -0,0 +1 @@
+# Payment processing exceptions
diff --git a/app/exceptions/search.py b/app/exceptions/search.py
new file mode 100644
index 00000000..5db85736
--- /dev/null
+++ b/app/exceptions/search.py
@@ -0,0 +1 @@
+# Search exceptions
diff --git a/app/services/audit_service.py b/app/services/audit_service.py
new file mode 100644
index 00000000..ea03eb12
--- /dev/null
+++ b/app/services/audit_service.py
@@ -0,0 +1 @@
+# Audit logging services
diff --git a/app/services/backup_service.py b/app/services/backup_service.py
new file mode 100644
index 00000000..b4d8b0d4
--- /dev/null
+++ b/app/services/backup_service.py
@@ -0,0 +1 @@
+# Backup and recovery services
diff --git a/app/services/cache_service.py b/app/services/cache_service.py
new file mode 100644
index 00000000..b0c83d5f
--- /dev/null
+++ b/app/services/cache_service.py
@@ -0,0 +1 @@
+# Caching services
diff --git a/app/services/configuration_service.py b/app/services/configuration_service.py
new file mode 100644
index 00000000..ff83d3b4
--- /dev/null
+++ b/app/services/configuration_service.py
@@ -0,0 +1 @@
+# Configuration management services
diff --git a/app/services/marketplace_service.py b/app/services/marketplace_service.py
new file mode 100644
index 00000000..00533012
--- /dev/null
+++ b/app/services/marketplace_service.py
@@ -0,0 +1 @@
+# Marketplace import services (MarketplaceProduct
diff --git a/app/services/media_service.py b/app/services/media_service.py
new file mode 100644
index 00000000..7d008007
--- /dev/null
+++ b/app/services/media_service.py
@@ -0,0 +1 @@
+# File and media management services
diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py
new file mode 100644
index 00000000..f1d92af4
--- /dev/null
+++ b/app/services/monitoring_service.py
@@ -0,0 +1 @@
+# Application monitoring services
diff --git a/app/services/notification_service.py b/app/services/notification_service.py
new file mode 100644
index 00000000..c25cb0d9
--- /dev/null
+++ b/app/services/notification_service.py
@@ -0,0 +1 @@
+# Email/notification services
diff --git a/app/services/payment_service.py b/app/services/payment_service.py
new file mode 100644
index 00000000..ec9ec61e
--- /dev/null
+++ b/app/services/payment_service.py
@@ -0,0 +1 @@
+# Payment processing services
diff --git a/app/services/search_service.py b/app/services/search_service.py
new file mode 100644
index 00000000..f2f192b8
--- /dev/null
+++ b/app/services/search_service.py
@@ -0,0 +1 @@
+# Search and indexing services
diff --git a/docs/project-roadmap/implementation_roadmap.md b/docs/project-roadmap/implementation_roadmap.md
new file mode 100644
index 00000000..cd5ea6af
--- /dev/null
+++ b/docs/project-roadmap/implementation_roadmap.md
@@ -0,0 +1,512 @@
+# Implementation Roadmap
+## Multi-Tenant Ecommerce Platform - Complete Development Guide
+
+**Last Updated**: October 11, 2025
+**Project Status**: Slice 1 In Progress (~75% complete)
+
+---
+
+## 📚 Documentation Structure
+
+Your complete vertical slice documentation is organized as follows:
+
+```
+docs/slices/
+├── 00_slices_overview.md ← Start here for overview
+├── 00_implementation_roadmap.md ← This file - your guide
+├── 01_slice1_admin_vendor_foundation.md
+├── 02_slice2_marketplace_import.md
+├── 03_slice3_product_catalog.md
+├── 04_slice4_customer_shopping.md
+└── 05_slice5_order_processing.md
+```
+
+---
+
+## 🎯 Quick Start Guide
+
+### For Current Development (Slice 1)
+1. ✅ Read `01_slice1_admin_vendor_foundation.md`
+2. ✅ Review what's marked as complete vs. in-progress
+3. ⏳ Focus on vendor login and dashboard pages
+4. ⏳ Complete testing checklist
+5. ⏳ Deploy to staging
+
+### For Future Slices
+1. Complete current slice 100%
+2. Read next slice documentation
+3. Set up backend (models, schemas, services, APIs)
+4. Build frontend (Jinja2 templates + Alpine.js)
+5. Test thoroughly
+6. Move to next slice
+
+---
+
+## 📊 Current Status Overview
+
+### Slice 1: Multi-Tenant Foundation (75% Complete)
+
+#### ✅ Completed
+- Backend database models (User, Vendor, Role, VendorUser)
+- Authentication system (JWT, bcrypt)
+- Admin service layer (vendor creation with owner)
+- Admin API endpoints (CRUD, dashboard)
+- Vendor context middleware (subdomain + path detection)
+- Admin login page (HTML + Alpine.js)
+- Admin dashboard (HTML + Alpine.js)
+- Admin vendor creation page (HTML + Alpine.js)
+
+#### ⏳ In Progress
+- Vendor login page (frontend)
+- Vendor dashboard page (frontend)
+- Testing and debugging
+- Deployment configuration
+
+#### 📋 To Do
+- Complete vendor login/dashboard
+- Full testing (see Slice 1 checklist)
+- Documentation updates
+- Staging deployment
+
+### Slices 2-5: Not Started
+
+All future slices have complete documentation ready to implement.
+
+---
+
+## 🗓️ Development Timeline
+
+### Week 1: Slice 1 - Foundation ⏳ CURRENT
+**Days 1-3**: ✅ Backend complete
+**Days 4-5**: ⏳ Frontend completion
+
+**Deliverable**: Admin can create vendors, vendors can log in
+
+### Week 2: Slice 2 - Marketplace Import
+**Days 1-3**: Backend (CSV import, Celery tasks, MarketplaceProduct model)
+**Days 4-5**: Frontend (import UI with Alpine.js, status tracking)
+
+**Deliverable**: Vendors can import products from Letzshop CSV
+
+### Week 3: Slice 3 - Product Catalog
+**Days 1-3**: Backend (Product model, publishing, inventory)
+**Days 4-5**: Frontend (product management, catalog UI)
+
+**Deliverable**: Vendors can manage product catalog
+
+### Week 4: Slice 4 - Customer Shopping
+**Days 1-3**: Backend (Customer model, Cart, public APIs)
+**Days 4-5**: Frontend (shop pages, cart with Alpine.js)
+
+**Deliverable**: Customers can browse and shop
+
+### Week 5: Slice 5 - Order Processing
+**Days 1-3**: Backend (Order model, checkout, order management)
+**Days 4-5**: Frontend (checkout flow, order history)
+
+**Deliverable**: Complete order workflow, platform ready for production
+
+---
+
+## 🎨 Technology Stack
+
+### Backend
+- **Framework**: FastAPI (Python 3.11+)
+- **Database**: PostgreSQL + SQLAlchemy ORM
+- **Authentication**: JWT tokens + bcrypt
+- **Background Jobs**: Celery + Redis/RabbitMQ
+- **API Docs**: Auto-generated OpenAPI/Swagger
+
+### Frontend
+- **Templating**: Jinja2 (server-side rendering)
+- **JavaScript**: Alpine.js v3.x (15KB, CDN-based)
+- **CSS**: Custom CSS with CSS variables
+- **AJAX**: Fetch API (vanilla JavaScript)
+- **No Build Step**: Everything runs directly in browser
+
+### Why This Stack?
+- ✅ **Alpine.js**: Lightweight reactivity without build complexity
+- ✅ **Jinja2**: Server-side rendering for SEO and performance
+- ✅ **No Build Step**: Faster development, easier deployment
+- ✅ **FastAPI**: Modern Python, async support, auto-docs
+- ✅ **PostgreSQL**: Robust, reliable, feature-rich
+
+---
+
+## 📋 Implementation Checklist
+
+Use this checklist to track your progress across all slices:
+
+### Slice 1: Foundation
+- [x] Backend models created
+- [x] Authentication system working
+- [x] Admin service layer complete
+- [x] Admin API endpoints working
+- [x] Vendor context middleware working
+- [x] Admin login page created
+- [x] Admin dashboard created
+- [x] Admin vendor creation page created
+- [ ] Vendor login page created
+- [ ] Vendor dashboard page created
+- [ ] All tests passing
+- [ ] Deployed to staging
+
+### Slice 2: Marketplace Import
+- [ ] MarketplaceProduct model
+- [ ] MarketplaceImportJob model
+- [ ] CSV processing service
+- [ ] Celery tasks configured
+- [ ] Import API endpoints
+- [ ] Import UI pages
+- [ ] Status tracking with Alpine.js
+- [ ] All tests passing
+
+### Slice 3: Product Catalog
+- [ ] Product model complete
+- [ ] Inventory model complete
+- [ ] Product service layer
+- [ ] Publishing logic
+- [ ] Product API endpoints
+- [ ] Product management UI
+- [ ] Catalog browsing
+- [ ] All tests passing
+
+### Slice 4: Customer Shopping
+- [ ] Customer model
+- [ ] Cart model
+- [ ] Customer service layer
+- [ ] Cart service layer
+- [ ] Public product APIs
+- [ ] Shop homepage
+- [ ] Product detail pages
+- [ ] Shopping cart UI
+- [ ] Customer registration/login
+- [ ] All tests passing
+
+### Slice 5: Order Processing
+- [ ] Order model
+- [ ] OrderItem model
+- [ ] Order service layer
+- [ ] Checkout logic
+- [ ] Order API endpoints
+- [ ] Checkout UI (multi-step)
+- [ ] Customer order history
+- [ ] Vendor order management
+- [ ] Email notifications
+- [ ] Payment integration (Stripe)
+- [ ] All tests passing
+- [ ] Production ready
+
+---
+
+## 🎯 Each Slice Must Include
+
+### Backend Checklist
+- [ ] Database models defined
+- [ ] Pydantic schemas created
+- [ ] Service layer implemented
+- [ ] API endpoints created
+- [ ] Exception handling added
+- [ ] Database migrations applied
+- [ ] Unit tests written
+- [ ] Integration tests written
+
+### Frontend Checklist
+- [ ] Jinja2 templates created
+- [ ] Alpine.js components implemented
+- [ ] CSS styling applied
+- [ ] API integration working
+- [ ] Loading states added
+- [ ] Error handling added
+- [ ] Mobile responsive
+- [ ] Browser tested (Chrome, Firefox, Safari)
+
+### Documentation Checklist
+- [ ] Slice documentation updated
+- [ ] API endpoints documented
+- [ ] Frontend components documented
+- [ ] Testing checklist completed
+- [ ] Known issues documented
+- [ ] Next steps identified
+
+---
+
+## 🔧 Development Workflow
+
+### Starting a New Slice
+
+1. **Read Documentation**
+ ```bash
+ # Open the slice markdown file
+ code docs/slices/0X_sliceX_name.md
+ ```
+
+2. **Set Up Backend**
+ ```bash
+ # Create database models
+ # Create Pydantic schema
+ # Implement service layer
+ # Create API endpoints
+ # Write tests
+ ```
+
+3. **Set Up Frontend**
+ ```bash
+ # Create Jinja2 templates
+ # Add Alpine.js components
+ # Style with CSS
+ # Test in browser
+ ```
+
+4. **Test Thoroughly**
+ ```bash
+ # Run backend tests
+ pytest tests/
+
+ # Manual testing
+ # Use testing checklist in slice docs
+ ```
+
+5. **Deploy & Demo**
+ ```bash
+ # Deploy to staging
+ # Demo to stakeholders
+ # Gather feedback
+ ```
+
+### Daily Development Flow
+
+**Morning**
+- Review slice documentation
+- Identify today's goals (backend or frontend)
+- Check testing checklist
+
+**During Development**
+- Follow code patterns from slice docs
+- Use Alpine.js examples provided
+- Keep vendor isolation in mind
+- Test incrementally
+
+**End of Day**
+- Update slice documentation with progress
+- Mark completed items in checklist
+- Note any blockers or issues
+- Commit code with meaningful messages
+
+---
+
+## 🎨 Alpine.js Patterns
+
+### Basic Component Pattern
+```javascript
+function myComponent() {
+ return {
+ // State
+ data: [],
+ loading: false,
+ error: null,
+
+ // Lifecycle
+ init() {
+ this.loadData();
+ },
+
+ // Methods
+ async loadData() {
+ this.loading = true;
+ try {
+ this.data = await apiClient.get('/api/endpoint');
+ } catch (error) {
+ this.error = error.message;
+ } finally {
+ this.loading = false;
+ }
+ }
+ }
+}
+```
+
+### Template Usage
+```html
+
+```
+
+### Common Directives
+- `x-data` - Component state
+- `x-init` - Initialization
+- `x-show` - Toggle visibility
+- `x-if` - Conditional rendering
+- `x-for` - Loop through arrays
+- `x-model` - Two-way binding
+- `@click` - Event handling
+- `:class` - Dynamic classes
+- `x-text` - Text content
+- `x-html` - HTML content
+
+---
+
+## 📚 Key Resources
+
+### Documentation Files
+- `00_slices_overview.md` - Complete overview
+- `01_slice1_admin_vendor_foundation.md` - Current work
+- `../quick_start_guide.md` - Setup guide
+- `../css_structure_guide.txt` - CSS organization
+- `../css_quick_reference.txt` - CSS usage
+- `../12.project_readme_final.md` - Complete README
+
+### External Resources
+- [Alpine.js Documentation](https://alpinejs.dev/)
+- [FastAPI Documentation](https://fastapi.tiangolo.com/)
+- [Jinja2 Documentation](https://jinja.palletsprojects.com/)
+- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
+
+---
+
+## 🚨 Common Pitfalls to Avoid
+
+### Backend
+- ❌ Forgetting vendor isolation in queries
+- ❌ Not validating vendor_id in API endpoints
+- ❌ Skipping database indexes
+- ❌ Not handling edge cases
+- ❌ Missing error handling
+
+### Frontend
+- ❌ Not handling loading states
+- ❌ Not displaying error messages
+- ❌ Forgetting mobile responsiveness
+- ❌ Not testing in multiple browsers
+- ❌ Mixing vendor contexts
+
+### General
+- ❌ Skipping tests
+- ❌ Not updating documentation
+- ❌ Moving to next slice before completing current
+- ❌ Not following naming conventions
+- ❌ Committing without testing
+
+---
+
+## ✅ Quality Gates
+
+Before moving to the next slice, ensure:
+
+1. **All Features Complete**
+ - All user stories implemented
+ - All acceptance criteria met
+ - All API endpoints working
+
+2. **All Tests Pass**
+ - Backend unit tests
+ - Backend integration tests
+ - Frontend manual testing
+ - Security testing (vendor isolation)
+
+3. **Documentation Updated**
+ - Slice documentation current
+ - API docs updated
+ - Testing checklist completed
+
+4. **Code Quality**
+ - Follows naming conventions
+ - No console errors
+ - No security vulnerabilities
+ - Performance acceptable
+
+5. **Stakeholder Approval**
+ - Demo completed
+ - Feedback incorporated
+ - Sign-off received
+
+---
+
+## 🎉 Success Metrics
+
+### After Slice 1
+- ✅ Admin can create vendors
+- ✅ Vendors can log in
+- ✅ Vendor isolation works
+- ✅ Context detection works
+
+### After Slice 2
+- ✅ Vendors can import CSVs
+- ✅ Background processing works
+- ✅ Import tracking functional
+
+### After Slice 3
+- ✅ Products published to catalog
+- ✅ Inventory management working
+- ✅ Product customization enabled
+
+### After Slice 4
+- ✅ Customers can browse products
+- ✅ Shopping cart functional
+- ✅ Customer accounts working
+
+### After Slice 5
+- ✅ Complete checkout workflow
+- ✅ Order management operational
+- ✅ **Platform production-ready!**
+
+---
+
+## 🚀 Ready to Start?
+
+### Current Focus: Complete Slice 1
+
+**Your immediate next steps:**
+
+1. ✅ Read `01_slice1_admin_vendor_foundation.md`
+2. ⏳ Complete vendor login page (`templates/vendor/login.html`)
+3. ⏳ Complete vendor dashboard (`templates/vendor/dashboard.html`)
+4. ⏳ Test complete admin → vendor flow
+5. ⏳ Check all items in Slice 1 testing checklist
+6. ⏳ Deploy to staging
+7. ⏳ Demo to stakeholders
+8. ✅ Move to Slice 2
+
+### Need Help?
+
+- Check the slice documentation for detailed implementation
+- Review Alpine.js examples in the docs
+- Look at CSS guides for styling
+- Test frequently and incrementally
+- Update documentation as you progress
+
+---
+
+## 💡 Pro Tips
+
+1. **Work Incrementally**: Complete one component at a time
+2. **Test Continuously**: Don't wait until the end to test
+3. **Follow Patterns**: Use the examples in slice documentation
+4. **Document as You Go**: Update docs while code is fresh
+5. **Ask for Reviews**: Get feedback early and often
+6. **Celebrate Progress**: Each completed slice is a milestone!
+
+---
+
+## 📞 Support
+
+If you need assistance:
+- Review the slice-specific documentation
+- Check the testing checklists
+- Look at the example code provided
+- Refer to the technology stack documentation
+
+---
+
+**Ready to build an amazing multi-tenant ecommerce platform?**
+
+**Start with**: `01_slice1_admin_vendor_foundation.md`
+
+**You've got this!** 🚀
\ No newline at end of file
diff --git a/docs/project-roadmap/slice1_doc.md b/docs/project-roadmap/slice1_doc.md
new file mode 100644
index 00000000..b50dc35a
--- /dev/null
+++ b/docs/project-roadmap/slice1_doc.md
@@ -0,0 +1,1069 @@
+# Slice 1: Multi-Tenant Foundation
+## Admin Creates Vendor → Vendor Owner Logs In
+
+**Status**: 🔄 IN PROGRESS
+**Timeline**: Week 1 (5 days)
+**Current Progress**: Backend ~90%, Frontend ~60%
+
+## 🎯 Slice Objectives
+
+Establish the multi-tenant foundation with complete vendor isolation and admin capabilities.
+
+### User Stories
+- ✅ As a Super Admin, I can create vendors through the admin interface
+- ✅ As a Super Admin, I can manage vendor accounts (verify, activate, deactivate)
+- ⏳ As a Vendor Owner, I can log into my vendor-specific admin interface
+- ✅ The system correctly isolates vendor contexts (subdomain + path-based)
+
+### Success Criteria
+- [ ] Admin can log into admin interface
+- [ ] Admin can create new vendors with auto-generated owner accounts
+- [ ] System generates secure temporary passwords
+- [ ] Vendor owner can log into vendor-specific interface
+- [ ] Vendor context detection works in dev (path) and prod (subdomain) modes
+- [ ] Database properly isolates vendor data
+- [ ] All API endpoints protected with JWT authentication
+- [ ] Frontend integrates seamlessly with backend
+
+## 📋 Backend Implementation
+
+### Database Models (✅ Complete)
+
+#### User Model (`models/database/user.py`)
+```python
+class User(Base, TimestampMixin):
+ __tablename__ = "users"
+
+ id = Column(Integer, primary_key=True, index=True)
+ email = Column(String, unique=True, nullable=False, index=True)
+ username = Column(String, unique=True, nullable=False, index=True)
+ hashed_password = Column(String, nullable=False)
+ role = Column(String, nullable=False) # 'admin' or 'user'
+ is_active = Column(Boolean, default=True)
+
+ # Relationships
+ owned_vendors = relationship("Vendor", back_populates="owner")
+ vendor_memberships = relationship("VendorUser", back_populates="user")
+```
+
+#### Vendor Model (`models/database/vendor.py`)
+```python
+class Vendor(Base, TimestampMixin):
+ __tablename__ = "vendors"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_code = Column(String, unique=True, nullable=False, index=True)
+ subdomain = Column(String(100), unique=True, nullable=False, index=True)
+ name = Column(String, nullable=False)
+ description = Column(Text)
+ owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
+
+ # Business info
+ business_email = Column(String)
+ business_phone = Column(String)
+ website = Column(String)
+ business_address = Column(Text)
+ tax_number = Column(String)
+
+ # Status
+ is_active = Column(Boolean, default=True)
+ is_verified = Column(Boolean, default=False)
+ verified_at = Column(DateTime, nullable=True)
+
+ # Configuration
+ theme_config = Column(JSON, default=dict)
+ letzshop_csv_url_fr = Column(String)
+ letzshop_csv_url_en = Column(String)
+ letzshop_csv_url_de = Column(String)
+
+ # Relationships
+ owner = relationship("User", back_populates="owned_vendors")
+ roles = relationship("Role", back_populates="vendor", cascade="all, delete-orphan")
+ team_members = relationship("VendorUser", back_populates="vendor")
+```
+
+#### Role Model (`models/database/vendor.py`)
+```python
+class Role(Base, TimestampMixin):
+ __tablename__ = "roles"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
+ name = Column(String, nullable=False) # Owner, Manager, Editor, Viewer
+ permissions = Column(JSON, default=list)
+
+ vendor = relationship("Vendor", back_populates="roles")
+ vendor_users = relationship("VendorUser", back_populates="role")
+```
+
+### Pydantic Schemas (✅ Complete)
+
+#### Vendor Schemas (`models/schema/vendor.py`)
+```python
+class VendorCreate(BaseModel):
+ vendor_code: str = Field(..., min_length=2, max_length=50)
+ name: str = Field(..., min_length=2, max_length=200)
+ subdomain: str = Field(..., min_length=2, max_length=100)
+ owner_email: EmailStr # NEW for Slice 1
+ description: Optional[str] = None
+ business_email: Optional[EmailStr] = None
+ business_phone: Optional[str] = None
+ website: Optional[str] = None
+
+ @validator('vendor_code')
+ def vendor_code_uppercase(cls, v):
+ return v.upper()
+
+ @validator('subdomain')
+ def subdomain_lowercase(cls, v):
+ return v.lower().strip()
+
+class VendorCreateResponse(BaseModel):
+ """Response after creating vendor - includes generated credentials"""
+ id: int
+ vendor_code: str
+ subdomain: str
+ name: str
+ owner_user_id: int
+ owner_email: str
+ owner_username: str
+ temporary_password: str # Shown only once!
+ is_active: bool
+ is_verified: bool
+ created_at: datetime
+```
+
+### Service Layer (✅ Complete)
+
+#### Admin Service (`app/services/admin_service.py`)
+
+**Key Method**: `create_vendor_with_owner()`
+```python
+async def create_vendor_with_owner(
+ self,
+ vendor_data: VendorCreate,
+ db: Session
+) -> Dict[str, Any]:
+ """
+ Creates vendor + owner user + default roles
+ Returns vendor details with temporary password
+ """
+ # 1. Generate owner username
+ owner_username = f"{vendor_data.subdomain}_owner"
+
+ # 2. Generate secure temporary password
+ temp_password = self._generate_temp_password()
+
+ # 3. Create owner user
+ owner_user = User(
+ email=vendor_data.owner_email,
+ username=owner_username,
+ hashed_password=self.auth_manager.hash_password(temp_password),
+ role="user",
+ is_active=True
+ )
+ db.add(owner_user)
+ db.flush() # Get owner_user.id
+
+ # 4. Create vendor
+ vendor = Vendor(
+ vendor_code=vendor_data.vendor_code,
+ name=vendor_data.name,
+ subdomain=vendor_data.subdomain,
+ owner_user_id=owner_user.id,
+ is_verified=True, # Auto-verify admin-created vendors
+ verified_at=datetime.utcnow(),
+ # ... other fields
+ )
+ db.add(vendor)
+ db.flush() # Get vendor.id
+
+ # 5. Create default roles
+ default_roles = ["Owner", "Manager", "Editor", "Viewer"]
+ for role_name in default_roles:
+ role = Role(
+ vendor_id=vendor.id,
+ name=role_name,
+ permissions=self._get_default_permissions(role_name)
+ )
+ db.add(role)
+
+ # 6. Link owner to Owner role
+ owner_role = db.query(Role).filter(
+ Role.vendor_id == vendor.id,
+ Role.name == "Owner"
+ ).first()
+
+ vendor_user = VendorUser(
+ vendor_id=vendor.id,
+ user_id=owner_user.id,
+ role_id=owner_role.id,
+ is_active=True
+ )
+ db.add(vendor_user)
+
+ db.commit()
+
+ return {
+ "vendor": vendor,
+ "owner_username": owner_username,
+ "temporary_password": temp_password
+ }
+```
+
+### API Endpoints (✅ Complete)
+
+#### Admin Endpoints (`app/api/v1/admin.py`)
+
+```python
+@router.post("/vendors", response_model=VendorCreateResponse)
+async def create_vendor(
+ vendor_data: VendorCreate,
+ current_user: User = Depends(get_current_admin_user),
+ db: Session = Depends(get_db)
+):
+ """Create new vendor with owner account"""
+ result = await admin_service.create_vendor_with_owner(vendor_data, db)
+
+ return VendorCreateResponse(
+ id=result["vendor"].id,
+ vendor_code=result["vendor"].vendor_code,
+ subdomain=result["vendor"].subdomain,
+ name=result["vendor"].name,
+ owner_user_id=result["vendor"].owner_user_id,
+ owner_email=vendor_data.owner_email,
+ owner_username=result["owner_username"],
+ temporary_password=result["temporary_password"],
+ is_active=result["vendor"].is_active,
+ is_verified=result["vendor"].is_verified,
+ created_at=result["vendor"].created_at
+ )
+
+@router.get("/vendors", response_model=VendorListResponse)
+async def list_vendors(
+ skip: int = 0,
+ limit: int = 100,
+ is_active: Optional[bool] = None,
+ is_verified: Optional[bool] = None,
+ current_user: User = Depends(get_current_admin_user),
+ db: Session = Depends(get_db)
+):
+ """List all vendors with filtering"""
+ return await admin_service.get_vendors(db, skip, limit, is_active, is_verified)
+
+@router.get("/dashboard", response_model=AdminDashboardResponse)
+async def get_admin_dashboard(
+ current_user: User = Depends(get_current_admin_user),
+ db: Session = Depends(get_db)
+):
+ """Get admin dashboard statistics"""
+ return await admin_service.get_dashboard_stats(db)
+
+@router.put("/vendors/{vendor_id}/verify")
+async def verify_vendor(
+ vendor_id: int,
+ current_user: User = Depends(get_current_admin_user),
+ db: Session = Depends(get_db)
+):
+ """Verify/unverify vendor"""
+ return await admin_service.toggle_vendor_verification(vendor_id, db)
+
+@router.put("/vendors/{vendor_id}/status")
+async def toggle_vendor_status(
+ vendor_id: int,
+ current_user: User = Depends(get_current_admin_user),
+ db: Session = Depends(get_db)
+):
+ """Activate/deactivate vendor"""
+ return await admin_service.toggle_vendor_status(vendor_id, db)
+```
+
+### Middleware (✅ Complete)
+
+#### Vendor Context Detection (`middleware/vendor_context.py`)
+
+```python
+async def vendor_context_middleware(request: Request, call_next):
+ """
+ Detects vendor context from:
+ 1. Subdomain: vendor.platform.com (production)
+ 2. Path: /vendor/VENDOR_CODE/ (development)
+ """
+ vendor_context = None
+
+ # Skip for admin/API routes
+ if request.url.path.startswith(("/api/", "/admin/", "/static/")):
+ response = await call_next(request)
+ return response
+
+ # 1. Try subdomain detection (production)
+ host = request.headers.get("host", "").split(":")[0]
+ parts = host.split(".")
+ if len(parts) > 2:
+ subdomain = parts[0]
+ vendor = get_vendor_by_subdomain(subdomain)
+ if vendor:
+ vendor_context = vendor
+
+ # 2. Try path detection (development)
+ if not vendor_context:
+ path_parts = request.url.path.split("/")
+ if len(path_parts) > 2 and path_parts[1] == "vendor":
+ vendor_code = path_parts[2].upper()
+ vendor = get_vendor_by_code(vendor_code)
+ if vendor:
+ vendor_context = vendor
+
+ request.state.vendor = vendor_context
+ response = await call_next(request)
+ return response
+```
+
+## 🎨 Frontend Implementation
+
+### Template Structure (Jinja2)
+
+#### Base Template (`templates/base.html`)
+```html
+
+
+
+
+
+ {% block title %}Multi-Tenant Platform{% endblock %}
+
+
+
+ {% block extra_css %}{% endblock %}
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+ {% block extra_scripts %}{% endblock %}
+
+
+```
+
+### Admin Pages (🔄 In Progress)
+
+#### 1. Admin Login (`templates/admin/login.html`)
+
+**Status**: ✅ Complete
+
+```html
+{% extends "base.html" %}
+
+{% block title %}Admin Login{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+```
+
+#### 2. Admin Dashboard (`templates/admin/dashboard.html`)
+
+**Status**: ⏳ In Progress
+
+```html
+{% extends "admin/base_admin.html" %}
+
+{% block title %}Admin Dashboard{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Vendor Code |
+ Name |
+ Subdomain |
+ Status |
+ Created |
+
+
+
+
+
+ |
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+
+
+
+ No vendors yet. Create your first vendor!
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+```
+
+#### 3. Vendor Creation (`templates/admin/vendors.html`)
+
+**Status**: ⏳ In Progress
+
+Alpine.js component for creating vendors with real-time validation and credential display.
+
+### Vendor Pages (📋 To Do)
+
+#### 1. Vendor Login (`templates/vendor/login.html`)
+
+**Status**: 📋 To Do
+
+```html
+{% extends "base.html" %}
+
+{% block title %}Vendor Login{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Vendor Not Found
+
The vendor you're trying to access doesn't exist.
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+```
+
+#### 2. Vendor Dashboard (`templates/vendor/dashboard.html`)
+
+**Status**: 📋 To Do
+
+```html
+{% extends "vendor/base_vendor.html" %}
+
+{% block title %}{{ vendor.name }} Dashboard{% endblock %}
+
+{% block content %}
+
+
+
+
Welcome to {{ vendor.name }}
+
Vendor Code: {{ vendor.vendor_code }}
+
+
+ ${vendor.is_verified ? 'Verified' : 'Pending Verification'}
+
+
+
+
+
+
+
+
+
+
🚀 Coming in Slice 2
+
+ Product import from Letzshop marketplace will be available soon!
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+```
+
+## ✅ Testing Checklist
+
+### Backend Tests
+
+#### Authentication
+- [ ] Admin can login with valid credentials
+- [ ] Admin login rejects invalid credentials
+- [ ] Non-admin users cannot access admin endpoints
+- [ ] JWT tokens expire after configured time
+- [ ] Token refresh works correctly
+
+#### Vendor Management
+- [ ] Admin can create vendor with all required fields
+- [ ] System generates unique vendor code
+- [ ] System generates unique subdomain
+- [ ] Owner user account is created automatically
+- [ ] Temporary password is generated (12+ characters)
+- [ ] Default roles are created (Owner, Manager, Editor, Viewer)
+- [ ] Owner is linked to Owner role
+- [ ] Vendor is auto-verified when created by admin
+- [ ] Duplicate vendor code is rejected
+- [ ] Duplicate subdomain is rejected
+- [ ] Duplicate owner email is rejected
+
+#### Vendor Context Detection
+- [ ] Subdomain detection works: `vendor.platform.com`
+- [ ] Path detection works: `/vendor/VENDORCODE/`
+- [ ] Admin routes bypass vendor context
+- [ ] API routes bypass vendor context
+- [ ] Static routes bypass vendor context
+- [ ] Invalid vendor returns appropriate error
+
+#### API Endpoints
+- [ ] `POST /api/v1/admin/vendors` creates vendor
+- [ ] `GET /api/v1/admin/vendors` lists vendors with pagination
+- [ ] `GET /api/v1/admin/dashboard` returns statistics
+- [ ] `PUT /api/v1/admin/vendors/{id}/verify` toggles verification
+- [ ] `PUT /api/v1/admin/vendors/{id}/status` toggles active status
+- [ ] All endpoints require admin authentication
+- [ ] All endpoints return proper error messages
+
+### Frontend Tests
+
+#### Admin Login Page
+- [ ] Page loads without errors
+- [ ] Form validation works
+- [ ] Loading state displays during login
+- [ ] Error messages display correctly
+- [ ] Successful login redirects to dashboard
+- [ ] Token is stored in localStorage
+- [ ] Page is responsive on mobile
+
+#### Admin Dashboard
+- [ ] Dashboard loads with statistics
+- [ ] Vendor count is accurate
+- [ ] User count is accurate
+- [ ] Recent vendors list displays
+- [ ] Navigation works correctly
+- [ ] Refresh button updates data
+- [ ] Loading states work correctly
+- [ ] No console errors
+
+#### Vendor Creation Page
+- [ ] Form loads correctly
+- [ ] All fields are validated
+- [ ] Vendor code auto-uppercases
+- [ ] Subdomain auto-lowercases
+- [ ] Email validation works
+- [ ] Submit creates vendor successfully
+- [ ] Credentials are displayed once
+- [ ] Can copy credentials
+- [ ] Form resets after creation
+- [ ] Error messages display correctly
+
+#### Vendor Login Page
+- [ ] Page loads with vendor context
+- [ ] Vendor name displays correctly
+- [ ] Form validation works
+- [ ] Login succeeds with correct credentials
+- [ ] Login fails with wrong credentials
+- [ ] Redirects to vendor dashboard
+- [ ] Token stored in localStorage
+- [ ] "Vendor Not Found" shows for invalid vendor
+
+#### Vendor Dashboard
+- [ ] Dashboard loads successfully
+- [ ] Vendor information displays
+- [ ] Statistics load correctly
+- [ ] Welcome message shows
+- [ ] Verification badge shows correct status
+- [ ] No console errors
+
+### Database Tests
+
+#### Schema Verification
+```sql
+-- Check tables exist
+SELECT table_name FROM information_schema.tables
+WHERE table_schema = 'public';
+-- Expected: users, vendors, roles, vendor_users
+
+-- Check admin user
+SELECT * FROM users WHERE role = 'admin';
+
+-- Check vendor creation
+SELECT * FROM vendors WHERE vendor_code = 'TESTVENDOR';
+
+-- Check owner user
+SELECT * FROM users WHERE email = 'owner@testvendor.com';
+
+-- Check default roles
+SELECT * FROM roles WHERE vendor_id = (
+ SELECT id FROM vendors WHERE vendor_code = 'TESTVENDOR'
+);
+-- Expected: 4 roles (Owner, Manager, Editor, Viewer)
+```
+
+### Security Tests
+
+- [ ] Passwords are hashed with bcrypt
+- [ ] JWT tokens are properly signed
+- [ ] Admin endpoints reject non-admin users
+- [ ] Vendor endpoints require authentication
+- [ ] Cross-vendor access is prevented
+- [ ] SQL injection is prevented
+- [ ] XSS is prevented in forms
+- [ ] CSRF protection is enabled
+
+### Performance Tests
+
+- [ ] Admin login responds < 1 second
+- [ ] Dashboard loads < 2 seconds
+- [ ] Vendor creation completes < 3 seconds
+- [ ] Vendor list loads < 1 second
+- [ ] No N+1 query problems
+
+## 📝 Documentation Tasks
+
+- [ ] Update API documentation with new endpoints
+- [ ] Document Alpine.js component patterns
+- [ ] Document Jinja2 template structure
+- [ ] Create deployment guide
+- [ ] Update environment variables documentation
+- [ ] Document vendor context detection logic
+
+## 🚀 Deployment Checklist
+
+### Environment Setup
+- [ ] PostgreSQL database created
+- [ ] Environment variables configured
+- [ ] Static files directory created
+- [ ] Template directory created
+- [ ] Database migrations applied
+
+### Configuration
+- [ ] `JWT_SECRET_KEY` set to strong random value
+- [ ] `DATABASE_URL` configured correctly
+- [ ] `DEBUG` set appropriately (False for production)
+- [ ] `ALLOWED_HOSTS` configured
+- [ ] CORS settings configured
+
+### Security
+- [ ] Change default admin password
+- [ ] Enable HTTPS in production
+- [ ] Configure secure cookie settings
+- [ ] Set up rate limiting
+- [ ] Enable request logging
+
+### DNS (Production Only)
+- [ ] Wildcard subdomain configured: `*.platform.com`
+- [ ] Admin subdomain configured: `admin.platform.com`
+- [ ] SSL certificates installed
+
+## 🎯 Acceptance Criteria
+
+Slice 1 is complete when:
+
+1. **Admin Workflow Works**
+ - [ ] Admin can log in
+ - [ ] Admin can view dashboard
+ - [ ] Admin can create vendors
+ - [ ] Admin can manage vendors
+
+2. **Vendor Workflow Works**
+ - [ ] Vendor owner receives credentials
+ - [ ] Vendor owner can log in
+ - [ ] Vendor dashboard displays correctly
+ - [ ] Vendor context is properly isolated
+
+3. **Technical Requirements Met**
+ - [ ] All API endpoints implemented
+ - [ ] All frontend pages created
+ - [ ] Database schema complete
+ - [ ] Tests pass
+ - [ ] Documentation complete
+
+4. **Quality Standards Met**
+ - [ ] Code follows conventions
+ - [ ] No security vulnerabilities
+ - [ ] Performance acceptable
+ - [ ] Mobile responsive
+ - [ ] Browser compatible
+
+## 🔄 Known Issues / To Do
+
+### High Priority
+- [ ] Complete vendor login page
+- [ ] Complete vendor dashboard page
+- [ ] Test vendor context detection thoroughly
+- [ ] Add password reset functionality
+
+### Medium Priority
+- [ ] Add vendor list page for admin
+- [ ] Add vendor detail page for admin
+- [ ] Improve error messages
+- [ ] Add loading skeletons
+
+### Low Priority
+- [ ] Add dark mode support
+- [ ] Add keyboard shortcuts
+- [ ] Add export functionality
+- [ ] Add audit logging
+
+## 📚 Related Documentation
+
+- `00_slices_overview.md` - Overview of all slices
+- `../quick_start_guide.md` - Quick setup guide
+- `../css_structure_guide.txt` - CSS organization
+- `../12.project_readme_final.md` - Complete README
+
+## ➡️ Next Steps
+
+After completing Slice 1:
+
+1. **Test Thoroughly**: Run through entire testing checklist
+2. **Deploy to Staging**: Test in production-like environment
+3. **Demo to Stakeholders**: Show admin → vendor creation flow
+4. **Document Learnings**: Update this document with lessons learned
+5. **Move to Slice 2**: Begin marketplace import implementation
+
+## 💡 Tips & Best Practices
+
+### Working with Alpine.js
+- Keep components focused and small
+- Use `x-data` for reactive state
+- Use `x-init` for loading data
+- Prefer `x-show` over `x-if` for toggles
+- Use `@click`, `@submit` for event handling
+
+### Working with Jinja2
+- Pass initial data from backend to avoid extra API calls
+- Use template inheritance (extends, blocks)
+- Keep logic in backend, not templates
+- Use filters for formatting
+
+### API Integration
+- Always handle loading states
+- Always handle errors gracefully
+- Use the shared `apiClient` utility
+- Store tokens in localStorage
+- Clear tokens on logout
+
+---
+
+**Slice 1 Status**: 🔄 In Progress
+**Next Milestone**: Complete vendor login and dashboard pages
+**Estimated Completion**: End of Week 1
\ No newline at end of file
diff --git a/docs/project-roadmap/slice2_doc.md b/docs/project-roadmap/slice2_doc.md
new file mode 100644
index 00000000..7ca75b02
--- /dev/null
+++ b/docs/project-roadmap/slice2_doc.md
@@ -0,0 +1,808 @@
+# Slice 2: Marketplace Product Import
+## Vendor Imports Products from Letzshop
+
+**Status**: 📋 NOT STARTED
+**Timeline**: Week 2 (5 days)
+**Prerequisites**: Slice 1 complete
+
+## 🎯 Slice Objectives
+
+Enable vendors to import product catalogs from Letzshop marketplace via CSV files.
+
+### User Stories
+- As a Vendor Owner, I can configure my Letzshop CSV URL
+- As a Vendor Owner, I can trigger product imports from Letzshop
+- As a Vendor Owner, I can view import job status and history
+- The system processes CSV data in the background
+- As a Vendor Owner, I can see real-time import progress
+
+### Success Criteria
+- [ ] Vendor can configure Letzshop CSV URL (FR, EN, DE)
+- [ ] Vendor can trigger import jobs manually
+- [ ] System downloads and processes CSV files
+- [ ] Import status updates in real-time (Alpine.js)
+- [ ] Import history is properly tracked
+- [ ] Error handling for failed imports
+- [ ] Products stored in staging area (MarketplaceProduct table)
+- [ ] Large CSV files process without timeout
+
+## 📋 Backend Implementation
+
+### Database Models
+
+#### MarketplaceProduct Model (`models/database/marketplace_product.py`)
+
+```python
+class MarketplaceProduct(Base, TimestampMixin):
+ """
+ Staging table for imported marketplace products
+ Products stay here until vendor publishes them to catalog
+ """
+ __tablename__ = "marketplace_products"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
+ import_job_id = Column(Integer, ForeignKey("marketplace_import_jobs.id"))
+
+ # External identifiers
+ external_sku = Column(String, nullable=False, index=True)
+ marketplace = Column(String, default="letzshop") # Future: other marketplaces
+
+ # Product information (from CSV)
+ title = Column(String, nullable=False)
+ description = Column(Text)
+ price = Column(Numeric(10, 2))
+ currency = Column(String(3), default="EUR")
+
+ # Categories and attributes
+ category = Column(String)
+ brand = Column(String)
+ attributes = Column(JSON, default=dict) # Store all CSV columns
+
+ # Images
+ image_urls = Column(JSON, default=list) # List of image URLs
+
+ # Inventory
+ stock_quantity = Column(Integer)
+ is_in_stock = Column(Boolean, default=True)
+
+ # Status
+ is_selected = Column(Boolean, default=False) # Ready to publish?
+ is_published = Column(Boolean, default=False) # Already in catalog?
+ published_product_id = Column(Integer, ForeignKey("products.id"), nullable=True)
+
+ # Metadata
+ language = Column(String(2)) # 'fr', 'en', 'de'
+ raw_data = Column(JSON) # Store complete CSV row
+
+ # Relationships
+ vendor = relationship("Vendor", back_populates="marketplace_products")
+ import_job = relationship("MarketplaceImportJob", back_populates="products")
+ published_product = relationship("Product", back_populates="marketplace_source")
+
+ # Indexes
+ __table_args__ = (
+ Index('ix_marketplace_vendor_sku', 'vendor_id', 'external_sku'),
+ Index('ix_marketplace_selected', 'vendor_id', 'is_selected'),
+ )
+```
+
+#### MarketplaceImportJob Model (`models/database/marketplace.py`)
+
+```python
+class MarketplaceImportJob(Base, TimestampMixin):
+ """Track CSV import jobs"""
+ __tablename__ = "marketplace_import_jobs"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
+
+ # Job details
+ marketplace = Column(String, default="letzshop")
+ csv_url = Column(String, nullable=False)
+ language = Column(String(2)) # 'fr', 'en', 'de'
+
+ # Status tracking
+ status = Column(
+ String,
+ default="pending"
+ ) # pending, processing, completed, failed
+
+ # Progress
+ total_rows = Column(Integer, default=0)
+ processed_rows = Column(Integer, default=0)
+ imported_count = Column(Integer, default=0)
+ updated_count = Column(Integer, default=0)
+ error_count = Column(Integer, default=0)
+
+ # Timing
+ started_at = Column(DateTime, nullable=True)
+ completed_at = Column(DateTime, nullable=True)
+
+ # Error handling
+ error_message = Column(Text, nullable=True)
+ error_details = Column(JSON, nullable=True)
+
+ # Relationships
+ vendor = relationship("Vendor", back_populates="import_jobs")
+ products = relationship("MarketplaceProduct", back_populates="import_job")
+```
+
+### Pydantic Schemas
+
+#### Import Schemas (`models/schema/marketplace.py`)
+
+```python
+from pydantic import BaseModel, HttpUrl
+from typing import Optional, List
+from datetime import datetime
+
+class MarketplaceImportCreate(BaseModel):
+ """Create new import job"""
+ csv_url: HttpUrl
+ language: str = Field(..., regex="^(fr|en|de)$")
+ marketplace: str = "letzshop"
+
+class MarketplaceImportJobResponse(BaseModel):
+ """Import job details"""
+ id: int
+ vendor_id: int
+ marketplace: str
+ csv_url: str
+ language: str
+ status: str
+ total_rows: int
+ processed_rows: int
+ imported_count: int
+ updated_count: int
+ error_count: int
+ started_at: Optional[datetime]
+ completed_at: Optional[datetime]
+ error_message: Optional[str]
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+class MarketplaceProductResponse(BaseModel):
+ """Marketplace product in staging"""
+ id: int
+ vendor_id: int
+ external_sku: str
+ title: str
+ description: Optional[str]
+ price: float
+ currency: str
+ category: Optional[str]
+ brand: Optional[str]
+ stock_quantity: Optional[int]
+ is_in_stock: bool
+ is_selected: bool
+ is_published: bool
+ image_urls: List[str]
+ language: str
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+```
+
+### Service Layer
+
+#### Marketplace Service (`app/services/marketplace_service.py`)
+
+```python
+from typing import List, Dict, Any
+import csv
+import requests
+from io import StringIO
+from sqlalchemy.orm import Session
+from models.database.marketplace_product import MarketplaceProduct
+from models.database.marketplace import MarketplaceImportJob
+
+class MarketplaceService:
+ """Handle marketplace product imports"""
+
+ async def create_import_job(
+ self,
+ vendor_id: int,
+ csv_url: str,
+ language: str,
+ db: Session
+ ) -> MarketplaceImportJob:
+ """Create new import job and start processing"""
+
+ # Create job record
+ job = MarketplaceImportJob(
+ vendor_id=vendor_id,
+ csv_url=csv_url,
+ language=language,
+ marketplace="letzshop",
+ status="pending"
+ )
+ db.add(job)
+ db.commit()
+ db.refresh(job)
+
+ # Trigger background processing
+ from tasks.marketplace_import import process_csv_import
+ process_csv_import.delay(job.id)
+
+ return job
+
+ def process_csv_import(self, job_id: int, db: Session):
+ """
+ Process CSV import (called by Celery task)
+ This is a long-running operation
+ """
+ job = db.query(MarketplaceImportJob).get(job_id)
+ if not job:
+ return
+
+ try:
+ # Update status
+ job.status = "processing"
+ job.started_at = datetime.utcnow()
+ db.commit()
+
+ # Download CSV
+ response = requests.get(job.csv_url, timeout=30)
+ response.raise_for_status()
+
+ # Parse CSV
+ csv_content = StringIO(response.text)
+ reader = csv.DictReader(csv_content)
+
+ # Count total rows
+ rows = list(reader)
+ job.total_rows = len(rows)
+ db.commit()
+
+ # Process each row
+ for idx, row in enumerate(rows):
+ try:
+ self._process_csv_row(job, row, db)
+ job.processed_rows = idx + 1
+
+ # Commit every 100 rows
+ if idx % 100 == 0:
+ db.commit()
+
+ except Exception as e:
+ job.error_count += 1
+ # Log error but continue
+
+ # Final commit
+ job.status = "completed"
+ job.completed_at = datetime.utcnow()
+ db.commit()
+
+ except Exception as e:
+ job.status = "failed"
+ job.error_message = str(e)
+ job.completed_at = datetime.utcnow()
+ db.commit()
+
+ def _process_csv_row(
+ self,
+ job: MarketplaceImportJob,
+ row: Dict[str, Any],
+ db: Session
+ ):
+ """Process single CSV row"""
+
+ # Extract fields from CSV
+ external_sku = row.get('SKU') or row.get('sku')
+ if not external_sku:
+ raise ValueError("Missing SKU in CSV row")
+
+ # Check if product already exists
+ existing = db.query(MarketplaceProduct).filter(
+ MarketplaceProduct.vendor_id == job.vendor_id,
+ MarketplaceProduct.external_sku == external_sku
+ ).first()
+
+ # Parse image URLs
+ image_urls = []
+ for i in range(1, 6): # Support up to 5 images
+ img_url = row.get(f'Image{i}') or row.get(f'image_{i}')
+ if img_url:
+ image_urls.append(img_url)
+
+ if existing:
+ # Update existing product
+ existing.title = row.get('Title') or row.get('title')
+ existing.description = row.get('Description')
+ existing.price = float(row.get('Price', 0))
+ existing.stock_quantity = int(row.get('Stock', 0))
+ existing.is_in_stock = existing.stock_quantity > 0
+ existing.category = row.get('Category')
+ existing.brand = row.get('Brand')
+ existing.image_urls = image_urls
+ existing.raw_data = row
+ existing.import_job_id = job.id
+
+ job.updated_count += 1
+ else:
+ # Create new product
+ product = MarketplaceProduct(
+ vendor_id=job.vendor_id,
+ import_job_id=job.id,
+ external_sku=external_sku,
+ marketplace="letzshop",
+ title=row.get('Title') or row.get('title'),
+ description=row.get('Description'),
+ price=float(row.get('Price', 0)),
+ currency="EUR",
+ category=row.get('Category'),
+ brand=row.get('Brand'),
+ stock_quantity=int(row.get('Stock', 0)),
+ is_in_stock=int(row.get('Stock', 0)) > 0,
+ image_urls=image_urls,
+ language=job.language,
+ raw_data=row,
+ is_selected=False,
+ is_published=False
+ )
+ db.add(product)
+ job.imported_count += 1
+
+ def get_import_jobs(
+ self,
+ vendor_id: int,
+ db: Session,
+ skip: int = 0,
+ limit: int = 20
+ ) -> List[MarketplaceImportJob]:
+ """Get import job history for vendor"""
+ return db.query(MarketplaceImportJob).filter(
+ MarketplaceImportJob.vendor_id == vendor_id
+ ).order_by(
+ MarketplaceImportJob.created_at.desc()
+ ).offset(skip).limit(limit).all()
+
+ def get_marketplace_products(
+ self,
+ vendor_id: int,
+ db: Session,
+ import_job_id: Optional[int] = None,
+ is_selected: Optional[bool] = None,
+ skip: int = 0,
+ limit: int = 100
+ ) -> List[MarketplaceProduct]:
+ """Get marketplace products in staging"""
+ query = db.query(MarketplaceProduct).filter(
+ MarketplaceProduct.vendor_id == vendor_id,
+ MarketplaceProduct.is_published == False # Only unpublished
+ )
+
+ if import_job_id:
+ query = query.filter(MarketplaceProduct.import_job_id == import_job_id)
+
+ if is_selected is not None:
+ query = query.filter(MarketplaceProduct.is_selected == is_selected)
+
+ return query.order_by(
+ MarketplaceProduct.created_at.desc()
+ ).offset(skip).limit(limit).all()
+```
+
+### API Endpoints
+
+#### Marketplace Endpoints (`app/api/v1/vendor/marketplace.py`)
+
+```python
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+from typing import List, Optional
+
+router = APIRouter()
+
+@router.post("/import", response_model=MarketplaceImportJobResponse)
+async def trigger_import(
+ import_data: MarketplaceImportCreate,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Trigger CSV import from marketplace"""
+ service = MarketplaceService()
+ job = await service.create_import_job(
+ vendor_id=vendor.id,
+ csv_url=str(import_data.csv_url),
+ language=import_data.language,
+ db=db
+ )
+ return job
+
+@router.get("/jobs", response_model=List[MarketplaceImportJobResponse])
+async def get_import_jobs(
+ skip: int = 0,
+ limit: int = 20,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Get import job history"""
+ service = MarketplaceService()
+ jobs = service.get_import_jobs(vendor.id, db, skip, limit)
+ return jobs
+
+@router.get("/jobs/{job_id}", response_model=MarketplaceImportJobResponse)
+async def get_import_job(
+ job_id: int,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Get specific import job status"""
+ job = db.query(MarketplaceImportJob).filter(
+ MarketplaceImportJob.id == job_id,
+ MarketplaceImportJob.vendor_id == vendor.id
+ ).first()
+
+ if not job:
+ raise HTTPException(status_code=404, detail="Import job not found")
+
+ return job
+
+@router.get("/products", response_model=List[MarketplaceProductResponse])
+async def get_marketplace_products(
+ import_job_id: Optional[int] = None,
+ is_selected: Optional[bool] = None,
+ skip: int = 0,
+ limit: int = 100,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Get products in marketplace staging area"""
+ service = MarketplaceService()
+ products = service.get_marketplace_products(
+ vendor.id, db, import_job_id, is_selected, skip, limit
+ )
+ return products
+```
+
+### Background Tasks
+
+#### Celery Task (`tasks/marketplace_import.py`)
+
+```python
+from celery import shared_task
+from app.core.database import SessionLocal
+from app.services.marketplace_service import MarketplaceService
+
+@shared_task(bind=True, max_retries=3)
+def process_csv_import(self, job_id: int):
+ """
+ Process CSV import in background
+ This can take several minutes for large files
+ """
+ db = SessionLocal()
+ try:
+ service = MarketplaceService()
+ service.process_csv_import(job_id, db)
+ except Exception as e:
+ # Retry on failure
+ raise self.retry(exc=e, countdown=60)
+ finally:
+ db.close()
+```
+
+## 🎨 Frontend Implementation
+
+### Templates
+
+#### Import Dashboard (`templates/vendor/marketplace/imports.html`)
+
+```html
+{% extends "vendor/base_vendor.html" %}
+
+{% block title %}Product Import{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
Letzshop CSV URLs
+
+
+ Configure these URLs in vendor settings
+
+
+
+
+
+
Import History
+
+
+ No imports yet. Start your first import!
+
+
+
+
+
+
+ | ID |
+ Language |
+ Status |
+ Progress |
+ Results |
+ Started |
+ Actions |
+
+
+
+
+
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+ ✓ imported
+ ↻ updated
+
+ ✗ errors
+
+
+
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+```
+
+## ✅ Testing Checklist
+
+### Backend Tests
+- [ ] CSV download works with valid URL
+- [ ] CSV parsing handles various formats
+- [ ] Products created in staging table
+- [ ] Duplicate SKUs are updated, not duplicated
+- [ ] Import job status updates correctly
+- [ ] Progress tracking is accurate
+- [ ] Error handling works for invalid CSV
+- [ ] Large CSV files (10,000+ rows) process successfully
+- [ ] Celery tasks execute correctly
+
+### Frontend Tests
+- [ ] Import page loads correctly
+- [ ] New import modal works
+- [ ] Import jobs display in table
+- [ ] Real-time progress updates (polling)
+- [ ] Completed imports show results
+- [ ] Can view products from import
+- [ ] Error states display correctly
+- [ ] Loading states work correctly
+
+### Integration Tests
+- [ ] Complete import workflow works end-to-end
+- [ ] Vendor isolation maintained
+- [ ] API endpoints require authentication
+- [ ] Vendor can only see their own imports
+
+## 🚀 Deployment Checklist
+
+- [ ] Celery worker running
+- [ ] Redis/RabbitMQ configured for Celery
+- [ ] Database migrations applied
+- [ ] CSV download timeout configured
+- [ ] Error logging configured
+- [ ] Background task monitoring set up
+
+## ➡️ Next Steps
+
+After completing Slice 2, move to **Slice 3: Product Catalog Management** to enable vendors to publish imported products to their catalog.
+
+---
+
+**Slice 2 Status**: 📋 Not Started
+**Dependencies**: Slice 1 must be complete
+**Estimated Duration**: 5 days
\ No newline at end of file
diff --git a/docs/project-roadmap/slice3_doc.md b/docs/project-roadmap/slice3_doc.md
new file mode 100644
index 00000000..c4775b09
--- /dev/null
+++ b/docs/project-roadmap/slice3_doc.md
@@ -0,0 +1,624 @@
+# Slice 3: Product Catalog Management
+## Vendor Selects and Publishes Products
+
+**Status**: 📋 NOT STARTED
+**Timeline**: Week 3 (5 days)
+**Prerequisites**: Slice 1 & 2 complete
+
+## 🎯 Slice Objectives
+
+Enable vendors to browse imported products, select which to publish, customize them, and manage their product catalog.
+
+### User Stories
+- As a Vendor Owner, I can browse imported products from staging
+- As a Vendor Owner, I can select which products to publish to my catalog
+- As a Vendor Owner, I can customize product information (pricing, descriptions)
+- As a Vendor Owner, I can manage my published product catalog
+- As a Vendor Owner, I can manually add products (not from marketplace)
+- As a Vendor Owner, I can manage inventory for catalog products
+
+### Success Criteria
+- [ ] Vendor can browse all imported products in staging
+- [ ] Vendor can filter/search staging products
+- [ ] Vendor can select products for publishing
+- [ ] Vendor can customize product details before/after publishing
+- [ ] Published products appear in vendor catalog
+- [ ] Vendor can manually create products
+- [ ] Vendor can update product inventory
+- [ ] Vendor can activate/deactivate products
+- [ ] Product operations are properly isolated by vendor
+
+## 📋 Backend Implementation
+
+### Database Models
+
+#### Product Model (`models/database/product.py`)
+
+```python
+class Product(Base, TimestampMixin):
+ """
+ Vendor's published product catalog
+ These are customer-facing products
+ """
+ __tablename__ = "products"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
+
+ # Basic information
+ sku = Column(String, nullable=False, index=True)
+ title = Column(String, nullable=False)
+ description = Column(Text)
+ short_description = Column(String(500))
+
+ # Pricing
+ price = Column(Numeric(10, 2), nullable=False)
+ compare_at_price = Column(Numeric(10, 2)) # Original price for discounts
+ cost_per_item = Column(Numeric(10, 2)) # For profit tracking
+ currency = Column(String(3), default="EUR")
+
+ # Categorization
+ category = Column(String)
+ subcategory = Column(String)
+ brand = Column(String)
+ tags = Column(JSON, default=list)
+
+ # SEO
+ slug = Column(String, unique=True, index=True)
+ meta_title = Column(String)
+ meta_description = Column(String)
+
+ # Images
+ featured_image = Column(String) # Main product image
+ image_urls = Column(JSON, default=list) # Additional images
+
+ # Status
+ is_active = Column(Boolean, default=True)
+ is_featured = Column(Boolean, default=False)
+ is_on_sale = Column(Boolean, default=False)
+
+ # Inventory (simple - detailed in Inventory model)
+ track_inventory = Column(Boolean, default=True)
+ stock_quantity = Column(Integer, default=0)
+ low_stock_threshold = Column(Integer, default=10)
+
+ # Marketplace source (if imported)
+ marketplace_product_id = Column(
+ Integer,
+ ForeignKey("marketplace_products.id"),
+ nullable=True
+ )
+ external_sku = Column(String, nullable=True) # Original marketplace SKU
+
+ # Additional data
+ attributes = Column(JSON, default=dict) # Custom attributes
+ weight = Column(Numeric(10, 2)) # For shipping
+ dimensions = Column(JSON) # {length, width, height}
+
+ # Relationships
+ vendor = relationship("Vendor", back_populates="products")
+ marketplace_source = relationship(
+ "MarketplaceProduct",
+ back_populates="published_product"
+ )
+ inventory_records = relationship("Inventory", back_populates="product")
+ order_items = relationship("OrderItem", back_populates="product")
+
+ # Indexes
+ __table_args__ = (
+ Index('ix_product_vendor_sku', 'vendor_id', 'sku'),
+ Index('ix_product_active', 'vendor_id', 'is_active'),
+ Index('ix_product_featured', 'vendor_id', 'is_featured'),
+ )
+```
+
+#### Inventory Model (`models/database/inventory.py`)
+
+```python
+class Inventory(Base, TimestampMixin):
+ """Track product inventory by location"""
+ __tablename__ = "inventory"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
+ product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
+
+ # Location
+ location_name = Column(String, default="Default") # Warehouse name
+
+ # Quantities
+ available_quantity = Column(Integer, default=0)
+ reserved_quantity = Column(Integer, default=0) # Pending orders
+
+ # Relationships
+ vendor = relationship("Vendor")
+ product = relationship("Product", back_populates="inventory_records")
+ movements = relationship("InventoryMovement", back_populates="inventory")
+
+class InventoryMovement(Base, TimestampMixin):
+ """Track inventory changes"""
+ __tablename__ = "inventory_movements"
+
+ id = Column(Integer, primary_key=True, index=True)
+ inventory_id = Column(Integer, ForeignKey("inventory.id"), nullable=False)
+
+ # Movement details
+ movement_type = Column(String) # 'received', 'sold', 'adjusted', 'returned'
+ quantity_change = Column(Integer) # Positive or negative
+
+ # Context
+ reference_type = Column(String, nullable=True) # 'order', 'import', 'manual'
+ reference_id = Column(Integer, nullable=True)
+ notes = Column(Text)
+
+ # Relationships
+ inventory = relationship("Inventory", back_populates="movements")
+```
+
+### Pydantic Schemas
+
+#### Product Schemas (`models/schema/product.py`)
+
+```python
+class ProductCreate(BaseModel):
+ """Create product from scratch"""
+ sku: str
+ title: str
+ description: Optional[str] = None
+ price: float = Field(..., gt=0)
+ compare_at_price: Optional[float] = None
+ cost_per_item: Optional[float] = None
+ category: Optional[str] = None
+ brand: Optional[str] = None
+ tags: List[str] = []
+ image_urls: List[str] = []
+ track_inventory: bool = True
+ stock_quantity: int = 0
+ is_active: bool = True
+
+class ProductPublishFromMarketplace(BaseModel):
+ """Publish product from marketplace staging"""
+ marketplace_product_id: int
+ custom_title: Optional[str] = None
+ custom_description: Optional[str] = None
+ custom_price: Optional[float] = None
+ custom_sku: Optional[str] = None
+ stock_quantity: int = 0
+ is_active: bool = True
+
+class ProductUpdate(BaseModel):
+ """Update existing product"""
+ title: Optional[str] = None
+ description: Optional[str] = None
+ short_description: Optional[str] = None
+ price: Optional[float] = None
+ compare_at_price: Optional[float] = None
+ category: Optional[str] = None
+ brand: Optional[str] = None
+ tags: Optional[List[str]] = None
+ image_urls: Optional[List[str]] = None
+ is_active: Optional[bool] = None
+ is_featured: Optional[bool] = None
+ stock_quantity: Optional[int] = None
+
+class ProductResponse(BaseModel):
+ """Product details"""
+ id: int
+ vendor_id: int
+ sku: str
+ title: str
+ description: Optional[str]
+ price: float
+ compare_at_price: Optional[float]
+ category: Optional[str]
+ brand: Optional[str]
+ tags: List[str]
+ featured_image: Optional[str]
+ image_urls: List[str]
+ is_active: bool
+ is_featured: bool
+ stock_quantity: int
+ marketplace_product_id: Optional[int]
+ created_at: datetime
+ updated_at: datetime
+
+ class Config:
+ from_attributes = True
+```
+
+### Service Layer
+
+#### Product Service (`app/services/product_service.py`)
+
+```python
+class ProductService:
+ """Handle product catalog operations"""
+
+ async def publish_from_marketplace(
+ self,
+ vendor_id: int,
+ publish_data: ProductPublishFromMarketplace,
+ db: Session
+ ) -> Product:
+ """Publish marketplace product to catalog"""
+
+ # Get marketplace product
+ mp_product = db.query(MarketplaceProduct).filter(
+ MarketplaceProduct.id == publish_data.marketplace_product_id,
+ MarketplaceProduct.vendor_id == vendor_id,
+ MarketplaceProduct.is_published == False
+ ).first()
+
+ if not mp_product:
+ raise ProductNotFoundError("Marketplace product not found")
+
+ # Check if SKU already exists
+ sku = publish_data.custom_sku or mp_product.external_sku
+ existing = db.query(Product).filter(
+ Product.vendor_id == vendor_id,
+ Product.sku == sku
+ ).first()
+
+ if existing:
+ raise ProductAlreadyExistsError(f"Product with SKU {sku} already exists")
+
+ # Create product
+ product = Product(
+ vendor_id=vendor_id,
+ sku=sku,
+ title=publish_data.custom_title or mp_product.title,
+ description=publish_data.custom_description or mp_product.description,
+ price=publish_data.custom_price or mp_product.price,
+ currency=mp_product.currency,
+ category=mp_product.category,
+ brand=mp_product.brand,
+ image_urls=mp_product.image_urls,
+ featured_image=mp_product.image_urls[0] if mp_product.image_urls else None,
+ marketplace_product_id=mp_product.id,
+ external_sku=mp_product.external_sku,
+ stock_quantity=publish_data.stock_quantity,
+ is_active=publish_data.is_active,
+ slug=self._generate_slug(publish_data.custom_title or mp_product.title)
+ )
+
+ db.add(product)
+
+ # Mark marketplace product as published
+ mp_product.is_published = True
+ mp_product.published_product_id = product.id
+
+ # Create initial inventory record
+ inventory = Inventory(
+ vendor_id=vendor_id,
+ product_id=product.id,
+ location_name="Default",
+ available_quantity=publish_data.stock_quantity,
+ reserved_quantity=0
+ )
+ db.add(inventory)
+
+ # Record inventory movement
+ if publish_data.stock_quantity > 0:
+ movement = InventoryMovement(
+ inventory_id=inventory.id,
+ movement_type="received",
+ quantity_change=publish_data.stock_quantity,
+ reference_type="import",
+ notes="Initial stock from marketplace import"
+ )
+ db.add(movement)
+
+ db.commit()
+ db.refresh(product)
+
+ return product
+
+ async def create_product(
+ self,
+ vendor_id: int,
+ product_data: ProductCreate,
+ db: Session
+ ) -> Product:
+ """Create product manually"""
+
+ # Check SKU uniqueness
+ existing = db.query(Product).filter(
+ Product.vendor_id == vendor_id,
+ Product.sku == product_data.sku
+ ).first()
+
+ if existing:
+ raise ProductAlreadyExistsError(f"SKU {product_data.sku} already exists")
+
+ product = Product(
+ vendor_id=vendor_id,
+ **product_data.dict(),
+ slug=self._generate_slug(product_data.title),
+ featured_image=product_data.image_urls[0] if product_data.image_urls else None
+ )
+
+ db.add(product)
+
+ # Create inventory
+ if product_data.track_inventory:
+ inventory = Inventory(
+ vendor_id=vendor_id,
+ product_id=product.id,
+ available_quantity=product_data.stock_quantity
+ )
+ db.add(inventory)
+
+ db.commit()
+ db.refresh(product)
+
+ return product
+
+ def get_products(
+ self,
+ vendor_id: int,
+ db: Session,
+ is_active: Optional[bool] = None,
+ category: Optional[str] = None,
+ search: Optional[str] = None,
+ skip: int = 0,
+ limit: int = 100
+ ) -> List[Product]:
+ """Get vendor's product catalog"""
+
+ query = db.query(Product).filter(Product.vendor_id == vendor_id)
+
+ if is_active is not None:
+ query = query.filter(Product.is_active == is_active)
+
+ if category:
+ query = query.filter(Product.category == category)
+
+ if search:
+ query = query.filter(
+ or_(
+ Product.title.ilike(f"%{search}%"),
+ Product.sku.ilike(f"%{search}%"),
+ Product.brand.ilike(f"%{search}%")
+ )
+ )
+
+ return query.order_by(
+ Product.created_at.desc()
+ ).offset(skip).limit(limit).all()
+
+ async def update_product(
+ self,
+ vendor_id: int,
+ product_id: int,
+ update_data: ProductUpdate,
+ db: Session
+ ) -> Product:
+ """Update product details"""
+
+ product = db.query(Product).filter(
+ Product.id == product_id,
+ Product.vendor_id == vendor_id
+ ).first()
+
+ if not product:
+ raise ProductNotFoundError()
+
+ # Update fields
+ update_dict = update_data.dict(exclude_unset=True)
+ for field, value in update_dict.items():
+ setattr(product, field, value)
+
+ # Update stock if changed
+ if 'stock_quantity' in update_dict:
+ self._update_inventory(product, update_dict['stock_quantity'], db)
+
+ db.commit()
+ db.refresh(product)
+
+ return product
+
+ def _generate_slug(self, title: str) -> str:
+ """Generate URL-friendly slug"""
+ import re
+ slug = title.lower()
+ slug = re.sub(r'[^a-z0-9]+', '-', slug)
+ slug = slug.strip('-')
+ return slug
+
+ def _update_inventory(
+ self,
+ product: Product,
+ new_quantity: int,
+ db: Session
+ ):
+ """Update product inventory"""
+ inventory = db.query(Inventory).filter(
+ Inventory.product_id == product.id
+ ).first()
+
+ if inventory:
+ quantity_change = new_quantity - inventory.available_quantity
+ inventory.available_quantity = new_quantity
+
+ # Record movement
+ movement = InventoryMovement(
+ inventory_id=inventory.id,
+ movement_type="adjusted",
+ quantity_change=quantity_change,
+ reference_type="manual",
+ notes="Manual adjustment"
+ )
+ db.add(movement)
+```
+
+### API Endpoints
+
+#### Product Endpoints (`app/api/v1/vendor/products.py`)
+
+```python
+@router.get("", response_model=List[ProductResponse])
+async def get_products(
+ is_active: Optional[bool] = None,
+ category: Optional[str] = None,
+ search: Optional[str] = None,
+ skip: int = 0,
+ limit: int = 100,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Get vendor's product catalog"""
+ service = ProductService()
+ products = service.get_products(
+ vendor.id, db, is_active, category, search, skip, limit
+ )
+ return products
+
+@router.post("", response_model=ProductResponse)
+async def create_product(
+ product_data: ProductCreate,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Create product manually"""
+ service = ProductService()
+ product = await service.create_product(vendor.id, product_data, db)
+ return product
+
+@router.post("/from-marketplace", response_model=ProductResponse)
+async def publish_from_marketplace(
+ publish_data: ProductPublishFromMarketplace,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Publish marketplace product to catalog"""
+ service = ProductService()
+ product = await service.publish_from_marketplace(
+ vendor.id, publish_data, db
+ )
+ return product
+
+@router.get("/{product_id}", response_model=ProductResponse)
+async def get_product(
+ product_id: int,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Get product details"""
+ product = db.query(Product).filter(
+ Product.id == product_id,
+ Product.vendor_id == vendor.id
+ ).first()
+
+ if not product:
+ raise HTTPException(status_code=404, detail="Product not found")
+
+ return product
+
+@router.put("/{product_id}", response_model=ProductResponse)
+async def update_product(
+ product_id: int,
+ update_data: ProductUpdate,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Update product"""
+ service = ProductService()
+ product = await service.update_product(vendor.id, product_id, update_data, db)
+ return product
+
+@router.put("/{product_id}/toggle-active")
+async def toggle_product_active(
+ product_id: int,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Activate/deactivate product"""
+ product = db.query(Product).filter(
+ Product.id == product_id,
+ Product.vendor_id == vendor.id
+ ).first()
+
+ if not product:
+ raise HTTPException(status_code=404, detail="Product not found")
+
+ product.is_active = not product.is_active
+ db.commit()
+
+ return {"is_active": product.is_active}
+
+@router.delete("/{product_id}")
+async def delete_product(
+ product_id: int,
+ current_user: User = Depends(get_current_vendor_user),
+ vendor: Vendor = Depends(get_current_vendor),
+ db: Session = Depends(get_db)
+):
+ """Remove product from catalog"""
+ product = db.query(Product).filter(
+ Product.id == product_id,
+ Product.vendor_id == vendor.id
+ ).first()
+
+ if not product:
+ raise HTTPException(status_code=404, detail="Product not found")
+
+ # Mark as inactive instead of deleting
+ product.is_active = False
+ db.commit()
+
+ return {"success": True}
+```
+
+## 🎨 Frontend Implementation
+
+### Templates
+
+#### Browse Marketplace Products (`templates/vendor/marketplace/browse.html`)
+
+Uses Alpine.js for reactive filtering, selection, and bulk publishing.
+
+#### Product Catalog (`templates/vendor/products/list.html`)
+
+Product management interface with search, filters, and quick actions.
+
+#### Product Edit (`templates/vendor/products/edit.html`)
+
+Detailed product editing with image management and inventory tracking.
+
+## ✅ Testing Checklist
+
+### Backend Tests
+- [ ] Product publishing from marketplace works
+- [ ] Manual product creation works
+- [ ] Product updates work correctly
+- [ ] Inventory tracking is accurate
+- [ ] SKU uniqueness is enforced
+- [ ] Vendor isolation maintained
+- [ ] Product search/filtering works
+- [ ] Slug generation works correctly
+
+### Frontend Tests
+- [ ] Browse marketplace products
+- [ ] Select multiple products for publishing
+- [ ] Publish single product with customization
+- [ ] View product catalog
+- [ ] Edit product details
+- [ ] Toggle product active status
+- [ ] Delete/deactivate products
+- [ ] Search and filter products
+
+## ➡️ Next Steps
+
+After completing Slice 3, move to **Slice 4: Customer Shopping Experience** to build the public-facing shop.
+
+---
+
+**Slice 3 Status**: 📋 Not Started
+**Dependencies**: Slices 1 & 2 must be complete
+**Estimated Duration**: 5 days
\ No newline at end of file
diff --git a/docs/project-roadmap/slice4_doc.md b/docs/project-roadmap/slice4_doc.md
new file mode 100644
index 00000000..9161f9c7
--- /dev/null
+++ b/docs/project-roadmap/slice4_doc.md
@@ -0,0 +1,887 @@
+# Slice 4: Customer Shopping Experience
+## Customers Browse and Shop on Vendor Stores
+
+**Status**: 📋 NOT STARTED
+**Timeline**: Week 4 (5 days)
+**Prerequisites**: Slices 1, 2, & 3 complete
+
+## 🎯 Slice Objectives
+
+Build the public-facing customer shop where customers can browse products, register accounts, and add items to cart.
+
+### User Stories
+- As a Customer, I can browse products on a vendor's shop
+- As a Customer, I can view detailed product information
+- As a Customer, I can search for products
+- As a Customer, I can register for a vendor-specific account
+- As a Customer, I can log into my account
+- As a Customer, I can add products to my shopping cart
+- As a Customer, I can manage my cart (update quantities, remove items)
+- Cart persists across sessions
+
+### Success Criteria
+- [ ] Customers can browse products without authentication
+- [ ] Product catalog displays correctly with images and prices
+- [ ] Product detail pages show complete information
+- [ ] Search functionality works
+- [ ] Customers can register vendor-specific accounts
+- [ ] Customer login/logout works
+- [ ] Shopping cart is functional with Alpine.js reactivity
+- [ ] Cart persists (session-based before login, user-based after)
+- [ ] Customer data is properly isolated by vendor
+- [ ] Mobile responsive design
+
+## 📋 Backend Implementation
+
+### Database Models
+
+#### Customer Model (`models/database/customer.py`)
+
+```python
+class Customer(Base, TimestampMixin):
+ """
+ Vendor-scoped customer accounts
+ Each customer belongs to ONE vendor
+ """
+ __tablename__ = "customers"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
+
+ # Authentication
+ email = Column(String, nullable=False, index=True)
+ hashed_password = Column(String, nullable=False)
+
+ # Personal information
+ first_name = Column(String)
+ last_name = Column(String)
+ phone = Column(String)
+
+ # Customer metadata
+ customer_number = Column(String, unique=True, index=True) # Auto-generated
+
+ # Preferences
+ language = Column(String(2), default="en")
+ newsletter_subscribed = Column(Boolean, default=False)
+ marketing_emails = Column(Boolean, default=True)
+ preferences = Column(JSON, default=dict)
+
+ # Statistics
+ total_orders = Column(Integer, default=0)
+ total_spent = Column(Numeric(10, 2), default=0)
+
+ # Status
+ is_active = Column(Boolean, default=True)
+ email_verified = Column(Boolean, default=False)
+ last_login_at = Column(DateTime, nullable=True)
+
+ # Relationships
+ vendor = relationship("Vendor", back_populates="customers")
+ addresses = relationship("CustomerAddress", back_populates="customer", cascade="all, delete-orphan")
+ orders = relationship("Order", back_populates="customer")
+ cart = relationship("Cart", back_populates="customer", uselist=False)
+
+ # Indexes
+ __table_args__ = (
+ Index('ix_customer_vendor_email', 'vendor_id', 'email', unique=True),
+ )
+
+class CustomerAddress(Base, TimestampMixin):
+ """Customer shipping/billing addresses"""
+ __tablename__ = "customer_addresses"
+
+ id = Column(Integer, primary_key=True, index=True)
+ customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
+
+ # Address type
+ address_type = Column(String, default="shipping") # shipping, billing, both
+ is_default = Column(Boolean, default=False)
+
+ # Address details
+ first_name = Column(String)
+ last_name = Column(String)
+ company = Column(String)
+ address_line1 = Column(String, nullable=False)
+ address_line2 = Column(String)
+ city = Column(String, nullable=False)
+ state_province = Column(String)
+ postal_code = Column(String, nullable=False)
+ country = Column(String, nullable=False, default="LU")
+ phone = Column(String)
+
+ # Relationships
+ customer = relationship("Customer", back_populates="addresses")
+```
+
+#### Cart Model (`models/database/cart.py`)
+
+```python
+class Cart(Base, TimestampMixin):
+ """Shopping cart - session or customer-based"""
+ __tablename__ = "carts"
+
+ id = Column(Integer, primary_key=True, index=True)
+ vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
+
+ # Owner (one of these must be set)
+ customer_id = Column(Integer, ForeignKey("customers.id"), nullable=True)
+ session_id = Column(String, nullable=True, index=True) # For guest users
+
+ # Cart metadata
+ currency = Column(String(3), default="EUR")
+
+ # Relationships
+ vendor = relationship("Vendor")
+ customer = relationship("Customer", back_populates="cart")
+ items = relationship("CartItem", back_populates="cart", cascade="all, delete-orphan")
+
+ # Computed properties
+ @property
+ def total_items(self) -> int:
+ return sum(item.quantity for item in self.items)
+
+ @property
+ def subtotal(self) -> Decimal:
+ return sum(item.line_total for item in self.items)
+
+ __table_args__ = (
+ Index('ix_cart_vendor_session', 'vendor_id', 'session_id'),
+ Index('ix_cart_vendor_customer', 'vendor_id', 'customer_id'),
+ )
+
+class CartItem(Base, TimestampMixin):
+ """Individual items in cart"""
+ __tablename__ = "cart_items"
+
+ id = Column(Integer, primary_key=True, index=True)
+ cart_id = Column(Integer, ForeignKey("carts.id"), nullable=False)
+ product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
+
+ # Item details
+ quantity = Column(Integer, nullable=False, default=1)
+ unit_price = Column(Numeric(10, 2), nullable=False) # Snapshot at time of add
+
+ # Relationships
+ cart = relationship("Cart", back_populates="items")
+ product = relationship("Product")
+
+ @property
+ def line_total(self) -> Decimal:
+ return self.unit_price * self.quantity
+```
+
+### Pydantic Schemas
+
+#### Customer Schemas (`models/schema/customer.py`)
+
+```python
+class CustomerRegister(BaseModel):
+ """Customer registration"""
+ email: EmailStr
+ password: str = Field(..., min_length=8)
+ first_name: str
+ last_name: str
+ phone: Optional[str] = None
+ newsletter_subscribed: bool = False
+
+class CustomerLogin(BaseModel):
+ """Customer login"""
+ email: EmailStr
+ password: str
+
+class CustomerResponse(BaseModel):
+ """Customer details"""
+ id: int
+ vendor_id: int
+ email: str
+ first_name: str
+ last_name: str
+ phone: Optional[str]
+ customer_number: str
+ total_orders: int
+ total_spent: float
+ is_active: bool
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+class CustomerAddressCreate(BaseModel):
+ """Create address"""
+ address_type: str = "shipping"
+ is_default: bool = False
+ first_name: str
+ last_name: str
+ company: Optional[str] = None
+ address_line1: str
+ address_line2: Optional[str] = None
+ city: str
+ state_province: Optional[str] = None
+ postal_code: str
+ country: str = "LU"
+ phone: Optional[str] = None
+
+class CustomerAddressResponse(BaseModel):
+ """Address details"""
+ id: int
+ address_type: str
+ is_default: bool
+ first_name: str
+ last_name: str
+ company: Optional[str]
+ address_line1: str
+ address_line2: Optional[str]
+ city: str
+ state_province: Optional[str]
+ postal_code: str
+ country: str
+ phone: Optional[str]
+
+ class Config:
+ from_attributes = True
+```
+
+#### Cart Schemas (`models/schema/cart.py`)
+
+```python
+class CartItemAdd(BaseModel):
+ """Add item to cart"""
+ product_id: int
+ quantity: int = Field(..., gt=0)
+
+class CartItemUpdate(BaseModel):
+ """Update cart item"""
+ quantity: int = Field(..., gt=0)
+
+class CartItemResponse(BaseModel):
+ """Cart item details"""
+ id: int
+ product_id: int
+ product_title: str
+ product_image: Optional[str]
+ product_sku: str
+ quantity: int
+ unit_price: float
+ line_total: float
+
+ class Config:
+ from_attributes = True
+
+class CartResponse(BaseModel):
+ """Complete cart"""
+ id: int
+ vendor_id: int
+ total_items: int
+ subtotal: float
+ currency: str
+ items: List[CartItemResponse]
+
+ class Config:
+ from_attributes = True
+```
+
+### Service Layer
+
+#### Customer Service (`app/services/customer_service.py`)
+
+```python
+class CustomerService:
+ """Handle customer operations"""
+
+ def __init__(self):
+ self.auth_manager = AuthManager()
+
+ async def register_customer(
+ self,
+ vendor_id: int,
+ customer_data: CustomerRegister,
+ db: Session
+ ) -> Customer:
+ """Register new customer for vendor"""
+
+ # Check if email already exists for this vendor
+ existing = db.query(Customer).filter(
+ Customer.vendor_id == vendor_id,
+ Customer.email == customer_data.email
+ ).first()
+
+ if existing:
+ raise CustomerAlreadyExistsError("Email already registered")
+
+ # Generate customer number
+ customer_number = self._generate_customer_number(vendor_id, db)
+
+ # Create customer
+ customer = Customer(
+ vendor_id=vendor_id,
+ email=customer_data.email,
+ hashed_password=self.auth_manager.hash_password(customer_data.password),
+ first_name=customer_data.first_name,
+ last_name=customer_data.last_name,
+ phone=customer_data.phone,
+ customer_number=customer_number,
+ newsletter_subscribed=customer_data.newsletter_subscribed,
+ is_active=True
+ )
+
+ db.add(customer)
+ db.commit()
+ db.refresh(customer)
+
+ return customer
+
+ async def authenticate_customer(
+ self,
+ vendor_id: int,
+ email: str,
+ password: str,
+ db: Session
+ ) -> Tuple[Customer, str]:
+ """Authenticate customer and return token"""
+
+ customer = db.query(Customer).filter(
+ Customer.vendor_id == vendor_id,
+ Customer.email == email
+ ).first()
+
+ if not customer:
+ raise InvalidCredentialsError()
+
+ if not customer.is_active:
+ raise CustomerInactiveError()
+
+ if not self.auth_manager.verify_password(password, customer.hashed_password):
+ raise InvalidCredentialsError()
+
+ # Update last login
+ customer.last_login_at = datetime.utcnow()
+ db.commit()
+
+ # Generate JWT token
+ token = self.auth_manager.create_access_token({
+ "sub": str(customer.id),
+ "email": customer.email,
+ "vendor_id": vendor_id,
+ "type": "customer"
+ })
+
+ return customer, token
+
+ def _generate_customer_number(self, vendor_id: int, db: Session) -> str:
+ """Generate unique customer number"""
+ # Format: VENDOR_CODE-YYYYMMDD-XXXX
+ from models.database.vendor import Vendor
+
+ vendor = db.query(Vendor).get(vendor_id)
+ date_str = datetime.utcnow().strftime("%Y%m%d")
+
+ # Count customers today
+ today_start = datetime.utcnow().replace(hour=0, minute=0, second=0)
+ count = db.query(Customer).filter(
+ Customer.vendor_id == vendor_id,
+ Customer.created_at >= today_start
+ ).count()
+
+ return f"{vendor.vendor_code}-{date_str}-{count+1:04d}"
+```
+
+#### Cart Service (`app/services/cart_service.py`)
+
+```python
+class CartService:
+ """Handle shopping cart operations"""
+
+ async def get_or_create_cart(
+ self,
+ vendor_id: int,
+ db: Session,
+ customer_id: Optional[int] = None,
+ session_id: Optional[str] = None
+ ) -> Cart:
+ """Get existing cart or create new one"""
+
+ if customer_id:
+ cart = db.query(Cart).filter(
+ Cart.vendor_id == vendor_id,
+ Cart.customer_id == customer_id
+ ).first()
+ else:
+ cart = db.query(Cart).filter(
+ Cart.vendor_id == vendor_id,
+ Cart.session_id == session_id
+ ).first()
+
+ if not cart:
+ cart = Cart(
+ vendor_id=vendor_id,
+ customer_id=customer_id,
+ session_id=session_id
+ )
+ db.add(cart)
+ db.commit()
+ db.refresh(cart)
+
+ return cart
+
+ async def add_to_cart(
+ self,
+ cart: Cart,
+ product_id: int,
+ quantity: int,
+ db: Session
+ ) -> CartItem:
+ """Add product to cart"""
+
+ # Verify product exists and is active
+ product = db.query(Product).filter(
+ Product.id == product_id,
+ Product.vendor_id == cart.vendor_id,
+ Product.is_active == True
+ ).first()
+
+ if not product:
+ raise ProductNotFoundError()
+
+ # Check if product already in cart
+ existing_item = db.query(CartItem).filter(
+ CartItem.cart_id == cart.id,
+ CartItem.product_id == product_id
+ ).first()
+
+ if existing_item:
+ # Update quantity
+ existing_item.quantity += quantity
+ db.commit()
+ db.refresh(existing_item)
+ return existing_item
+ else:
+ # Add new item
+ cart_item = CartItem(
+ cart_id=cart.id,
+ product_id=product_id,
+ quantity=quantity,
+ unit_price=product.price
+ )
+ db.add(cart_item)
+ db.commit()
+ db.refresh(cart_item)
+ return cart_item
+
+ async def update_cart_item(
+ self,
+ cart_item_id: int,
+ quantity: int,
+ cart: Cart,
+ db: Session
+ ) -> CartItem:
+ """Update cart item quantity"""
+
+ cart_item = db.query(CartItem).filter(
+ CartItem.id == cart_item_id,
+ CartItem.cart_id == cart.id
+ ).first()
+
+ if not cart_item:
+ raise CartItemNotFoundError()
+
+ cart_item.quantity = quantity
+ db.commit()
+ db.refresh(cart_item)
+
+ return cart_item
+
+ async def remove_from_cart(
+ self,
+ cart_item_id: int,
+ cart: Cart,
+ db: Session
+ ):
+ """Remove item from cart"""
+
+ cart_item = db.query(CartItem).filter(
+ CartItem.id == cart_item_id,
+ CartItem.cart_id == cart.id
+ ).first()
+
+ if not cart_item:
+ raise CartItemNotFoundError()
+
+ db.delete(cart_item)
+ db.commit()
+
+ async def clear_cart(self, cart: Cart, db: Session):
+ """Clear all items from cart"""
+
+ db.query(CartItem).filter(CartItem.cart_id == cart.id).delete()
+ db.commit()
+
+ async def merge_carts(
+ self,
+ session_cart_id: int,
+ customer_cart_id: int,
+ db: Session
+ ):
+ """Merge session cart into customer cart after login"""
+
+ session_cart = db.query(Cart).get(session_cart_id)
+ customer_cart = db.query(Cart).get(customer_cart_id)
+
+ if not session_cart or not customer_cart:
+ return
+
+ # Move items from session cart to customer cart
+ for item in session_cart.items:
+ # Check if product already in customer cart
+ existing = db.query(CartItem).filter(
+ CartItem.cart_id == customer_cart.id,
+ CartItem.product_id == item.product_id
+ ).first()
+
+ if existing:
+ existing.quantity += item.quantity
+ else:
+ item.cart_id = customer_cart.id
+
+ # Delete session cart
+ db.delete(session_cart)
+ db.commit()
+```
+
+### API Endpoints
+
+#### Public Product Endpoints (`app/api/v1/public/vendors/products.py`)
+
+```python
+@router.get("", response_model=List[ProductResponse])
+async def get_public_products(
+ vendor_id: int,
+ category: Optional[str] = None,
+ search: Optional[str] = None,
+ min_price: Optional[float] = None,
+ max_price: Optional[float] = None,
+ skip: int = 0,
+ limit: int = 50,
+ db: Session = Depends(get_db)
+):
+ """Get public product catalog (no auth required)"""
+
+ query = db.query(Product).filter(
+ Product.vendor_id == vendor_id,
+ Product.is_active == True
+ )
+
+ if category:
+ query = query.filter(Product.category == category)
+
+ if search:
+ query = query.filter(
+ or_(
+ Product.title.ilike(f"%{search}%"),
+ Product.description.ilike(f"%{search}%")
+ )
+ )
+
+ if min_price:
+ query = query.filter(Product.price >= min_price)
+
+ if max_price:
+ query = query.filter(Product.price <= max_price)
+
+ products = query.order_by(
+ Product.is_featured.desc(),
+ Product.created_at.desc()
+ ).offset(skip).limit(limit).all()
+
+ return products
+
+@router.get("/{product_id}", response_model=ProductResponse)
+async def get_public_product(
+ vendor_id: int,
+ product_id: int,
+ db: Session = Depends(get_db)
+):
+ """Get product details (no auth required)"""
+
+ product = db.query(Product).filter(
+ Product.id == product_id,
+ Product.vendor_id == vendor_id,
+ Product.is_active == True
+ ).first()
+
+ if not product:
+ raise HTTPException(status_code=404, detail="Product not found")
+
+ return product
+
+@router.get("/search")
+async def search_products(
+ vendor_id: int,
+ q: str,
+ db: Session = Depends(get_db)
+):
+ """Search products"""
+ # Implement search logic
+ pass
+```
+
+#### Customer Auth Endpoints (`app/api/v1/public/vendors/auth.py`)
+
+```python
+@router.post("/register", response_model=CustomerResponse)
+async def register_customer(
+ vendor_id: int,
+ customer_data: CustomerRegister,
+ db: Session = Depends(get_db)
+):
+ """Register new customer"""
+ service = CustomerService()
+ customer = await service.register_customer(vendor_id, customer_data, db)
+ return customer
+
+@router.post("/login")
+async def login_customer(
+ vendor_id: int,
+ credentials: CustomerLogin,
+ db: Session = Depends(get_db)
+):
+ """Customer login"""
+ service = CustomerService()
+ customer, token = await service.authenticate_customer(
+ vendor_id, credentials.email, credentials.password, db
+ )
+
+ return {
+ "access_token": token,
+ "token_type": "bearer",
+ "customer": CustomerResponse.from_orm(customer)
+ }
+```
+
+#### Cart Endpoints (`app/api/v1/public/vendors/cart.py`)
+
+```python
+@router.get("/{session_id}", response_model=CartResponse)
+async def get_cart(
+ vendor_id: int,
+ session_id: str,
+ current_customer: Optional[Customer] = Depends(get_current_customer_optional),
+ db: Session = Depends(get_db)
+):
+ """Get cart (session or customer)"""
+ service = CartService()
+ cart = await service.get_or_create_cart(
+ vendor_id,
+ db,
+ customer_id=current_customer.id if current_customer else None,
+ session_id=session_id if not current_customer else None
+ )
+ return cart
+
+@router.post("/{session_id}/items", response_model=CartItemResponse)
+async def add_to_cart(
+ vendor_id: int,
+ session_id: str,
+ item_data: CartItemAdd,
+ current_customer: Optional[Customer] = Depends(get_current_customer_optional),
+ db: Session = Depends(get_db)
+):
+ """Add item to cart"""
+ service = CartService()
+ cart = await service.get_or_create_cart(vendor_id, db, current_customer.id if current_customer else None, session_id)
+ item = await service.add_to_cart(cart, item_data.product_id, item_data.quantity, db)
+ return item
+```
+
+## 🎨 Frontend Implementation
+
+### Templates
+
+#### Shop Homepage (`templates/shop/home.html`)
+
+```html
+{% extends "shop/base_shop.html" %}
+
+{% block content %}
+
+
+
+
Welcome to {{ vendor.name }}
+
{{ vendor.description }}
+
+ Shop Now
+
+
+
+
+
+
+{% endblock %}
+```
+
+#### Product Detail (`templates/shop/product.html`)
+
+```html
+{% extends "shop/base_shop.html" %}
+
+{% block content %}
+
+
+
+
+
![]()
+
+
+
+
+
+
+ €
+
+ €
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+```
+
+#### Shopping Cart (`templates/shop/cart.html`)
+
+Full Alpine.js reactive cart with real-time totals and quantity updates.
+
+## ✅ Testing Checklist
+
+### Backend Tests
+- [ ] Customer registration works
+- [ ] Duplicate email prevention works
+- [ ] Customer login/authentication works
+- [ ] Customer number generation is unique
+- [ ] Public product browsing works without auth
+- [ ] Product search/filtering works
+- [ ] Cart creation works (session and customer)
+- [ ] Add to cart works
+- [ ] Update cart quantity works
+- [ ] Remove from cart works
+- [ ] Cart persists across sessions
+- [ ] Cart merges after login
+- [ ] Vendor isolation maintained
+
+### Frontend Tests
+- [ ] Shop homepage loads
+- [ ] Product listing displays
+- [ ] Product search works
+- [ ] Product detail page works
+- [ ] Customer registration form works
+- [ ] Customer login works
+- [ ] Add to cart works
+- [ ] Cart updates in real-time (Alpine.js)
+- [ ] Cart icon shows count
+- [ ] Mobile responsive
+
+## ➡️ Next Steps
+
+After completing Slice 4, move to **Slice 5: Order Processing** to complete the checkout flow and order management.
+
+---
+
+**Slice 4 Status**: 📋 Not Started
+**Dependencies**: Slices 1, 2, & 3 must be complete
+**Estimated Duration**: 5 days
\ No newline at end of file
diff --git a/docs/project-roadmap/slice5_doc.md b/docs/project-roadmap/slice5_doc.md
new file mode 100644
index 00000000..d437a678
--- /dev/null
+++ b/docs/project-roadmap/slice5_doc.md
@@ -0,0 +1,1628 @@
+ 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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No orders found
+
+
+
+
+
+
+ | Order # |
+ Customer |
+ Date |
+ Items |
+ Total |
+ Status |
+ Payment |
+ Actions |
+
+
+
+
+
+ |
+
+ |
+
+
+
+ |
+ |
+ |
+ € |
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+{% 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 %}
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
+{% endblock %}
+```
+
+### Alpine.js Component Pattern
+```javascript
+function componentName() {
+ return {
+ // State
+ data: window.initialData || [],
+ loading: false,
+ error: null,
+
+ // Lifecycle
+ init() {
+ this.loadData();
+ },
+
+ // Methods
+ async loadData() {
+ this.loading = true;
+ try {
+ const response = await apiClient.get('/api/endpoint');
+ this.data = response;
+ } catch (error) {
+ this.error = error.message;
+ } finally {
+ this.loading = false;
+ }
+ }
+ }
+}
+```
+
+## 🔑 Key Principles
+
+### 1. Complete Features
+Each slice delivers a complete, working feature from database to UI.
+
+### 2. Vendor Isolation
+All slices maintain strict vendor data isolation and context detection.
+
+### 3. Progressive Enhancement
+- HTML works without JavaScript
+- Alpine.js enhances interactivity
+- Jinja2 provides server-side rendering
+
+### 4. API-First Design
+- Backend exposes RESTful APIs
+- Frontend consumes APIs via Fetch
+- Clear separation of concerns
+
+### 5. Clean Architecture
+- Service layer for business logic
+- Repository pattern for data access
+- Exception-first error handling
+- Dependency injection
+
+## 📖 Documentation Files
+
+### Slice Files (This Directory)
+- `00_slices_overview.md` - This file
+- `01_slice1_admin_vendor_foundation.md`
+- `02_slice2_marketplace_import.md`
+- `03_slice3_product_catalog.md`
+- `04_slice4_customer_shopping.md`
+- `05_slice5_order_processing.md`
+
+### Supporting Documentation
+- `../quick_start_guide.md` - Get running in 15 minutes
+- `../css_structure_guide.txt` - CSS organization
+- `../css_quick_reference.txt` - CSS usage guide
+- `../12.project_readme_final.md` - Complete project README
+
+## 🚀 Getting Started
+
+### For Current Development (Slice 1)
+1. Read `01_slice1_admin_vendor_foundation.md`
+2. Follow setup in `../quick_start_guide.md`
+3. Complete Slice 1 testing checklist
+4. Move to Slice 2
+
+### For New Features
+1. Review this overview
+2. Read the relevant slice documentation
+3. Follow the implementation pattern
+4. Test thoroughly before moving forward
+
+## 💡 Tips for Success
+
+### Working with Slices
+- ✅ Complete one slice fully before starting the next
+- ✅ Test each slice thoroughly
+- ✅ Update documentation as you go
+- ✅ Commit code after each slice completion
+- ✅ Demo each slice to stakeholders
+
+### Alpine.js Best Practices
+- Keep components small and focused
+- Use `x-data` for component state
+- Use `x-init` for initialization
+- Prefer `x-show` over `x-if` for toggles
+- Use Alpine directives, not vanilla JS DOM manipulation
+
+### Jinja2 Best Practices
+- Extend base templates
+- Use template inheritance
+- Pass initial data from backend
+- Keep logic in backend, not templates
+- Use filters for formatting
+
+## 🎯 Success Metrics
+
+### By End of Slice 1
+- Admin can create vendors ✅
+- Vendor owners can log in ⏳
+- Vendor context detection works ✅
+- Complete data isolation verified
+
+### By End of Slice 2
+- Vendors can import CSV files
+- Import jobs tracked in background
+- Product staging area functional
+
+### By End of Slice 3
+- Products published to catalog
+- Inventory management working
+- Product customization enabled
+
+### By End of Slice 4
+- Customers can browse products
+- Shopping cart functional
+- Customer accounts working
+
+### By End of Slice 5
+- Complete checkout workflow
+- Order management operational
+- Platform ready for production
+
+---
+
+**Next Steps**: Start with `01_slice1_admin_vendor_foundation.md` to continue your current work on Slice 1.
diff --git a/middleware/__init__.py b/middleware/__init__.py
new file mode 100644
index 00000000..f6b3f5f8
--- /dev/null
+++ b/middleware/__init__.py
@@ -0,0 +1 @@
+ECHO is off.
diff --git a/models/database/admin.py b/models/database/admin.py
new file mode 100644
index 00000000..a74f10b4
--- /dev/null
+++ b/models/database/admin.py
@@ -0,0 +1 @@
+# Admin-specific models
diff --git a/models/database/audit.py b/models/database/audit.py
new file mode 100644
index 00000000..a4093794
--- /dev/null
+++ b/models/database/audit.py
@@ -0,0 +1 @@
+# AuditLog, DataExportLog models
diff --git a/models/database/backup.py b/models/database/backup.py
new file mode 100644
index 00000000..7efe94f4
--- /dev/null
+++ b/models/database/backup.py
@@ -0,0 +1 @@
+# BackupLog, RestoreLog models
diff --git a/models/database/configuration.py b/models/database/configuration.py
new file mode 100644
index 00000000..a6d610ee
--- /dev/null
+++ b/models/database/configuration.py
@@ -0,0 +1 @@
+# PlatformConfig, VendorConfig, FeatureFlag models
diff --git a/models/database/marketplace.py b/models/database/marketplace.py
new file mode 100644
index 00000000..9e5c025c
--- /dev/null
+++ b/models/database/marketplace.py
@@ -0,0 +1 @@
+# MarketplaceImportJob model
diff --git a/models/database/media.py b/models/database/media.py
new file mode 100644
index 00000000..9e51d54d
--- /dev/null
+++ b/models/database/media.py
@@ -0,0 +1 @@
+# MediaFile, ProductMedia models
diff --git a/models/database/monitoring.py b/models/database/monitoring.py
new file mode 100644
index 00000000..ed2edb20
--- /dev/null
+++ b/models/database/monitoring.py
@@ -0,0 +1 @@
+# PerformanceMetric, ErrorLog, SystemAlert models
diff --git a/models/database/notification.py b/models/database/notification.py
new file mode 100644
index 00000000..5224c7a9
--- /dev/null
+++ b/models/database/notification.py
@@ -0,0 +1 @@
+# NotificationTemplate, NotificationQueue, NotificationLog models
diff --git a/models/database/payment.py b/models/database/payment.py
new file mode 100644
index 00000000..4ebd4ebd
--- /dev/null
+++ b/models/database/payment.py
@@ -0,0 +1 @@
+# Payment, PaymentMethod, VendorPaymentConfig models
diff --git a/models/database/search.py b/models/database/search.py
new file mode 100644
index 00000000..e13e7439
--- /dev/null
+++ b/models/database/search.py
@@ -0,0 +1 @@
+# SearchIndex, SearchQuery models
diff --git a/models/database/task.py b/models/database/task.py
new file mode 100644
index 00000000..34945e62
--- /dev/null
+++ b/models/database/task.py
@@ -0,0 +1 @@
+# TaskLog model
diff --git a/models/schema/admin.py b/models/schema/admin.py
new file mode 100644
index 00000000..47bda2ec
--- /dev/null
+++ b/models/schema/admin.py
@@ -0,0 +1 @@
+# Admin operation models
diff --git a/models/schema/marketplace.py b/models/schema/marketplace.py
new file mode 100644
index 00000000..22a01dd7
--- /dev/null
+++ b/models/schema/marketplace.py
@@ -0,0 +1 @@
+# Marketplace import job models
diff --git a/models/schema/media.py b/models/schema/media.py
new file mode 100644
index 00000000..cd0a71f0
--- /dev/null
+++ b/models/schema/media.py
@@ -0,0 +1 @@
+# Media/file management models
diff --git a/models/schema/monitoring.py b/models/schema/monitoring.py
new file mode 100644
index 00000000..84f2d5a3
--- /dev/null
+++ b/models/schema/monitoring.py
@@ -0,0 +1 @@
+# Monitoring models
diff --git a/models/schema/notification.py b/models/schema/notification.py
new file mode 100644
index 00000000..cb27dfba
--- /dev/null
+++ b/models/schema/notification.py
@@ -0,0 +1 @@
+# Notification models
diff --git a/models/schema/payment.py b/models/schema/payment.py
new file mode 100644
index 00000000..1442207e
--- /dev/null
+++ b/models/schema/payment.py
@@ -0,0 +1 @@
+# Payment models
diff --git a/models/schema/search.py b/models/schema/search.py
new file mode 100644
index 00000000..69abe85d
--- /dev/null
+++ b/models/schema/search.py
@@ -0,0 +1 @@
+# Search models
diff --git a/models/schema/team.py b/models/schema/team.py
new file mode 100644
index 00000000..ad854e3c
--- /dev/null
+++ b/models/schema/team.py
@@ -0,0 +1 @@
+# Team management models
diff --git a/static/admin/marketplace.html b/static/admin/marketplace.html
new file mode 100644
index 00000000..24b14149
--- /dev/null
+++ b/static/admin/marketplace.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ System-wide marketplace monitoring
+
+
+ <-- System-wide marketplace monitoring -->
+
+
diff --git a/static/admin/monitoring.html b/static/admin/monitoring.html
new file mode 100644
index 00000000..7a52b543
--- /dev/null
+++ b/static/admin/monitoring.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ System monitoring
+
+
+ <-- System monitoring -->
+
+
diff --git a/static/admin/users.html b/static/admin/users.html
new file mode 100644
index 00000000..74c5f6ae
--- /dev/null
+++ b/static/admin/users.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ User management
+
+
+ <-- User management -->
+
+
diff --git a/static/js/admin/analytics.js b/static/js/admin/analytics.js
new file mode 100644
index 00000000..78688cc3
--- /dev/null
+++ b/static/js/admin/analytics.js
@@ -0,0 +1 @@
+// Admin analytics
diff --git a/static/js/admin/dashboard.js b/static/js/admin/dashboard.js
new file mode 100644
index 00000000..9989ac44
--- /dev/null
+++ b/static/js/admin/dashboard.js
@@ -0,0 +1 @@
+// Admin dashboard
diff --git a/static/js/admin/monitoring.js b/static/js/admin/monitoring.js
new file mode 100644
index 00000000..a974542d
--- /dev/null
+++ b/static/js/admin/monitoring.js
@@ -0,0 +1 @@
+// System monitoring
diff --git a/static/js/admin/vendors.js b/static/js/admin/vendors.js
new file mode 100644
index 00000000..02cec395
--- /dev/null
+++ b/static/js/admin/vendors.js
@@ -0,0 +1 @@
+// Vendor management
diff --git a/static/js/shared/media-upload.js b/static/js/shared/media-upload.js
new file mode 100644
index 00000000..cabbf399
--- /dev/null
+++ b/static/js/shared/media-upload.js
@@ -0,0 +1 @@
+// File upload utilities
diff --git a/static/js/shared/notification.js b/static/js/shared/notification.js
new file mode 100644
index 00000000..787cf2d7
--- /dev/null
+++ b/static/js/shared/notification.js
@@ -0,0 +1 @@
+// Notification handling
diff --git a/static/js/shared/search.js b/static/js/shared/search.js
new file mode 100644
index 00000000..adc32660
--- /dev/null
+++ b/static/js/shared/search.js
@@ -0,0 +1 @@
+// Search functionality
diff --git a/static/js/shared/vendor-context.js b/static/js/shared/vendor-context.js
new file mode 100644
index 00000000..28e82f06
--- /dev/null
+++ b/static/js/shared/vendor-context.js
@@ -0,0 +1 @@
+// Vendor context detection and management
diff --git a/static/js/shop/account.js b/static/js/shop/account.js
new file mode 100644
index 00000000..c573b782
--- /dev/null
+++ b/static/js/shop/account.js
@@ -0,0 +1 @@
+// Customer account
diff --git a/static/js/shop/cart.js b/static/js/shop/cart.js
new file mode 100644
index 00000000..9ab57ccb
--- /dev/null
+++ b/static/js/shop/cart.js
@@ -0,0 +1 @@
+// Shopping cart
diff --git a/static/js/shop/catalog.js b/static/js/shop/catalog.js
new file mode 100644
index 00000000..b0d701ed
--- /dev/null
+++ b/static/js/shop/catalog.js
@@ -0,0 +1 @@
+// Product browsing
diff --git a/static/js/shop/checkout.js b/static/js/shop/checkout.js
new file mode 100644
index 00000000..dfe60dd2
--- /dev/null
+++ b/static/js/shop/checkout.js
@@ -0,0 +1 @@
+// Checkout process
diff --git a/static/js/shop/search.js b/static/js/shop/search.js
new file mode 100644
index 00000000..7ec3d1e3
--- /dev/null
+++ b/static/js/shop/search.js
@@ -0,0 +1 @@
+// Product search
diff --git a/static/js/vendor/dashboard.js b/static/js/vendor/dashboard.js
new file mode 100644
index 00000000..6421c5d6
--- /dev/null
+++ b/static/js/vendor/dashboard.js
@@ -0,0 +1 @@
+// Vendor dashboard
diff --git a/static/js/vendor/marketplace.js b/static/js/vendor/marketplace.js
new file mode 100644
index 00000000..9b6ade64
--- /dev/null
+++ b/static/js/vendor/marketplace.js
@@ -0,0 +1 @@
+// Marketplace integration
diff --git a/static/js/vendor/media.js b/static/js/vendor/media.js
new file mode 100644
index 00000000..853f78a3
--- /dev/null
+++ b/static/js/vendor/media.js
@@ -0,0 +1 @@
+// Media management
diff --git a/static/js/vendor/orders.js b/static/js/vendor/orders.js
new file mode 100644
index 00000000..917f7f8a
--- /dev/null
+++ b/static/js/vendor/orders.js
@@ -0,0 +1 @@
+// Order management
diff --git a/static/js/vendor/payments.js b/static/js/vendor/payments.js
new file mode 100644
index 00000000..4c9efd9a
--- /dev/null
+++ b/static/js/vendor/payments.js
@@ -0,0 +1 @@
+// Payment configuration
diff --git a/static/js/vendor/products.js b/static/js/vendor/products.js
new file mode 100644
index 00000000..c3f4a0b4
--- /dev/null
+++ b/static/js/vendor/products.js
@@ -0,0 +1 @@
+// Catalog management
diff --git a/static/shop/account/addresses.html b/static/shop/account/addresses.html
new file mode 100644
index 00000000..0ca25b1b
--- /dev/null
+++ b/static/shop/account/addresses.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Address management
+
+
+ <-- Address management -->
+
+
diff --git a/static/shop/account/orders.html b/static/shop/account/orders.html
new file mode 100644
index 00000000..909a3bf9
--- /dev/null
+++ b/static/shop/account/orders.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Order history
+
+
+ <-- Order history -->
+
+
diff --git a/static/shop/account/profile.html b/static/shop/account/profile.html
new file mode 100644
index 00000000..96b6d750
--- /dev/null
+++ b/static/shop/account/profile.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Customer profile
+
+
+ <-- Customer profile -->
+
+
diff --git a/static/shop/checkout.html b/static/shop/checkout.html
new file mode 100644
index 00000000..3270d419
--- /dev/null
+++ b/static/shop/checkout.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Checkout process
+
+
+ <-- Checkout process -->
+
+
diff --git a/static/shop/home.html b/static/shop/home.html
new file mode 100644
index 00000000..8e463635
--- /dev/null
+++ b/static/shop/home.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Shop homepage
+
+
+ <-- Shop homepage -->
+
+
diff --git a/static/shop/search.html b/static/shop/search.html
new file mode 100644
index 00000000..3dfc5efa
--- /dev/null
+++ b/static/shop/search.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Search results page
+
+
+ <-- Search results page -->
+
+
diff --git a/static/vendor/admin/customers.html b/static/vendor/admin/customers.html
new file mode 100644
index 00000000..a8f5c42e
--- /dev/null
+++ b/static/vendor/admin/customers.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Customer management
+
+
+ <-- Customer management -->
+
+
diff --git a/static/vendor/admin/inventory.html b/static/vendor/admin/inventory.html
new file mode 100644
index 00000000..5753706c
--- /dev/null
+++ b/static/vendor/admin/inventory.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Inventory management - catalog products
+
+
+ <-- Inventory management - catalog products -->
+
+
diff --git a/static/vendor/admin/marketplace/browse.html b/static/vendor/admin/marketplace/browse.html
new file mode 100644
index 00000000..1f51208a
--- /dev/null
+++ b/static/vendor/admin/marketplace/browse.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Browse marketplace products - staging
+
+
+ <-- Browse marketplace products - staging -->
+
+
diff --git a/static/vendor/admin/marketplace/config.html b/static/vendor/admin/marketplace/config.html
new file mode 100644
index 00000000..293391dd
--- /dev/null
+++ b/static/vendor/admin/marketplace/config.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Marketplace configuration
+
+
+ <-- Marketplace configuration -->
+
+
diff --git a/static/vendor/admin/marketplace/imports.html b/static/vendor/admin/marketplace/imports.html
new file mode 100644
index 00000000..eab10ee1
--- /dev/null
+++ b/static/vendor/admin/marketplace/imports.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Import jobs and history
+
+
+ <-- Import jobs and history -->
+
+
diff --git a/static/vendor/admin/marketplace/selected.html b/static/vendor/admin/marketplace/selected.html
new file mode 100644
index 00000000..6c41fe2f
--- /dev/null
+++ b/static/vendor/admin/marketplace/selected.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Selected products - pre-publish
+
+
+ <-- Selected products - pre-publish -->
+
+
diff --git a/static/vendor/admin/media.html b/static/vendor/admin/media.html
new file mode 100644
index 00000000..91f6506a
--- /dev/null
+++ b/static/vendor/admin/media.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Media library
+
+
+ <-- Media library -->
+
+
diff --git a/static/vendor/admin/notifications.html b/static/vendor/admin/notifications.html
new file mode 100644
index 00000000..4958e795
--- /dev/null
+++ b/static/vendor/admin/notifications.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Notification templates and logs
+
+
+ <-- Notification templates and logs -->
+
+
diff --git a/static/vendor/admin/orders.html b/static/vendor/admin/orders.html
new file mode 100644
index 00000000..e9cefeee
--- /dev/null
+++ b/static/vendor/admin/orders.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Order management
+
+
+ <-- Order management -->
+
+
diff --git a/static/vendor/admin/payments.html b/static/vendor/admin/payments.html
new file mode 100644
index 00000000..9b5eee1d
--- /dev/null
+++ b/static/vendor/admin/payments.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Payment configuration
+
+
+ <-- Payment configuration -->
+
+
diff --git a/static/vendor/admin/products.html b/static/vendor/admin/products.html
new file mode 100644
index 00000000..de610c02
--- /dev/null
+++ b/static/vendor/admin/products.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Catalog management - Product table
+
+
+ <-- Catalog management - Product table -->
+
+
diff --git a/static/vendor/admin/settings.html b/static/vendor/admin/settings.html
new file mode 100644
index 00000000..c4e0c574
--- /dev/null
+++ b/static/vendor/admin/settings.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Vendor settings
+
+
+ <-- Vendor settings -->
+
+
diff --git a/static/vendor/admin/teams.html b/static/vendor/admin/teams.html
new file mode 100644
index 00000000..1e8811bf
--- /dev/null
+++ b/static/vendor/admin/teams.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Team management
+
+
+ <-- Team management -->
+
+
diff --git a/storage/__init__.py b/storage/__init__.py
new file mode 100644
index 00000000..f6b3f5f8
--- /dev/null
+++ b/storage/__init__.py
@@ -0,0 +1 @@
+ECHO is off.
diff --git a/storage/backends.py b/storage/backends.py
new file mode 100644
index 00000000..4eb1b1ea
--- /dev/null
+++ b/storage/backends.py
@@ -0,0 +1 @@
+# Storage backend implementations
diff --git a/storage/utils.py b/storage/utils.py
new file mode 100644
index 00000000..99c27a1d
--- /dev/null
+++ b/storage/utils.py
@@ -0,0 +1 @@
+# Storage utilities
diff --git a/tasks/__init__.py b/tasks/__init__.py
new file mode 100644
index 00000000..f6b3f5f8
--- /dev/null
+++ b/tasks/__init__.py
@@ -0,0 +1 @@
+ECHO is off.
diff --git a/tasks/analytics_tasks.py b/tasks/analytics_tasks.py
new file mode 100644
index 00000000..6adffe9b
--- /dev/null
+++ b/tasks/analytics_tasks.py
@@ -0,0 +1 @@
+# Analytics and reporting tasks
diff --git a/tasks/backup_tasks.py b/tasks/backup_tasks.py
new file mode 100644
index 00000000..ce44c3ec
--- /dev/null
+++ b/tasks/backup_tasks.py
@@ -0,0 +1 @@
+# Backup and recovery tasks
diff --git a/tasks/cleanup_tasks.py b/tasks/cleanup_tasks.py
new file mode 100644
index 00000000..822a2e92
--- /dev/null
+++ b/tasks/cleanup_tasks.py
@@ -0,0 +1 @@
+# Data cleanup and maintenance tasks
diff --git a/tasks/email_tasks.py b/tasks/email_tasks.py
new file mode 100644
index 00000000..81bf1fd3
--- /dev/null
+++ b/tasks/email_tasks.py
@@ -0,0 +1 @@
+# Email sending tasks
diff --git a/tasks/marketplace_import.py b/tasks/marketplace_import.py
new file mode 100644
index 00000000..552e1a7e
--- /dev/null
+++ b/tasks/marketplace_import.py
@@ -0,0 +1 @@
+# Marketplace CSV import tasks
diff --git a/tasks/media_processing.py b/tasks/media_processing.py
new file mode 100644
index 00000000..5a57bed2
--- /dev/null
+++ b/tasks/media_processing.py
@@ -0,0 +1 @@
+# Image processing and optimization tasks
diff --git a/tasks/search_indexing.py b/tasks/search_indexing.py
new file mode 100644
index 00000000..64f776af
--- /dev/null
+++ b/tasks/search_indexing.py
@@ -0,0 +1 @@
+# Search index maintenance tasks
diff --git a/tasks/task_manager.py b/tasks/task_manager.py
new file mode 100644
index 00000000..d7fe33a0
--- /dev/null
+++ b/tasks/task_manager.py
@@ -0,0 +1 @@
+# Celery configuration and task management