compiling project documentation
This commit is contained in:
617
docs/__temp/10.stripe_payment_integration.md
Normal file
617
docs/__temp/10.stripe_payment_integration.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# Stripe Payment Integration - Multi-Tenant Ecommerce Platform
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The payment integration uses **Stripe Connect** to handle multi-vendor payments, enabling:
|
||||
- Each vendor to receive payments directly
|
||||
- Platform to collect fees/commissions
|
||||
- Proper financial isolation between vendors
|
||||
- Compliance with financial regulations
|
||||
|
||||
## Payment Models
|
||||
|
||||
### Database Models
|
||||
|
||||
```python
|
||||
# models/database/payment.py
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.core.database import Base
|
||||
from .base import TimestampMixin
|
||||
|
||||
|
||||
class VendorPaymentConfig(Base, TimestampMixin):
|
||||
"""Vendor-specific payment configuration."""
|
||||
__tablename__ = "vendor_payment_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, unique=True)
|
||||
|
||||
# Stripe Connect configuration
|
||||
stripe_account_id = Column(String(255)) # Stripe Connect account ID
|
||||
stripe_account_status = Column(String(50)) # pending, active, restricted, inactive
|
||||
stripe_onboarding_url = Column(Text) # Onboarding link for vendor
|
||||
stripe_dashboard_url = Column(Text) # Vendor's Stripe dashboard
|
||||
|
||||
# Payment settings
|
||||
accepts_payments = Column(Boolean, default=False)
|
||||
currency = Column(String(3), default="EUR")
|
||||
platform_fee_percentage = Column(Numeric(5, 2), default=2.5) # Platform commission
|
||||
|
||||
# Payout settings
|
||||
payout_schedule = Column(String(20), default="weekly") # daily, weekly, monthly
|
||||
minimum_payout = Column(Numeric(10, 2), default=20.00)
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="payment_config")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VendorPaymentConfig(vendor_id={self.vendor_id}, stripe_account_id='{self.stripe_account_id}')>"
|
||||
|
||||
|
||||
class Payment(Base, TimestampMixin):
|
||||
"""Payment records for orders."""
|
||||
__tablename__ = "payments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
|
||||
|
||||
# Stripe payment details
|
||||
stripe_payment_intent_id = Column(String(255), unique=True, index=True)
|
||||
stripe_charge_id = Column(String(255), index=True)
|
||||
stripe_transfer_id = Column(String(255)) # Transfer to vendor account
|
||||
|
||||
# Payment amounts (in cents to avoid floating point issues)
|
||||
amount_total = Column(Integer, nullable=False) # Total customer payment
|
||||
amount_vendor = Column(Integer, nullable=False) # Amount to vendor
|
||||
amount_platform_fee = Column(Integer, nullable=False) # Platform commission
|
||||
currency = Column(String(3), default="EUR")
|
||||
|
||||
# Payment status
|
||||
status = Column(String(50), nullable=False) # pending, succeeded, failed, refunded
|
||||
payment_method = Column(String(50)) # card, bank_transfer, etc.
|
||||
|
||||
# Metadata
|
||||
stripe_metadata = Column(Text) # JSON string of Stripe metadata
|
||||
failure_reason = Column(Text)
|
||||
refund_reason = Column(Text)
|
||||
|
||||
# Timestamps
|
||||
paid_at = Column(DateTime)
|
||||
refunded_at = Column(DateTime)
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor")
|
||||
order = relationship("Order", back_populates="payment")
|
||||
customer = relationship("Customer")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Payment(id={self.id}, order_id={self.order_id}, status='{self.status}')>"
|
||||
|
||||
@property
|
||||
def amount_total_euros(self):
|
||||
"""Convert cents to euros for display."""
|
||||
return self.amount_total / 100
|
||||
|
||||
@property
|
||||
def amount_vendor_euros(self):
|
||||
"""Convert cents to euros for display."""
|
||||
return self.amount_vendor / 100
|
||||
|
||||
|
||||
class PaymentMethod(Base, TimestampMixin):
|
||||
"""Saved customer payment methods."""
|
||||
__tablename__ = "payment_methods"
|
||||
|
||||
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)
|
||||
|
||||
# Stripe payment method details
|
||||
stripe_payment_method_id = Column(String(255), nullable=False, index=True)
|
||||
payment_method_type = Column(String(50), nullable=False) # card, sepa_debit, etc.
|
||||
|
||||
# Card details (if applicable)
|
||||
card_brand = Column(String(50)) # visa, mastercard, etc.
|
||||
card_last4 = Column(String(4))
|
||||
card_exp_month = Column(Integer)
|
||||
card_exp_year = Column(Integer)
|
||||
|
||||
# Settings
|
||||
is_default = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor")
|
||||
customer = relationship("Customer")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PaymentMethod(id={self.id}, customer_id={self.customer_id}, type='{self.payment_method_type}')>"
|
||||
```
|
||||
|
||||
### Updated Order Model
|
||||
|
||||
```python
|
||||
# Update models/database/order.py
|
||||
class Order(Base, TimestampMixin):
|
||||
# ... existing fields ...
|
||||
|
||||
# Payment integration
|
||||
payment_status = Column(String(50), default="pending") # pending, paid, failed, refunded
|
||||
payment_intent_id = Column(String(255)) # Stripe PaymentIntent ID
|
||||
total_amount_cents = Column(Integer, nullable=False) # Amount in cents
|
||||
|
||||
# Relationships
|
||||
payment = relationship("Payment", back_populates="order", uselist=False)
|
||||
|
||||
@property
|
||||
def total_amount_euros(self):
|
||||
"""Convert cents to euros for display."""
|
||||
return self.total_amount_cents / 100 if self.total_amount_cents else 0
|
||||
```
|
||||
|
||||
## Payment Service Integration
|
||||
|
||||
### Stripe Service
|
||||
|
||||
```python
|
||||
# services/payment_service.py
|
||||
import stripe
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from models.database.payment import Payment, VendorPaymentConfig
|
||||
from models.database.order import Order
|
||||
from models.database.vendor import Vendor
|
||||
from app.exceptions.payment import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configure Stripe
|
||||
stripe.api_key = settings.stripe_secret_key
|
||||
|
||||
|
||||
class PaymentService:
|
||||
"""Service for handling Stripe payments in multi-tenant environment."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def create_payment_intent(
|
||||
self,
|
||||
vendor_id: int,
|
||||
order_id: int,
|
||||
amount_euros: Decimal,
|
||||
customer_email: str,
|
||||
metadata: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""Create Stripe PaymentIntent for vendor order."""
|
||||
|
||||
# Get vendor payment configuration
|
||||
payment_config = self.get_vendor_payment_config(vendor_id)
|
||||
if not payment_config.accepts_payments:
|
||||
raise PaymentNotConfiguredException(f"Vendor {vendor_id} not configured for payments")
|
||||
|
||||
# Calculate amounts
|
||||
amount_cents = int(amount_euros * 100)
|
||||
platform_fee_cents = int(amount_cents * (payment_config.platform_fee_percentage / 100))
|
||||
vendor_amount_cents = amount_cents - platform_fee_cents
|
||||
|
||||
try:
|
||||
# Create PaymentIntent with Stripe Connect
|
||||
payment_intent = stripe.PaymentIntent.create(
|
||||
amount=amount_cents,
|
||||
currency=payment_config.currency.lower(),
|
||||
application_fee_amount=platform_fee_cents,
|
||||
transfer_data={
|
||||
'destination': payment_config.stripe_account_id,
|
||||
},
|
||||
metadata={
|
||||
'vendor_id': str(vendor_id),
|
||||
'order_id': str(order_id),
|
||||
'platform': 'multi_tenant_ecommerce',
|
||||
**(metadata or {})
|
||||
},
|
||||
receipt_email=customer_email,
|
||||
description=f"Order payment for vendor {vendor_id}"
|
||||
)
|
||||
|
||||
# Create payment record
|
||||
payment = Payment(
|
||||
vendor_id=vendor_id,
|
||||
order_id=order_id,
|
||||
customer_id=self.get_order_customer_id(order_id),
|
||||
stripe_payment_intent_id=payment_intent.id,
|
||||
amount_total=amount_cents,
|
||||
amount_vendor=vendor_amount_cents,
|
||||
amount_platform_fee=platform_fee_cents,
|
||||
currency=payment_config.currency,
|
||||
status='pending',
|
||||
stripe_metadata=json.dumps(payment_intent.metadata)
|
||||
)
|
||||
|
||||
self.db.add(payment)
|
||||
|
||||
# Update order
|
||||
order = self.db.query(Order).filter(Order.id == order_id).first()
|
||||
if order:
|
||||
order.payment_intent_id = payment_intent.id
|
||||
order.payment_status = 'pending'
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return {
|
||||
'payment_intent_id': payment_intent.id,
|
||||
'client_secret': payment_intent.client_secret,
|
||||
'amount_total': amount_euros,
|
||||
'amount_vendor': vendor_amount_cents / 100,
|
||||
'platform_fee': platform_fee_cents / 100,
|
||||
'currency': payment_config.currency
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating PaymentIntent: {e}")
|
||||
raise PaymentProcessingException(f"Payment processing failed: {str(e)}")
|
||||
|
||||
def confirm_payment(self, payment_intent_id: str) -> Payment:
|
||||
"""Confirm payment and update records."""
|
||||
|
||||
try:
|
||||
# Retrieve PaymentIntent from Stripe
|
||||
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
|
||||
|
||||
# Find payment record
|
||||
payment = self.db.query(Payment).filter(
|
||||
Payment.stripe_payment_intent_id == payment_intent_id
|
||||
).first()
|
||||
|
||||
if not payment:
|
||||
raise PaymentNotFoundException(f"Payment not found for intent {payment_intent_id}")
|
||||
|
||||
# Update payment status based on Stripe status
|
||||
if payment_intent.status == 'succeeded':
|
||||
payment.status = 'succeeded'
|
||||
payment.stripe_charge_id = payment_intent.charges.data[0].id if payment_intent.charges.data else None
|
||||
payment.paid_at = datetime.utcnow()
|
||||
|
||||
# Update order status
|
||||
order = self.db.query(Order).filter(Order.id == payment.order_id).first()
|
||||
if order:
|
||||
order.payment_status = 'paid'
|
||||
order.status = 'processing' # Move order to processing
|
||||
|
||||
elif payment_intent.status == 'payment_failed':
|
||||
payment.status = 'failed'
|
||||
payment.failure_reason = payment_intent.last_payment_error.message if payment_intent.last_payment_error else "Unknown error"
|
||||
|
||||
# Update order status
|
||||
order = self.db.query(Order).filter(Order.id == payment.order_id).first()
|
||||
if order:
|
||||
order.payment_status = 'failed'
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return payment
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error confirming payment: {e}")
|
||||
raise PaymentProcessingException(f"Payment confirmation failed: {str(e)}")
|
||||
|
||||
def create_vendor_stripe_account(self, vendor_id: int, vendor_data: Dict) -> str:
|
||||
"""Create Stripe Connect account for vendor."""
|
||||
|
||||
try:
|
||||
# Create Stripe Connect Express account
|
||||
account = stripe.Account.create(
|
||||
type='express',
|
||||
country='LU', # Luxembourg
|
||||
email=vendor_data.get('business_email'),
|
||||
capabilities={
|
||||
'card_payments': {'requested': True},
|
||||
'transfers': {'requested': True},
|
||||
},
|
||||
business_type='company',
|
||||
company={
|
||||
'name': vendor_data.get('business_name'),
|
||||
'phone': vendor_data.get('business_phone'),
|
||||
'address': {
|
||||
'line1': vendor_data.get('address_line1'),
|
||||
'city': vendor_data.get('city'),
|
||||
'postal_code': vendor_data.get('postal_code'),
|
||||
'country': 'LU'
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
'vendor_id': str(vendor_id),
|
||||
'platform': 'multi_tenant_ecommerce'
|
||||
}
|
||||
)
|
||||
|
||||
# Update or create payment configuration
|
||||
payment_config = self.get_or_create_vendor_payment_config(vendor_id)
|
||||
payment_config.stripe_account_id = account.id
|
||||
payment_config.stripe_account_status = account.charges_enabled and account.payouts_enabled and 'active' or 'pending'
|
||||
|
||||
self.db.commit()
|
||||
|
||||
return account.id
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating account: {e}")
|
||||
raise PaymentConfigurationException(f"Failed to create payment account: {str(e)}")
|
||||
|
||||
def create_onboarding_link(self, vendor_id: int) -> str:
|
||||
"""Create Stripe onboarding link for vendor."""
|
||||
|
||||
payment_config = self.get_vendor_payment_config(vendor_id)
|
||||
if not payment_config.stripe_account_id:
|
||||
raise PaymentNotConfiguredException("Vendor does not have Stripe account")
|
||||
|
||||
try:
|
||||
account_link = stripe.AccountLink.create(
|
||||
account=payment_config.stripe_account_id,
|
||||
refresh_url=f"{settings.frontend_url}/vendor/admin/payments/refresh",
|
||||
return_url=f"{settings.frontend_url}/vendor/admin/payments/success",
|
||||
type='account_onboarding',
|
||||
)
|
||||
|
||||
# Update onboarding URL
|
||||
payment_config.stripe_onboarding_url = account_link.url
|
||||
self.db.commit()
|
||||
|
||||
return account_link.url
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating onboarding link: {e}")
|
||||
raise PaymentConfigurationException(f"Failed to create onboarding link: {str(e)}")
|
||||
|
||||
def get_vendor_payment_config(self, vendor_id: int) -> VendorPaymentConfig:
|
||||
"""Get vendor payment configuration."""
|
||||
config = self.db.query(VendorPaymentConfig).filter(
|
||||
VendorPaymentConfig.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
raise PaymentNotConfiguredException(f"No payment configuration for vendor {vendor_id}")
|
||||
|
||||
return config
|
||||
|
||||
def webhook_handler(self, event_type: str, event_data: Dict) -> None:
|
||||
"""Handle Stripe webhook events."""
|
||||
|
||||
if event_type == 'payment_intent.succeeded':
|
||||
payment_intent_id = event_data['object']['id']
|
||||
self.confirm_payment(payment_intent_id)
|
||||
|
||||
elif event_type == 'payment_intent.payment_failed':
|
||||
payment_intent_id = event_data['object']['id']
|
||||
self.confirm_payment(payment_intent_id)
|
||||
|
||||
elif event_type == 'account.updated':
|
||||
# Update vendor account status
|
||||
account_id = event_data['object']['id']
|
||||
self.update_vendor_account_status(account_id, event_data['object'])
|
||||
|
||||
# Add more webhook handlers as needed
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Payment APIs
|
||||
|
||||
```python
|
||||
# app/api/v1/vendor/payments.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.vendor import Vendor
|
||||
from services.payment_service import PaymentService
|
||||
|
||||
router = APIRouter(prefix="/payments", tags=["vendor-payments"])
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def get_payment_config(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get vendor payment configuration."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
try:
|
||||
config = payment_service.get_vendor_payment_config(vendor.id)
|
||||
return {
|
||||
"stripe_account_id": config.stripe_account_id,
|
||||
"account_status": config.stripe_account_status,
|
||||
"accepts_payments": config.accepts_payments,
|
||||
"currency": config.currency,
|
||||
"platform_fee_percentage": float(config.platform_fee_percentage),
|
||||
"needs_onboarding": config.stripe_account_status != 'active'
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
"stripe_account_id": None,
|
||||
"account_status": "not_configured",
|
||||
"accepts_payments": False,
|
||||
"needs_setup": True
|
||||
}
|
||||
|
||||
|
||||
@router.post("/setup")
|
||||
async def setup_payments(
|
||||
setup_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Set up Stripe payments for vendor."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
vendor_data = {
|
||||
"business_name": vendor.name,
|
||||
"business_email": vendor.business_email,
|
||||
"business_phone": vendor.business_phone,
|
||||
**setup_data
|
||||
}
|
||||
|
||||
account_id = payment_service.create_vendor_stripe_account(vendor.id, vendor_data)
|
||||
onboarding_url = payment_service.create_onboarding_link(vendor.id)
|
||||
|
||||
return {
|
||||
"stripe_account_id": account_id,
|
||||
"onboarding_url": onboarding_url,
|
||||
"message": "Payment setup initiated. Complete onboarding to accept payments."
|
||||
}
|
||||
|
||||
|
||||
# app/api/v1/public/vendors/payments.py
|
||||
@router.post("/{vendor_id}/payments/create-intent")
|
||||
async def create_payment_intent(
|
||||
vendor_id: int,
|
||||
payment_data: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create payment intent for customer order."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
payment_intent = payment_service.create_payment_intent(
|
||||
vendor_id=vendor_id,
|
||||
order_id=payment_data['order_id'],
|
||||
amount_euros=Decimal(str(payment_data['amount'])),
|
||||
customer_email=payment_data['customer_email'],
|
||||
metadata=payment_data.get('metadata', {})
|
||||
)
|
||||
|
||||
return payment_intent
|
||||
|
||||
|
||||
@router.post("/webhooks/stripe")
|
||||
async def stripe_webhook(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle Stripe webhook events."""
|
||||
import stripe
|
||||
|
||||
payload = await request.body()
|
||||
sig_header = request.headers.get('stripe-signature')
|
||||
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, settings.stripe_webhook_secret
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid payload")
|
||||
except stripe.error.SignatureVerificationError:
|
||||
raise HTTPException(status_code=400, detail="Invalid signature")
|
||||
|
||||
payment_service = PaymentService(db)
|
||||
payment_service.webhook_handler(event['type'], event['data'])
|
||||
|
||||
return {"status": "success"}
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Checkout Process
|
||||
|
||||
```javascript
|
||||
// frontend/js/shop/checkout.js
|
||||
class CheckoutManager {
|
||||
constructor(vendorId) {
|
||||
this.vendorId = vendorId;
|
||||
this.stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
|
||||
this.elements = this.stripe.elements();
|
||||
this.paymentElement = null;
|
||||
}
|
||||
|
||||
async initializePayment(orderData) {
|
||||
// Create payment intent
|
||||
const response = await fetch(`/api/v1/public/vendors/${this.vendorId}/payments/create-intent`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
order_id: orderData.orderId,
|
||||
amount: orderData.total,
|
||||
customer_email: orderData.customerEmail
|
||||
})
|
||||
});
|
||||
|
||||
const { client_secret, amount_total, platform_fee } = await response.json();
|
||||
|
||||
// Display payment breakdown
|
||||
this.displayPaymentBreakdown(amount_total, platform_fee);
|
||||
|
||||
// Create payment element
|
||||
this.paymentElement = this.elements.create('payment', {
|
||||
clientSecret: client_secret
|
||||
});
|
||||
|
||||
this.paymentElement.mount('#payment-element');
|
||||
}
|
||||
|
||||
async confirmPayment(orderData) {
|
||||
const { error } = await this.stripe.confirmPayment({
|
||||
elements: this.elements,
|
||||
confirmParams: {
|
||||
return_url: `${window.location.origin}/shop/order-confirmation`,
|
||||
receipt_email: orderData.customerEmail
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
this.showPaymentError(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Updated Workflow Integration
|
||||
|
||||
### Enhanced Customer Purchase Workflow
|
||||
|
||||
```
|
||||
Customer adds products to cart
|
||||
↓
|
||||
Customer proceeds to checkout
|
||||
↓
|
||||
System creates Order (payment_status: pending)
|
||||
↓
|
||||
Frontend calls POST /api/v1/public/vendors/{vendor_id}/payments/create-intent
|
||||
↓
|
||||
PaymentService creates Stripe PaymentIntent with vendor destination
|
||||
↓
|
||||
Customer completes payment with Stripe Elements
|
||||
↓
|
||||
Stripe webhook confirms payment
|
||||
↓
|
||||
PaymentService updates Order (payment_status: paid, status: processing)
|
||||
↓
|
||||
Vendor receives order for fulfillment
|
||||
```
|
||||
|
||||
### Payment Configuration Workflow
|
||||
|
||||
```
|
||||
Vendor accesses payment settings
|
||||
↓
|
||||
POST /api/v1/vendor/payments/setup
|
||||
↓
|
||||
System creates Stripe Connect account
|
||||
↓
|
||||
Vendor completes Stripe onboarding
|
||||
↓
|
||||
Webhook updates account status to 'active'
|
||||
↓
|
||||
Vendor can now accept payments
|
||||
```
|
||||
|
||||
This integration provides secure, compliant payment processing while maintaining vendor isolation and enabling proper revenue distribution between vendors and the platform.
|
||||
781
docs/__temp/12.project_readme_final.md
Normal file
781
docs/__temp/12.project_readme_final.md
Normal file
@@ -0,0 +1,781 @@
|
||||
# Multi-Tenant Ecommerce Platform
|
||||
|
||||
A production-ready, multi-tenant ecommerce platform that enables vendors to operate independent webshops while integrating with external marketplaces. Built with complete vendor isolation, comprehensive business features, and modern reactive frontend.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
### Core Business Features
|
||||
|
||||
- **Multi-Vendor Marketplace**: Complete vendor isolation with independent webshops
|
||||
- **Marketplace Integration**: Import and curate products from external marketplaces (Letzshop CSV)
|
||||
- **Product Catalog Management**: Vendor-scoped product publishing from marketplace imports
|
||||
- **Inventory Management**: Real-time stock tracking with location-based inventory
|
||||
- **Order Management**: Complete order lifecycle with status tracking and fulfillment
|
||||
- **Customer Management**: Vendor-scoped customer accounts with order history
|
||||
- **Team Management**: Role-based access control with granular permissions
|
||||
- **Shopping Cart**: Session-based cart with real-time updates
|
||||
|
||||
### Technical Features
|
||||
|
||||
- **Modern Frontend Stack**: Alpine.js for reactive UI with zero build step
|
||||
- **RESTful API Architecture**: FastAPI with comprehensive OpenAPI documentation
|
||||
- **Service Layer Pattern**: Clean separation of business logic and data access
|
||||
- **Exception-First Error Handling**: Frontend-friendly error responses with consistent error codes
|
||||
- **Multi-tenant Security**: Complete data isolation with vendor context detection
|
||||
- **Background Job Processing**: Async marketplace imports with status tracking
|
||||
- **Comprehensive API**: Admin, Vendor, and Public (Customer) endpoints
|
||||
|
||||
### Security & Compliance
|
||||
|
||||
- **Complete Data Isolation**: Chinese wall between vendor data
|
||||
- **JWT Authentication**: Secure token-based authentication for all user types
|
||||
- **Role-Based Access Control**: Granular permissions (Owner, Manager, Editor, Viewer)
|
||||
- **Vendor Context Detection**: Subdomain and path-based tenant isolation
|
||||
- **Input Validation**: Pydantic models for all API requests
|
||||
- **Exception Handling**: Structured error responses with proper HTTP status codes
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Backend**: Python 3.13+ with FastAPI
|
||||
- **Database**: PostgreSQL with SQLAlchemy ORM
|
||||
- **Frontend**: Vanilla HTML, CSS, JavaScript with Alpine.js (CDN-based, no build step)
|
||||
- **Authentication**: JWT tokens with role-based permissions
|
||||
- **Background Jobs**: Async CSV import processing
|
||||
- **API Documentation**: Auto-generated OpenAPI/Swagger
|
||||
|
||||
### Multi-Tenancy Model
|
||||
|
||||
```
|
||||
Platform
|
||||
├── Admin Portal (admin.platform.com or /admin)
|
||||
│ ├── Vendor management
|
||||
│ ├── User administration
|
||||
│ ├── Platform statistics
|
||||
│ └── Import job monitoring
|
||||
├── Vendor A (vendor-a.platform.com or /vendor/{code})
|
||||
│ ├── Marketplace product imports
|
||||
│ ├── Product catalog publishing
|
||||
│ ├── Order management
|
||||
│ ├── Customer management
|
||||
│ ├── Team management
|
||||
│ └── Inventory tracking
|
||||
├── Vendor B (vendor-b.platform.com or /vendor/{code})
|
||||
│ └── Completely isolated from Vendor A
|
||||
└── Customer Shop (/shop or subdomain)
|
||||
├── Product browsing
|
||||
├── Shopping cart
|
||||
├── Order placement
|
||||
└── Order history
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Marketplace CSV → Import Job → MarketplaceProduct (Staging) → Product (Catalog) → Order → Customer
|
||||
↓ ↓ ↓
|
||||
Job Status Product Selection Inventory Tracking
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
├── main.py # FastAPI application entry point
|
||||
├── app/
|
||||
│ ├── api/
|
||||
│ │ ├── main.py # API router aggregation
|
||||
│ │ ├── deps.py # Dependency injection (auth, context)
|
||||
│ │ └── v1/ # API version 1
|
||||
│ │ ├── admin/ # Admin endpoints
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── auth.py
|
||||
│ │ │ ├── vendors.py
|
||||
│ │ │ ├── users.py
|
||||
│ │ │ ├── marketplace.py
|
||||
│ │ │ └── dashboard.py
|
||||
│ │ ├── vendor/ # Vendor endpoints
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── auth.py
|
||||
│ │ │ ├── dashboard.py
|
||||
│ │ │ ├── products.py
|
||||
│ │ │ ├── orders.py
|
||||
│ │ │ ├── marketplace.py
|
||||
│ │ │ ├── inventory.py
|
||||
│ │ │ └── vendor.py
|
||||
│ │ └── public/ # Customer endpoints
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── vendors/
|
||||
│ │ ├── auth.py
|
||||
│ │ ├── products.py
|
||||
│ │ ├── cart.py
|
||||
│ │ └── orders.py
|
||||
│ ├── core/
|
||||
│ │ ├── database.py # Database configuration
|
||||
│ │ ├── security.py # JWT and password utilities
|
||||
│ │ └── config.py # Application settings
|
||||
│ ├── exceptions/ # Custom exceptions
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py
|
||||
│ │ ├── auth.py
|
||||
│ │ ├── vendor.py
|
||||
│ │ ├── customer.py
|
||||
│ │ ├── product.py
|
||||
│ │ ├── order.py
|
||||
│ │ ├── inventory.py
|
||||
│ │ ├── team.py
|
||||
│ │ ├── marketplace_product.py
|
||||
│ │ ├── marketplace_import_job.py
|
||||
│ │ └── admin.py
|
||||
│ └── services/ # Business logic layer
|
||||
│ ├── auth_service.py
|
||||
│ ├── admin_service.py
|
||||
│ ├── vendor_service.py
|
||||
│ ├── customer_service.py
|
||||
│ ├── product_service.py
|
||||
│ ├── order_service.py
|
||||
│ ├── cart_service.py
|
||||
│ ├── inventory_service.py
|
||||
│ ├── team_service.py
|
||||
│ ├── marketplace_service.py
|
||||
│ └── stats_service.py
|
||||
├── models/
|
||||
│ ├── database/ # SQLAlchemy ORM models
|
||||
│ │ ├── base.py
|
||||
│ │ ├── user.py
|
||||
│ │ ├── vendor.py
|
||||
│ │ ├── customer.py
|
||||
│ │ ├── product.py
|
||||
│ │ ├── order.py
|
||||
│ │ ├── inventory.py
|
||||
│ │ ├── marketplace_product.py
|
||||
│ │ └── marketplace_import_job.py
|
||||
│ └── schemas/ # Pydantic validation models
|
||||
│ ├── auth.py
|
||||
│ ├── vendor.py
|
||||
│ ├── customer.py
|
||||
│ ├── product.py
|
||||
│ ├── order.py
|
||||
│ ├── inventory.py
|
||||
│ ├── marketplace_product.py
|
||||
│ ├── marketplace_import_job.py
|
||||
│ └── stats.py
|
||||
├── middleware/
|
||||
│ ├── auth.py # JWT authentication
|
||||
│ ├── vendor_context.py # Multi-tenant context detection
|
||||
│ ├── rate_limiter.py # API rate limiting
|
||||
│ └── decorators.py # Utility decorators
|
||||
├── static/ # Frontend assets (no build step required)
|
||||
│ ├── admin/ # Admin interface
|
||||
│ │ ├── login.html
|
||||
│ │ ├── dashboard.html
|
||||
│ │ └── vendors.html
|
||||
│ ├── vendor/ # Vendor management UI
|
||||
│ │ ├── login.html
|
||||
│ │ ├── dashboard.html
|
||||
│ │ └── admin/
|
||||
│ │ ├── products.html
|
||||
│ │ ├── orders.html
|
||||
│ │ └── marketplace.html
|
||||
│ ├── shop/ # Customer shop interface
|
||||
│ │ ├── products.html
|
||||
│ │ ├── product.html # Alpine.js product detail
|
||||
│ │ ├── cart.html
|
||||
│ │ └── account/
|
||||
│ │ ├── register.html
|
||||
│ │ ├── login.html
|
||||
│ │ └── orders.html
|
||||
│ ├── css/
|
||||
│ │ ├── shared/
|
||||
│ │ │ ├── base.css # CSS variables, utility classes
|
||||
│ │ │ └── auth.css
|
||||
│ │ ├── admin/
|
||||
│ │ │ └── admin.css
|
||||
│ │ ├── vendor/
|
||||
│ │ │ └── vendor.css
|
||||
│ │ └── shop/
|
||||
│ │ └── shop.css
|
||||
│ └── js/
|
||||
│ └── shared/
|
||||
│ ├── api-client.js
|
||||
│ ├── vendor-context.js
|
||||
│ └── utils.js
|
||||
├── scripts/
|
||||
│ ├── init_db.py # Database initialization
|
||||
│ └── create_admin.py # Admin user creation
|
||||
└── tests/
|
||||
├── unit/
|
||||
├── integration/
|
||||
└── e2e/
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.13+
|
||||
- PostgreSQL 14+ (SQLite for development)
|
||||
- Node.js (optional, only for development tools)
|
||||
|
||||
### Development Setup
|
||||
|
||||
#### 1. Clone and Setup Environment
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd multi-tenant-ecommerce
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 2. Database Setup
|
||||
|
||||
```bash
|
||||
# Create database
|
||||
createdb ecommerce_db
|
||||
|
||||
# Run migrations
|
||||
python scripts/init_db.py
|
||||
|
||||
# Create initial admin user
|
||||
python scripts/create_admin.py
|
||||
```
|
||||
|
||||
#### 3. Environment Configuration
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
Minimal `.env`:
|
||||
|
||||
```env
|
||||
DATABASE_URL=postgresql://user:pass@localhost:5432/ecommerce_db
|
||||
SECRET_KEY=your-secret-key-here-generate-with-openssl
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
DEVELOPMENT_MODE=true
|
||||
```
|
||||
|
||||
#### 4. Start Application
|
||||
|
||||
```bash
|
||||
# Start FastAPI application
|
||||
uvicorn main:app --reload --port 8000
|
||||
```
|
||||
|
||||
#### 5. Access the Platform
|
||||
|
||||
- **Admin Panel**: http://localhost:8000/admin/
|
||||
- **Vendor Login**: http://localhost:8000/vendor/login
|
||||
- **Customer Shop**: http://localhost:8000/shop/
|
||||
- **API Documentation**: http://localhost:8000/docs
|
||||
- **Health Check**: http://localhost:8000/health
|
||||
|
||||
### First Steps
|
||||
|
||||
1. **Login to Admin Panel**
|
||||
- URL: http://localhost:8000/admin/
|
||||
- Credentials: Created via `create_admin.py`
|
||||
|
||||
2. **Create First Vendor**
|
||||
- Navigate to Admin → Vendors
|
||||
- Click "Create Vendor"
|
||||
- Fill in vendor details (name, code, subdomain)
|
||||
- System creates vendor + owner user account
|
||||
- Note the temporary password
|
||||
|
||||
3. **Login as Vendor Owner**
|
||||
- URL: http://localhost:8000/vendor/login (or subdomain)
|
||||
- Use vendor owner credentials
|
||||
|
||||
4. **Import Products from Marketplace**
|
||||
- Navigate to Vendor → Marketplace Import
|
||||
- Configure Letzshop CSV URL
|
||||
- Trigger import job
|
||||
- Monitor import status
|
||||
|
||||
5. **Publish Products to Catalog**
|
||||
- Review imported products in staging
|
||||
- Select products to publish
|
||||
- Configure pricing and inventory
|
||||
- Publish to customer-facing catalog
|
||||
|
||||
## 📋 API Structure
|
||||
|
||||
### Admin APIs (`/api/v1/admin`)
|
||||
|
||||
**Authentication:**
|
||||
```
|
||||
POST /auth/login # Admin login
|
||||
```
|
||||
|
||||
**Vendor Management:**
|
||||
```
|
||||
GET /vendors # List all vendors
|
||||
POST /vendors # Create vendor with owner
|
||||
GET /vendors/{id} # Get vendor details
|
||||
PUT /vendors/{id}/verify # Verify/unverify vendor
|
||||
PUT /vendors/{id}/status # Toggle active status
|
||||
DELETE /vendors/{id} # Delete vendor
|
||||
```
|
||||
|
||||
**User Management:**
|
||||
```
|
||||
GET /users # List all users
|
||||
PUT /users/{id}/status # Toggle user status
|
||||
```
|
||||
|
||||
**Marketplace Monitoring:**
|
||||
```
|
||||
GET /marketplace-import-jobs # Monitor all import jobs
|
||||
```
|
||||
|
||||
**Dashboard & Statistics:**
|
||||
```
|
||||
GET /dashboard # Admin dashboard
|
||||
GET /dashboard/stats # Comprehensive statistics
|
||||
GET /dashboard/stats/marketplace # Marketplace breakdown
|
||||
GET /dashboard/stats/platform # Platform-wide metrics
|
||||
```
|
||||
|
||||
### Vendor APIs (`/api/v1/vendor`)
|
||||
|
||||
**Authentication:**
|
||||
```
|
||||
POST /auth/login # Vendor team login
|
||||
POST /auth/logout # Logout
|
||||
```
|
||||
|
||||
**Dashboard:**
|
||||
```
|
||||
GET /dashboard/stats # Vendor-specific statistics
|
||||
```
|
||||
|
||||
**Product Management:**
|
||||
```
|
||||
GET /products # List catalog products
|
||||
POST /products # Add product to catalog
|
||||
GET /products/{id} # Get product details
|
||||
PUT /products/{id} # Update product
|
||||
DELETE /products/{id} # Remove from catalog
|
||||
POST /products/from-import/{id} # Publish from marketplace
|
||||
PUT /products/{id}/toggle-active # Toggle product active
|
||||
PUT /products/{id}/toggle-featured # Toggle featured status
|
||||
```
|
||||
|
||||
**Order Management:**
|
||||
```
|
||||
GET /orders # List vendor orders
|
||||
GET /orders/{id} # Get order details
|
||||
PUT /orders/{id}/status # Update order status
|
||||
```
|
||||
|
||||
**Marketplace Integration:**
|
||||
```
|
||||
POST /marketplace/import # Trigger import job
|
||||
GET /marketplace/jobs # List import jobs
|
||||
GET /marketplace/jobs/{id} # Get job status
|
||||
GET /marketplace/products # List staged products
|
||||
POST /marketplace/products/publish # Bulk publish to catalog
|
||||
```
|
||||
|
||||
**Inventory Management:**
|
||||
```
|
||||
GET /inventory # List inventory items
|
||||
POST /inventory # Add inventory
|
||||
PUT /inventory/{id} # Update inventory
|
||||
GET /inventory/movements # Inventory movement history
|
||||
```
|
||||
|
||||
### Public/Customer APIs (`/api/v1/public/vendors`)
|
||||
|
||||
**Authentication:**
|
||||
```
|
||||
POST /{vendor_id}/auth/register # Customer registration
|
||||
POST /{vendor_id}/auth/login # Customer login
|
||||
POST /{vendor_id}/auth/logout # Customer logout
|
||||
```
|
||||
|
||||
**Product Browsing:**
|
||||
```
|
||||
GET /{vendor_id}/products # Browse product catalog
|
||||
GET /{vendor_id}/products/{id} # Product details
|
||||
GET /{vendor_id}/products/search # Search products
|
||||
```
|
||||
|
||||
**Shopping Cart:**
|
||||
```
|
||||
GET /{vendor_id}/cart/{session} # Get cart
|
||||
POST /{vendor_id}/cart/{session}/items # Add to cart
|
||||
PUT /{vendor_id}/cart/{session}/items/{id} # Update quantity
|
||||
DELETE /{vendor_id}/cart/{session}/items/{id} # Remove item
|
||||
DELETE /{vendor_id}/cart/{session} # Clear cart
|
||||
```
|
||||
|
||||
**Order Placement:**
|
||||
```
|
||||
POST /{vendor_id}/orders # Place order
|
||||
GET /{vendor_id}/customers/{id}/orders # Order history
|
||||
GET /{vendor_id}/customers/{id}/orders/{id} # Order details
|
||||
```
|
||||
|
||||
## 🎨 Frontend Architecture
|
||||
|
||||
### Alpine.js Integration
|
||||
|
||||
#### Why Alpine.js?
|
||||
|
||||
- ✅ Lightweight (15KB) - perfect for multi-tenant platform
|
||||
- ✅ No build step required - works directly in HTML
|
||||
- ✅ Reactive state management - modern UX without complexity
|
||||
- ✅ Perfect Jinja2 integration - server + client harmony
|
||||
- ✅ Scoped components - natural vendor isolation
|
||||
|
||||
#### Example: Product Detail Page
|
||||
|
||||
```html
|
||||
<div x-data="productDetail()" x-init="loadProduct()">
|
||||
<!-- Reactive product display -->
|
||||
<h1 x-text="product?.title"></h1>
|
||||
<p class="price">€<span x-text="product?.price"></span></p>
|
||||
|
||||
<!-- Quantity selector with validation -->
|
||||
<input
|
||||
type="number"
|
||||
x-model.number="quantity"
|
||||
:min="product?.min_quantity"
|
||||
:max="product?.available_inventory"
|
||||
>
|
||||
|
||||
<!-- Add to cart with loading state -->
|
||||
<button
|
||||
@click="addToCart()"
|
||||
:disabled="!canAddToCart || addingToCart"
|
||||
>
|
||||
<span x-show="!addingToCart">Add to Cart</span>
|
||||
<span x-show="addingToCart">Adding...</span>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### CSS Architecture
|
||||
|
||||
#### CSS Variables for Multi-Tenant Theming
|
||||
|
||||
```css
|
||||
/* Base variables in base.css */
|
||||
:root {
|
||||
--primary-color: #3b82f6;
|
||||
--primary-dark: #2563eb;
|
||||
--success-color: #10b981;
|
||||
--danger-color: #ef4444;
|
||||
--warning-color: #f59e0b;
|
||||
|
||||
/* Typography */
|
||||
--font-base: 16px;
|
||||
--font-sm: 0.875rem;
|
||||
--font-xl: 1.25rem;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
|
||||
/* Borders & Shadows */
|
||||
--radius-md: 0.375rem;
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Vendor-specific overrides */
|
||||
[data-vendor-theme="dark"] {
|
||||
--primary-color: #1f2937;
|
||||
--background-color: #111827;
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 Security Implementation
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
**Admin Login:**
|
||||
```
|
||||
1. POST /api/v1/admin/auth/login
|
||||
2. Verify credentials + admin role
|
||||
3. Generate JWT token
|
||||
4. Store token in localStorage
|
||||
5. Include in Authorization header for protected routes
|
||||
```
|
||||
|
||||
**Vendor Team Login:**
|
||||
```
|
||||
1. POST /api/v1/vendor/auth/login
|
||||
2. Detect vendor context (subdomain or path)
|
||||
3. Verify credentials + vendor membership
|
||||
4. Generate JWT token with vendor context
|
||||
5. All subsequent requests validated against vendor
|
||||
```
|
||||
|
||||
**Customer Login:**
|
||||
```
|
||||
1. POST /api/v1/public/vendors/{id}/auth/login
|
||||
2. Verify customer credentials for specific vendor
|
||||
3. Generate JWT token with customer context
|
||||
4. Customer can only access their own data
|
||||
```
|
||||
|
||||
### Vendor Context Detection
|
||||
|
||||
```python
|
||||
# Automatic vendor detection from:
|
||||
1. Subdomain: vendor-a.platform.com
|
||||
2. Path parameter: /vendor/VENDOR_A/
|
||||
3. JWT token: Embedded vendor_id in claims
|
||||
|
||||
# Complete data isolation:
|
||||
- All queries automatically scoped to vendor_id
|
||||
- Cross-vendor access prevented at service layer
|
||||
- Exception raised if vendor mismatch detected
|
||||
```
|
||||
|
||||
### Exception Handling Pattern
|
||||
|
||||
```python
|
||||
# Frontend-friendly error responses
|
||||
{
|
||||
"detail": "Human-readable error message",
|
||||
"error_code": "PRODUCT_NOT_FOUND",
|
||||
"status_code": 404,
|
||||
"timestamp": "2025-01-10T12:00:00Z",
|
||||
"request_id": "abc123"
|
||||
}
|
||||
|
||||
# Consistent error codes across platform
|
||||
- VENDOR_NOT_FOUND
|
||||
- PRODUCT_NOT_IN_CATALOG
|
||||
- INSUFFICIENT_INVENTORY
|
||||
- INVALID_ORDER_STATUS
|
||||
- UNAUTHORIZED_VENDOR_ACCESS
|
||||
```
|
||||
|
||||
## 📊 Database Schema
|
||||
|
||||
### Core Tables
|
||||
|
||||
**Multi-Tenant Foundation:**
|
||||
```sql
|
||||
vendors # Vendor accounts
|
||||
├── users # Platform/admin users
|
||||
├── vendor_users # Vendor team members (many-to-many)
|
||||
└── roles # Role definitions per vendor
|
||||
|
||||
customers # Vendor-scoped customers
|
||||
└── customer_addresses
|
||||
```
|
||||
|
||||
**Product & Inventory:**
|
||||
```sql
|
||||
marketplace_products # Imported from marketplaces (staging)
|
||||
└── marketplace_import_jobs # Import tracking
|
||||
|
||||
products # Published vendor catalog
|
||||
└── inventory # Stock tracking by location
|
||||
└── inventory_movements
|
||||
```
|
||||
|
||||
**Orders & Commerce:**
|
||||
```sql
|
||||
orders
|
||||
├── order_items
|
||||
├── shipping_address (FK to customer_addresses)
|
||||
└── billing_address (FK to customer_addresses)
|
||||
```
|
||||
|
||||
### Key Relationships
|
||||
|
||||
```
|
||||
Vendor (1) ──→ (N) Products
|
||||
Vendor (1) ──→ (N) Customers
|
||||
Vendor (1) ──→ (N) Orders
|
||||
Vendor (1) ──→ (N) MarketplaceProducts
|
||||
|
||||
Product (1) ──→ (N) Inventory
|
||||
Product (1) ──→ (1) MarketplaceProduct
|
||||
|
||||
Order (1) ──→ (N) OrderItems
|
||||
Order (1) ──→ (1) Customer
|
||||
Order (1) ──→ (1) ShippingAddress
|
||||
Order (1) ──→ (1) BillingAddress
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Current Test Coverage
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest tests/
|
||||
|
||||
# Run with coverage
|
||||
pytest --cov=app --cov=models --cov=middleware tests/
|
||||
|
||||
# Run specific test category
|
||||
pytest tests/unit/
|
||||
pytest tests/integration/
|
||||
pytest tests/e2e/
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
- **Unit Tests**: Service layer logic, model validation
|
||||
- **Integration Tests**: API endpoints, database operations
|
||||
- **E2E Tests**: Complete user workflows (admin creates vendor → vendor imports products → customer places order)
|
||||
|
||||
## 🚦 Development Status
|
||||
|
||||
### ✅ Completed Features
|
||||
|
||||
**Slice 1: Multi-Tenant Foundation**
|
||||
- ✅ Admin creates vendors through admin interface
|
||||
- ✅ Vendor owner login with context detection
|
||||
- ✅ Complete vendor data isolation
|
||||
- ✅ Role-based access control
|
||||
- ✅ JWT authentication system
|
||||
|
||||
**Slice 2: Marketplace Integration**
|
||||
- ✅ CSV import from Letzshop
|
||||
- ✅ Background job processing
|
||||
- ✅ Import status tracking
|
||||
- ✅ Product staging area
|
||||
- 🚧 Real-time Alpine.js status updates
|
||||
|
||||
**Slice 3: Product Catalog**
|
||||
- ✅ Product publishing from marketplace staging
|
||||
- ✅ Vendor product catalog management
|
||||
- ✅ Product CRUD operations
|
||||
- ✅ Inventory tracking
|
||||
- ✅ Product filtering and search
|
||||
|
||||
**Slice 4: Customer Shopping**
|
||||
- ✅ Customer service implementation
|
||||
- ✅ Customer registration/login
|
||||
- ✅ Product browsing interface
|
||||
- ✅ Shopping cart with Alpine.js
|
||||
- ✅ Product detail page (Alpine.js)
|
||||
|
||||
**Slice 5: Order Processing**
|
||||
- ✅ Order creation from cart
|
||||
- ✅ Order management (vendor)
|
||||
- ✅ Order history (customer)
|
||||
- ✅ Order status tracking
|
||||
- ✅ Inventory reservation
|
||||
|
||||
### 🚧 In Progress (BOOTS)
|
||||
|
||||
**Current Sprint:**
|
||||
- 🚧 Customer account dashboard (Alpine.js)
|
||||
- 🚧 Multi-step checkout flow
|
||||
- 🚧 Payment integration placeholder (Stripe ready)
|
||||
- 🚧 Order confirmation page
|
||||
- 🚧 Email notifications (order confirmations)
|
||||
|
||||
## 📋 Roadmap
|
||||
|
||||
### Phase 1: Core Platform (90% Complete)
|
||||
|
||||
- ✅ Multi-tenant architecture
|
||||
- ✅ Vendor management
|
||||
- ✅ Product catalog system
|
||||
- ✅ Order processing
|
||||
- ✅ Customer management
|
||||
- 🚧 Payment integration (ready for Stripe)
|
||||
- 🚧 Email notifications
|
||||
|
||||
### Phase 2: Advanced Features (Next)
|
||||
|
||||
- Persistent cart storage (Redis/Database)
|
||||
- Order search and filtering
|
||||
- Advanced inventory management
|
||||
- Product variants support
|
||||
- Customer reviews and ratings
|
||||
- Vendor analytics dashboard
|
||||
|
||||
### Phase 3: Enterprise Features (Future)
|
||||
|
||||
- Multi-language support
|
||||
- Advanced reporting and exports
|
||||
- Webhook integrations
|
||||
- API rate limiting enhancements
|
||||
- Performance monitoring
|
||||
- Automated backups
|
||||
|
||||
## 📝 Naming Conventions
|
||||
|
||||
The project follows strict naming conventions for consistency:
|
||||
|
||||
### Files
|
||||
|
||||
- **API files**: Plural (`products.py`, `orders.py`)
|
||||
- **Model files**: Singular (`product.py`, `order.py`)
|
||||
- **Service files**: Singular + service (`product_service.py`)
|
||||
- **Exception files**: Singular (`product.py`, `order.py`)
|
||||
|
||||
### Terminology
|
||||
|
||||
- **inventory** (not stock)
|
||||
- **vendor** (not shop)
|
||||
- **customer** (not user for end-users)
|
||||
- **team** (not staff)
|
||||
|
||||
See `docs/6.complete_naming_convention.md` for full details.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Fork the repository
|
||||
2. Create feature branch: `git checkout -b feature/amazing-feature`
|
||||
3. Follow existing patterns (service layer, exceptions, Alpine.js)
|
||||
4. Add tests for new features
|
||||
5. Update API documentation
|
||||
6. Submit pull request
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
black app/ models/ middleware/
|
||||
|
||||
# Sort imports
|
||||
isort app/ models/ middleware/
|
||||
|
||||
# Lint
|
||||
flake8 app/ models/ middleware/
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
### Documentation
|
||||
|
||||
- **API Reference**: http://localhost:8000/docs
|
||||
- **Development Guides**: `/docs/`
|
||||
- **Naming Conventions**: `/docs/6.complete_naming_convention.md`
|
||||
- **Vertical Slice Plan**: `/docs/3.vertical_slice_roadmap.md`
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Zero Build Step**: Frontend works without compilation
|
||||
- **Alpine.js Reactive UI**: Modern UX without framework complexity
|
||||
- **Service Layer Pattern**: Clean, testable business logic
|
||||
- **Exception-First**: Consistent error handling
|
||||
- **Multi-Tenant by Design**: Complete vendor isolation
|
||||
|
||||
---
|
||||
|
||||
Built with FastAPI, PostgreSQL, Alpine.js, and modern Python patterns for a scalable, maintainable multi-tenant ecommerce platform. 🚀
|
||||
313
docs/__temp/13.updated_application_workflows_final.md
Normal file
313
docs/__temp/13.updated_application_workflows_final.md
Normal file
@@ -0,0 +1,313 @@
|
||||
# Multi-Tenant Ecommerce Platform - Complete Application Workflows
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the complete workflows for the production-ready multi-tenant ecommerce platform, from marketplace import to customer analytics. Each workflow shows the interaction between different user types and the data flow through all system components including notifications, payments, media management, and monitoring.
|
||||
|
||||
## Core Data Flow Architecture
|
||||
|
||||
```
|
||||
Marketplace CSV → MarketplaceProduct (staging) → Product (catalog) → Customer Orders → Analytics
|
||||
↓ ↓ ↓
|
||||
Email Notifications Media Files Payment Processing
|
||||
↓ ↓ ↓
|
||||
Audit Logging Search Index Performance Monitoring
|
||||
```
|
||||
|
||||
## Workflow 1: Platform Setup and Vendor Onboarding
|
||||
|
||||
### Participants
|
||||
- **Admin**: Platform administrator
|
||||
- **New Vendor**: Business owner registering
|
||||
- **System**: Automated onboarding processes
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
#### 1.1 Admin Creates Vendor
|
||||
```
|
||||
Admin → Access admin panel (admin.platform.com)
|
||||
↓
|
||||
POST /api/v1/admin/auth/login
|
||||
↓
|
||||
Admin dashboard loads with platform metrics
|
||||
↓
|
||||
Admin → Create new vendor
|
||||
↓
|
||||
POST /api/v1/admin/vendors
|
||||
↓
|
||||
System creates:
|
||||
- Vendor record with subdomain
|
||||
- Owner user account
|
||||
- Default role structure (Owner, Manager, Editor, Viewer)
|
||||
- Default notification templates
|
||||
- Vendor payment configuration (inactive)
|
||||
- Initial search index
|
||||
- Audit log entry
|
||||
↓
|
||||
Email notification sent to vendor owner
|
||||
↓
|
||||
Vendor appears in admin vendor list
|
||||
```
|
||||
|
||||
#### 1.2 Vendor Owner Account Activation
|
||||
```
|
||||
Vendor Owner → Receives welcome email
|
||||
↓
|
||||
Click activation link → vendor.platform.com/admin/login
|
||||
↓
|
||||
POST /api/v1/vendor/auth/login
|
||||
↓
|
||||
Vendor context middleware detects vendor from subdomain
|
||||
↓
|
||||
Dashboard loads with vendor setup checklist:
|
||||
- ✅ Account created
|
||||
- ⏸️ Payment setup pending
|
||||
- ⏸️ Marketplace integration pending
|
||||
- ⏸️ First product pending
|
||||
```
|
||||
|
||||
#### 1.3 Payment Configuration
|
||||
```
|
||||
Vendor → Configure payments
|
||||
↓
|
||||
GET /api/v1/vendor/payments/config (returns needs_setup: true)
|
||||
↓
|
||||
POST /api/v1/vendor/payments/setup
|
||||
↓
|
||||
System creates Stripe Connect account
|
||||
↓
|
||||
Vendor redirected to Stripe onboarding
|
||||
↓
|
||||
Stripe webhook updates account status
|
||||
↓
|
||||
VendorPaymentConfig.accepts_payments = true
|
||||
↓
|
||||
Audit log: payment configuration completed
|
||||
```
|
||||
|
||||
### Data States After Onboarding
|
||||
- **Vendor**: Active with configured payments
|
||||
- **User**: Owner with full permissions
|
||||
- **Notifications**: Welcome sequence completed
|
||||
- **Audit Trail**: Complete onboarding history
|
||||
|
||||
---
|
||||
|
||||
## Workflow 2: Marketplace Import and Product Curation
|
||||
|
||||
### Participants
|
||||
- **Vendor**: Store owner/manager
|
||||
- **Background System**: Import processing
|
||||
- **Notification System**: Status updates
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
#### 2.1 Marketplace Configuration
|
||||
```
|
||||
Vendor → Configure Letzshop integration
|
||||
↓
|
||||
POST /api/v1/vendor/settings/marketplace
|
||||
↓
|
||||
Update Vendor.letzshop_csv_url
|
||||
↓
|
||||
Configuration validated and saved
|
||||
↓
|
||||
Audit log: marketplace configuration updated
|
||||
```
|
||||
|
||||
#### 2.2 Import Execution
|
||||
```
|
||||
Vendor → Trigger import
|
||||
↓
|
||||
POST /api/v1/vendor/marketplace/import
|
||||
↓
|
||||
System creates MarketplaceImportJob (status: pending)
|
||||
↓
|
||||
Background task queued: process_marketplace_import.delay(job_id)
|
||||
↓
|
||||
TaskLog created with progress tracking
|
||||
↓
|
||||
Celery worker processes import:
|
||||
- Downloads CSV from marketplace
|
||||
- Validates data format
|
||||
- Creates MarketplaceProduct records (staging)
|
||||
- Updates SearchIndex for browsing
|
||||
- Generates product image thumbnails
|
||||
- Updates job status with progress
|
||||
↓
|
||||
Email notification: import completed
|
||||
↓
|
||||
Audit log: import job completed
|
||||
```
|
||||
|
||||
#### 2.3 Product Discovery and Selection
|
||||
```
|
||||
Vendor → Browse imported products
|
||||
↓
|
||||
GET /api/v1/vendor/marketplace/imports/{job_id}/products
|
||||
↓
|
||||
Cache check for search results
|
||||
↓
|
||||
Display MarketplaceProduct records with:
|
||||
- Search and filtering capabilities
|
||||
- Thumbnail images
|
||||
- Selection status indicators
|
||||
- Bulk selection options
|
||||
↓
|
||||
Vendor → Select products for review
|
||||
↓
|
||||
POST /api/v1/vendor/marketplace/products/{id}/select
|
||||
↓
|
||||
MarketplaceProduct updated:
|
||||
- is_selected: true
|
||||
- selected_at: timestamp
|
||||
↓
|
||||
Search index updated
|
||||
↓
|
||||
Cache invalidation for product lists
|
||||
```
|
||||
|
||||
#### 2.4 Product Customization and Publishing
|
||||
```
|
||||
Vendor → Customize selected product
|
||||
↓
|
||||
Vendor uploads custom images:
|
||||
↓
|
||||
POST /api/v1/vendor/media/upload
|
||||
↓
|
||||
MediaService processes:
|
||||
- Creates vendor-scoped file path
|
||||
- Generates image variants (thumbnail, small, medium, large)
|
||||
- Uploads to storage backend (local/S3)
|
||||
- Creates MediaFile record
|
||||
↓
|
||||
Vendor customizes:
|
||||
- SKU (vendor-specific)
|
||||
- Price (markup from cost)
|
||||
- Description (enhanced/localized)
|
||||
- Images (vendor-uploaded + marketplace)
|
||||
- Categories (vendor taxonomy)
|
||||
- Inventory settings
|
||||
↓
|
||||
Vendor → Publish to catalog
|
||||
↓
|
||||
POST /api/v1/vendor/marketplace/products/{id}/publish
|
||||
↓
|
||||
System creates Product record:
|
||||
- Vendor-customized data
|
||||
- marketplace_product_id link
|
||||
- is_active: true
|
||||
↓
|
||||
Updates:
|
||||
- MarketplaceProduct: is_published: true
|
||||
- SearchIndex: product catalog entry
|
||||
- Cache invalidation: product catalogs
|
||||
↓
|
||||
Product now visible in vendor catalog
|
||||
↓
|
||||
Audit log: product published to catalog
|
||||
```
|
||||
|
||||
### Data States After Publication
|
||||
- **MarketplaceProduct**: Selected and published
|
||||
- **Product**: Active in vendor catalog
|
||||
- **MediaFile**: Vendor-specific product images
|
||||
- **SearchIndex**: Searchable in catalog
|
||||
- **Cache**: Invalidated and refreshed
|
||||
|
||||
---
|
||||
|
||||
## Workflow 3: Customer Shopping Experience
|
||||
|
||||
### Participants
|
||||
- **Customer**: End user shopping
|
||||
- **Vendor**: Store owner (indirect)
|
||||
- **Search System**: Product discovery
|
||||
- **Payment System**: Transaction processing
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
#### 3.1 Store Discovery and Browsing
|
||||
```
|
||||
Customer → Access vendor store
|
||||
↓
|
||||
vendor.platform.com OR platform.com/vendor/vendorname
|
||||
↓
|
||||
Vendor context middleware:
|
||||
- Identifies vendor from URL
|
||||
- Loads vendor-specific theme
|
||||
- Sets vendor_id context for all operations
|
||||
↓
|
||||
GET /api/v1/public/vendors/{vendor_id}/shop-info
|
||||
↓
|
||||
Cache check for vendor theme and configuration
|
||||
↓
|
||||
Store homepage loads with vendor branding
|
||||
```
|
||||
|
||||
#### 3.2 Product Search and Discovery
|
||||
```
|
||||
Customer → Search for products
|
||||
↓
|
||||
GET /api/v1/public/vendors/{vendor_id}/products/search?q=query
|
||||
↓
|
||||
SearchService processes query:
|
||||
- Cache check for search results
|
||||
- Elasticsearch query (if available) OR database search
|
||||
- Vendor-scoped results only
|
||||
- Logs search query for analytics
|
||||
↓
|
||||
Results include:
|
||||
- Product details with vendor customizations
|
||||
- Media files (images with variants)
|
||||
- Real-time inventory levels
|
||||
- Vendor-specific pricing
|
||||
↓
|
||||
Search analytics updated
|
||||
↓
|
||||
Cache results for future queries
|
||||
```
|
||||
|
||||
#### 3.3 Product Details and Media
|
||||
```
|
||||
Customer → View product details
|
||||
↓
|
||||
GET /api/v1/public/vendors/{vendor_id}/products/{id}
|
||||
↓
|
||||
System returns:
|
||||
- Product information from vendor catalog
|
||||
- Media gallery with all variants
|
||||
- Inventory availability
|
||||
- Vendor-specific descriptions and pricing
|
||||
↓
|
||||
Media files served from CDN for performance
|
||||
```
|
||||
|
||||
#### 3.4 Shopping Cart Management
|
||||
```
|
||||
Customer → Add to cart
|
||||
↓
|
||||
POST /api/v1/public/vendors/{vendor_id}/cart/{session_id}/items
|
||||
↓
|
||||
System:
|
||||
- Validates product availability
|
||||
- Checks inventory levels
|
||||
- Creates/updates session-based cart
|
||||
- Price validation against current Product prices
|
||||
↓
|
||||
Cart data cached for session
|
||||
```
|
||||
|
||||
#### 3.5 Customer Account Management
|
||||
```
|
||||
Customer → Create account
|
||||
↓
|
||||
POST /api/v1/public/vendors/{vendor_id}/customers/register
|
||||
↓
|
||||
System creates:
|
||||
- Customer record (vendor_id scoped)
|
||||
- Email unique within vendor only
|
||||
- Vendor-specific customer number
|
||||
- Default notification preferences
|
||||
↓
|
||||
Welcome email sent using vendor
|
||||
510
docs/__temp/14.updated_complete_project_structure_final.md
Normal file
510
docs/__temp/14.updated_complete_project_structure_final.md
Normal file
@@ -0,0 +1,510 @@
|
||||
# Multi-Tenant Ecommerce Platform - Complete Project Structure
|
||||
|
||||
## Project Overview
|
||||
|
||||
This document outlines the complete project structure for a production-ready multi-tenant ecommerce platform with marketplace integration. The platform implements complete vendor isolation with comprehensive business features including notifications, media management, search, caching, audit logging, and monitoring.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Backend**: Python FastAPI with PostgreSQL
|
||||
- **Frontend**: Vanilla HTML, CSS, JavaScript with Alpine.js (CDN-based, no build step)
|
||||
- **Background Jobs**: Celery with Redis
|
||||
- **Search**: Elasticsearch with database fallback
|
||||
- **Caching**: Redis multi-layer caching
|
||||
- **Storage**: Local/S3 with CDN integration
|
||||
- **Monitoring**: Custom monitoring with alerting
|
||||
- **Deployment**: Docker with environment-based configuration
|
||||
|
||||
## Complete Directory Structure
|
||||
|
||||
```
|
||||
├── main.py # FastAPI application entry point
|
||||
├── app/
|
||||
│ ├── api/
|
||||
│ │ ├── deps.py # Common dependencies (auth, context)
|
||||
│ │ ├── main.py # API router setup
|
||||
│ │ └── v1/ # API version 1 routes
|
||||
│ │ ├── admin/ # Super admin endpoints
|
||||
│ │ │ ├── __init__.py # Admin router aggregation
|
||||
│ │ │ ├── auth.py # Admin authentication
|
||||
│ │ │ ├── vendors.py # Vendor management (CRUD, bulk operations)
|
||||
│ │ │ ├── dashboard.py # Admin dashboard & statistics
|
||||
│ │ │ ├── users.py # User management across vendors
|
||||
│ │ │ ├── marketplace.py # System-wide marketplace monitoring
|
||||
│ │ │ ├── audit.py # Audit log endpoints
|
||||
│ │ │ ├── settings.py # Platform settings management
|
||||
│ │ │ ├── notifications.py # Admin notifications & alerts
|
||||
│ │ │ └── monitoring.py # Platform monitoring & health checks
|
||||
│ │ ├── vendor/ # Vendor-scoped endpoints
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── auth.py # Vendor team authentication
|
||||
│ │ │ ├── dashboard.py # Vendor dashboard & statistics
|
||||
│ │ │ ├── products.py # Vendor catalog management (Product table)
|
||||
│ │ │ ├── marketplace.py # Marketplace import & selection (MarketplaceProduct)
|
||||
│ │ │ ├── orders.py # Vendor order management
|
||||
│ │ │ ├── customers.py # Vendor customer management
|
||||
│ │ │ ├── teams.py # Team member management
|
||||
│ │ │ ├── inventory.py # Inventory operations (vendor catalog)
|
||||
│ │ │ ├── payments.py # Payment configuration & processing
|
||||
│ │ │ ├── media.py # File and media management
|
||||
│ │ │ ├── notifications.py # Vendor notification management
|
||||
│ │ │ └── settings.py # Vendor settings & configuration
|
||||
│ │ ├── public/ # Public customer-facing endpoints
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── vendors/ # Vendor-specific public APIs
|
||||
│ │ │ ├── shop.py # Public shop info
|
||||
│ │ │ ├── products.py # Public product catalog (Product table only)
|
||||
│ │ │ ├── search.py # Product search functionality
|
||||
│ │ │ ├── cart.py # Shopping cart operations
|
||||
│ │ │ ├── orders.py # Order placement
|
||||
│ │ │ ├── payments.py # Payment processing
|
||||
│ │ │ └── auth.py # Customer authentication
|
||||
│ │ └── shared/ # Shared/utility endpoints
|
||||
│ │ ├── health.py # Health checks
|
||||
│ │ ├── webhooks.py # External webhooks (Stripe, etc.)
|
||||
│ │ └── uploads.py # File upload handling
|
||||
│ ├── core/
|
||||
│ │ ├── config.py # Configuration settings
|
||||
│ │ ├── database.py # Database setup
|
||||
│ │ ├── security.py # Security utilities (JWT, passwords)
|
||||
│ │ └── lifespan.py # App lifecycle management
|
||||
│ ├── exceptions/ # Custom exception handling
|
||||
│ │ ├── __init__.py # All exception exports
|
||||
│ │ ├── base.py # Base exception classes
|
||||
│ │ ├── handler.py # Unified FastAPI exception handlers
|
||||
│ │ ├── auth.py # Authentication/authorization exceptions
|
||||
│ │ ├── admin.py # Admin operation exceptions
|
||||
│ │ ├── marketplace.py # Import/marketplace exceptions
|
||||
│ │ ├── marketplace_product.py # Marketplace staging exceptions
|
||||
│ │ ├── product.py # Vendor catalog exceptions
|
||||
│ │ ├── vendor.py # Vendor management exceptions
|
||||
│ │ ├── customer.py # Customer management exceptions
|
||||
│ │ ├── order.py # Order management exceptions
|
||||
│ │ ├── payment.py # Payment processing exceptions
|
||||
│ │ ├── inventory.py # Inventory management exceptions
|
||||
│ │ ├── media.py # Media/file management exceptions
|
||||
│ │ ├── notification.py # Notification exceptions
|
||||
│ │ ├── search.py # Search exceptions
|
||||
│ │ ├── monitoring.py # Monitoring exceptions
|
||||
│ │ └── backup.py # Backup/recovery exceptions
|
||||
│ └── services/ # Business logic layer
|
||||
│ ├── auth_service.py # Authentication/authorization services
|
||||
│ ├── admin_service.py # Admin services (vendor/user management)
|
||||
│ ├── admin_audit_service.py # Audit logging services
|
||||
│ ├── admin_settings_service.py # Platform settings services
|
||||
│ ├── vendor_service.py # Vendor management services
|
||||
│ ├── customer_service.py # Customer services (vendor-scoped)
|
||||
│ ├── team_service.py # Team management services
|
||||
│ ├── marketplace_service.py # Marketplace import services
|
||||
│ ├── marketplace_product_service.py # Marketplace staging services
|
||||
│ ├── product_service.py # Vendor catalog services (Product)
|
||||
│ ├── order_service.py # Order services (vendor-scoped)
|
||||
│ ├── payment_service.py # Payment processing services
|
||||
│ ├── inventory_service.py # Inventory services (vendor catalog)
|
||||
│ ├── media_service.py # File and media management services
|
||||
│ ├── notification_service.py # Email/notification services
|
||||
│ ├── search_service.py # Search and indexing services
|
||||
│ ├── cache_service.py # Caching services
|
||||
│ ├── monitoring_service.py # Application monitoring services
|
||||
│ ├── backup_service.py # Backup and recovery services
|
||||
│ ├── configuration_service.py # Configuration management services
|
||||
│ └── stats_service.py # Statistics services (vendor-aware)
|
||||
├── tasks/ # Background task processing
|
||||
│ ├── __init__.py
|
||||
│ ├── task_manager.py # Celery configuration and task management
|
||||
│ ├── marketplace_import.py # Marketplace CSV import tasks
|
||||
│ ├── email_tasks.py # Email sending tasks
|
||||
│ ├── media_processing.py # Image processing and optimization tasks
|
||||
│ ├── search_indexing.py # Search index maintenance tasks
|
||||
│ ├── analytics_tasks.py # Analytics and reporting tasks
|
||||
│ ├── cleanup_tasks.py # Data cleanup and maintenance tasks
|
||||
│ └── backup_tasks.py # Backup and recovery tasks
|
||||
├── models/
|
||||
│ ├── database/ # SQLAlchemy ORM models
|
||||
│ │ ├── __init__.py # Import all models for easy access
|
||||
│ │ ├── base.py # Base model class and common mixins (TimestampMixin)
|
||||
│ │ ├── user.py # User model (with vendor relationships)
|
||||
│ │ ├── vendor.py # Vendor, VendorUser, Role models
|
||||
│ │ ├── customer.py # Customer, CustomerAddress models (vendor-scoped)
|
||||
│ │ ├── marketplace_product.py # MarketplaceProduct model (staging data)
|
||||
│ │ ├── product.py # Product model (vendor catalog)
|
||||
│ │ ├── order.py # Order, OrderItem models (vendor-scoped)
|
||||
│ │ ├── payment.py # Payment, PaymentMethod, VendorPaymentConfig models
|
||||
│ │ ├── inventory.py # Inventory, InventoryMovement models (catalog products)
|
||||
│ │ ├── marketplace.py # MarketplaceImportJob model
|
||||
│ │ ├── media.py # MediaFile, ProductMedia models
|
||||
│ │ ├── notification.py # NotificationTemplate, NotificationQueue, NotificationLog
|
||||
│ │ ├── search.py # SearchIndex, SearchQuery models
|
||||
│ │ ├── audit.py # AuditLog, DataExportLog models
|
||||
│ │ ├── monitoring.py # PerformanceMetric, ErrorLog, SystemAlert models
|
||||
│ │ ├── backup.py # BackupLog, RestoreLog models
|
||||
│ │ ├── configuration.py # PlatformConfig, VendorConfig, FeatureFlag models
|
||||
│ │ ├── task.py # TaskLog model
|
||||
│ │ └── admin.py # Admin-specific models
|
||||
│ │ # - AdminAuditLog: Track all admin actions
|
||||
│ │ # - AdminNotification: System alerts for admins
|
||||
│ │ # - AdminSetting: Platform-wide settings
|
||||
│ │ # - PlatformAlert: System health alerts
|
||||
│ │ # - AdminSession: Admin login session tracking
|
||||
│ └── schema/ # Pydantic models for API validation
|
||||
│ ├── __init__.py # Common imports
|
||||
│ ├── base.py # Base Pydantic models
|
||||
│ ├── auth.py # Login, Token, User response models
|
||||
│ ├── vendor.py # Vendor management models
|
||||
│ ├── customer.py # Customer request/response models
|
||||
│ ├── team.py # Team management models
|
||||
│ ├── marketplace_product.py # Marketplace staging models
|
||||
│ ├── product.py # Vendor catalog models
|
||||
│ ├── order.py # Order models (vendor-scoped)
|
||||
│ ├── payment.py # Payment models
|
||||
│ ├── inventory.py # Inventory operation models
|
||||
│ ├── marketplace.py # Marketplace import job models
|
||||
│ ├── media.py # Media/file management models
|
||||
│ ├── notification.py # Notification models
|
||||
│ ├── search.py # Search models
|
||||
│ ├── monitoring.py # Monitoring models
|
||||
│ ├── admin.py # Admin operation models
|
||||
│ │ # - AdminAuditLog schemas (Response, Filters, List)
|
||||
│ │ # - AdminNotification schemas (Create, Response, Update, List)
|
||||
│ │ # - AdminSetting schemas (Create, Response, Update, List)
|
||||
│ │ # - PlatformAlert schemas (Create, Response, Resolve, List)
|
||||
│ │ # - BulkOperation schemas (BulkVendorAction, BulkUserAction)
|
||||
│ │ # - AdminDashboardStats, SystemHealthResponse
|
||||
│ │ # - AdminSession schemas
|
||||
│ └── stats.py # Statistics response models
|
||||
├── middleware/
|
||||
│ ├── auth.py # JWT authentication
|
||||
│ ├── vendor_context.py # Vendor context detection and injection
|
||||
│ ├── rate_limiter.py # Rate limiting
|
||||
│ ├── logging_middleware.py # Request logging
|
||||
│ └── decorators.py # Cross-cutting concern decorators
|
||||
├── storage/ # Storage backends
|
||||
│ ├── __init__.py
|
||||
│ ├── backends.py # Storage backend implementations
|
||||
│ └── utils.py # Storage utilities
|
||||
├── static/ # Frontend assets (No build step!)
|
||||
│ ├── admin/ # Super admin interface
|
||||
│ │ ├── login.html # Admin login page
|
||||
│ │ ├── dashboard.html # Admin dashboard
|
||||
│ │ ├── vendors.html # Vendor management
|
||||
│ │ ├── users.html # User management
|
||||
│ │ ├── marketplace.html # System-wide marketplace monitoring
|
||||
│ │ ├── audit_logs.html # Audit log viewer (NEW - TO CREATE)
|
||||
│ │ ├── settings.html # Platform settings (NEW - TO CREATE)
|
||||
│ │ ├── notifications.html # Admin notifications (NEW - TO CREATE)
|
||||
│ │ └── monitoring.html # System monitoring
|
||||
│ ├── vendor/ # Vendor admin interface
|
||||
│ │ ├── login.html # Vendor team login (TO COMPLETE)
|
||||
│ │ ├── dashboard.html # Vendor dashboard (TO COMPLETE)
|
||||
│ │ └── admin/ # Vendor admin pages
|
||||
│ │ ├── products.html # Catalog management (Product table)
|
||||
│ │ ├── marketplace/ # Marketplace integration
|
||||
│ │ │ ├── imports.html # Import jobs & history
|
||||
│ │ │ ├── browse.html # Browse marketplace products (staging)
|
||||
│ │ │ ├── selected.html # Selected products (pre-publish)
|
||||
│ │ │ └── config.html # Marketplace configuration
|
||||
│ │ ├── orders.html # Order management
|
||||
│ │ ├── customers.html # Customer management
|
||||
│ │ ├── teams.html # Team management
|
||||
│ │ ├── inventory.html # Inventory management (catalog products)
|
||||
│ │ ├── payments.html # Payment configuration
|
||||
│ │ ├── media.html # Media library
|
||||
│ │ ├── notifications.html # Notification templates & logs
|
||||
│ │ └── settings.html # Vendor settings
|
||||
│ ├── shop/ # Customer-facing shop interface
|
||||
│ │ ├── home.html # Shop homepage
|
||||
│ │ ├── products.html # Product catalog (Product table only)
|
||||
│ │ ├── product.html # Product detail page
|
||||
│ │ ├── search.html # Search results page
|
||||
│ │ ├── cart.html # Shopping cart
|
||||
│ │ ├── checkout.html # Checkout process
|
||||
│ │ └── account/ # Customer account pages
|
||||
│ │ ├── login.html # Customer login
|
||||
│ │ ├── register.html # Customer registration
|
||||
│ │ ├── profile.html # Customer profile
|
||||
│ │ ├── orders.html # Order history
|
||||
│ │ └── addresses.html # Address management
|
||||
│ ├── css/
|
||||
│ │ ├── admin/ # Admin interface styles
|
||||
│ │ │ └── admin.css
|
||||
│ │ ├── vendor/ # Vendor interface styles
|
||||
│ │ │ └── vendor.css
|
||||
│ │ ├── shop/ # Customer shop styles
|
||||
│ │ │ └── shop.css
|
||||
│ │ ├── shared/ # Common styles (base.css, auth.css)
|
||||
│ │ │ ├── base.css # CSS variables, utility classes
|
||||
│ │ │ └── auth.css # Login/auth page styles
|
||||
│ │ └── themes/ # Vendor-specific themes (future)
|
||||
│ └── js/
|
||||
│ ├── shared/ # Common JavaScript utilities
|
||||
│ │ ├── vendor-context.js # Vendor context detection & management
|
||||
│ │ ├── api-client.js # API communication utilities
|
||||
│ │ ├── notification.js # Notification handling
|
||||
│ │ ├── media-upload.js # File upload utilities
|
||||
│ │ └── utils.js # General utilities
|
||||
│ ├── admin/ # Admin interface scripts (Alpine.js components)
|
||||
│ │ ├── dashboard.js # Admin dashboard
|
||||
│ │ ├── vendors.js # Vendor management
|
||||
│ │ ├── audit-logs.js # Audit log viewer (NEW - TO CREATE)
|
||||
│ │ ├── settings.js # Platform settings (NEW - TO CREATE)
|
||||
│ │ ├── monitoring.js # System monitoring
|
||||
│ │ └── analytics.js # Admin analytics
|
||||
│ ├── vendor/ # Vendor interface scripts (Alpine.js components)
|
||||
│ │ ├── products.js # Catalog management
|
||||
│ │ ├── marketplace.js # Marketplace integration
|
||||
│ │ ├── orders.js # Order management
|
||||
│ │ ├── payments.js # Payment configuration
|
||||
│ │ ├── media.js # Media management
|
||||
│ │ └── dashboard.js # Vendor dashboard
|
||||
│ └── shop/ # Customer shop scripts (Alpine.js components)
|
||||
│ ├── catalog.js # Product browsing
|
||||
│ ├── search.js # Product search
|
||||
│ ├── cart.js # Shopping cart
|
||||
│ ├── checkout.js # Checkout process
|
||||
│ └── account.js # Customer account
|
||||
├── tests/ # Comprehensive test suite
|
||||
│ ├── unit/ # Unit tests
|
||||
│ │ ├── services/ # Service layer tests
|
||||
│ │ │ ├── test_admin_service.py
|
||||
│ │ │ ├── test_admin_audit_service.py # NEW
|
||||
│ │ │ ├── test_admin_settings_service.py # NEW
|
||||
│ │ │ ├── test_marketplace_service.py
|
||||
│ │ │ ├── test_product_service.py
|
||||
│ │ │ ├── test_payment_service.py
|
||||
│ │ │ ├── test_notification_service.py
|
||||
│ │ │ ├── test_search_service.py
|
||||
│ │ │ ├── test_media_service.py
|
||||
│ │ │ └── test_cache_service.py
|
||||
│ │ ├── models/ # Model tests
|
||||
│ │ │ ├── test_admin_models.py # NEW
|
||||
│ │ │ ├── test_marketplace_product.py
|
||||
│ │ │ ├── test_product.py
|
||||
│ │ │ ├── test_payment.py
|
||||
│ │ │ └── test_vendor.py
|
||||
│ │ └── api/ # API endpoint tests
|
||||
│ │ ├── test_admin_api.py
|
||||
│ │ ├── test_admin_audit_api.py # NEW
|
||||
│ │ ├── test_admin_settings_api.py # NEW
|
||||
│ │ ├── test_vendor_api.py
|
||||
│ │ └── test_public_api.py
|
||||
│ ├── integration/ # Integration tests
|
||||
│ │ ├── test_marketplace_workflow.py
|
||||
│ │ ├── test_order_workflow.py
|
||||
│ │ ├── test_payment_workflow.py
|
||||
│ │ ├── test_audit_workflow.py # NEW
|
||||
│ │ └── test_notification_workflow.py
|
||||
│ ├── e2e/ # End-to-end tests
|
||||
│ │ ├── test_vendor_onboarding.py
|
||||
│ │ ├── test_customer_journey.py
|
||||
│ │ └── test_admin_operations.py
|
||||
│ └── fixtures/ # Test data fixtures
|
||||
│ ├── marketplace_data.py # Sample marketplace import data
|
||||
│ ├── catalog_data.py # Sample vendor catalog data
|
||||
│ ├── order_data.py # Sample order data
|
||||
│ └── user_data.py # Sample user and vendor data
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── init_db.py # Database initialization
|
||||
│ ├── create_admin.py # Create initial admin user
|
||||
│ ├── init_platform_settings.py # Create default platform settings
|
||||
│ ├── backup_database.py # Manual backup script
|
||||
│ ├── seed_data.py # Development data seeding
|
||||
│ └── migrate_data.py # Data migration utilities
|
||||
├── docker/ # Docker configuration
|
||||
│ ├── Dockerfile # Application container
|
||||
│ ├── docker-compose.yml # Development environment
|
||||
│ ├── docker-compose.prod.yml # Production environment
|
||||
│ └── nginx.conf # Nginx configuration
|
||||
├── docs/ # Documentation
|
||||
│ ├── slices/ # Vertical slice documentation
|
||||
│ │ ├── 00_slices_overview.md
|
||||
│ │ ├── 00_implementation_roadmap.md
|
||||
│ │ ├── 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
|
||||
│ ├── api/ # API documentation
|
||||
│ ├── deployment/ # Deployment guides
|
||||
│ ├── development/ # Development setup
|
||||
│ ├── user_guides/ # User manuals
|
||||
│ ├── 6.complete_naming_convention.md
|
||||
│ ├── 10.stripe_payment_integration.md
|
||||
│ ├── 12.project_readme_final.md
|
||||
│ ├── 13.updated_application_workflows_final.md
|
||||
│ └── 14.updated_complete_project_structure_final.md
|
||||
├── .env.example # Environment variables template
|
||||
├── requirements.txt # Python dependencies
|
||||
├── requirements-dev.txt # Development dependencies
|
||||
├── README.md # Project documentation
|
||||
└── DEPLOYMENT.md # Deployment instructions
|
||||
```
|
||||
|
||||
## Key Changes from Previous Version
|
||||
|
||||
### Admin Infrastructure
|
||||
|
||||
**Database Models** (`models/database/admin.py`):
|
||||
- ✅ `AdminAuditLog` - Complete audit trail of all admin actions
|
||||
- ✅ `AdminNotification` - System notifications for admins
|
||||
- ✅ `AdminSetting` - Platform-wide configuration with encryption support
|
||||
- ✅ `PlatformAlert` - System health and issue tracking
|
||||
- ✅ `AdminSession` - Admin login session tracking
|
||||
|
||||
**Pydantic Schemas** (`models/schema/admin.py`):
|
||||
- ✅ Comprehensive request/response models for all admin operations
|
||||
- ✅ Filtering and pagination schemas
|
||||
- ✅ Bulk operation schemas (vendor/user actions)
|
||||
- ✅ System health monitoring schemas
|
||||
|
||||
**Services**:
|
||||
- ✅ `admin_audit_service.py` - Audit logging functionality
|
||||
- ✅ `admin_settings_service.py` - Platform settings with type conversion
|
||||
|
||||
**API Endpoints**:
|
||||
- ✅ `/api/v1/admin/audit/*` - Audit log querying and filtering
|
||||
- ✅ `/api/v1/admin/settings/*` - Settings CRUD operations
|
||||
- ✅ `/api/v1/admin/notifications/*` - Notifications & alerts (structure ready)
|
||||
|
||||
### Naming Convention Fixes
|
||||
- ✅ Changed `models/schemas/` to `models/schema/` (singular)
|
||||
- ✅ All schema files now consistently use singular naming
|
||||
|
||||
### Current Development Status (Slice 1)
|
||||
|
||||
**Completed (✅)**:
|
||||
- Backend database models (User, Vendor, Role, VendorUser, Admin models)
|
||||
- JWT authentication with bcrypt
|
||||
- Admin service layer with audit logging capability
|
||||
- Admin API endpoints (CRUD, dashboard, audit, settings)
|
||||
- Vendor context middleware
|
||||
- Admin login page (Alpine.js)
|
||||
- Admin dashboard (Alpine.js)
|
||||
- Admin vendor creation page (Alpine.js)
|
||||
|
||||
**In Progress (⏳)**:
|
||||
- Vendor login page (frontend)
|
||||
- Vendor dashboard page (frontend)
|
||||
- Admin audit logs page (frontend)
|
||||
- Admin platform settings page (frontend)
|
||||
|
||||
**To Do (📋)**:
|
||||
- Complete vendor login/dashboard pages
|
||||
- Integrate audit logging into existing admin operations
|
||||
- Create admin audit logs frontend
|
||||
- Create platform settings frontend
|
||||
- Full testing of Slice 1
|
||||
- Deployment to staging
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### Multi-Tenancy Model
|
||||
- **Complete Vendor Isolation**: Each vendor operates independently with no data sharing
|
||||
- **Chinese Wall**: Strict separation between vendor data and operations
|
||||
- **Self-Service**: Vendors manage their own teams, products, and marketplace integrations
|
||||
- **Scalable**: Single codebase serves all vendors with vendor-specific customization
|
||||
|
||||
### Data Flow Architecture
|
||||
```
|
||||
Marketplace CSV → MarketplaceProduct (staging) → Product (catalog) → Order → Analytics
|
||||
↓
|
||||
Admin Audit Logs → Platform Settings → Notifications → Monitoring
|
||||
```
|
||||
|
||||
### API Structure
|
||||
- **Admin APIs** (`/api/v1/admin/`): Platform-level administration
|
||||
- Vendor management, user management, audit logs, settings, alerts
|
||||
- **Vendor APIs** (`/api/v1/vendor/`): Vendor-scoped operations (requires vendor context)
|
||||
- Products, orders, customers, teams, marketplace integration
|
||||
- **Public APIs** (`/api/v1/public/vendors/{vendor_id}/`): Customer-facing operations
|
||||
- Shop, products, cart, orders, checkout
|
||||
- **Shared APIs** (`/api/v1/shared/`): Utility endpoints (health, webhooks)
|
||||
|
||||
### Service Layer Architecture
|
||||
- **Domain Services**: Each service handles one business domain
|
||||
- **Cross-Cutting Services**: Audit, cache, monitoring, notifications
|
||||
- **Integration Services**: Payments, search, storage
|
||||
|
||||
### Frontend Architecture
|
||||
- **Zero Build Step**: Alpine.js from CDN, vanilla CSS, no compilation
|
||||
- **Server-Side Rendering**: Jinja2 templates with Alpine.js enhancement
|
||||
- **Context-Aware**: Automatic vendor detection via subdomain or path
|
||||
- **Dynamic Theming**: Vendor-specific customization via CSS variables
|
||||
- **Role-Based UI**: Permission-driven interface elements
|
||||
|
||||
## Key Features
|
||||
|
||||
### Core Business Features
|
||||
- Multi-vendor marketplace with complete isolation
|
||||
- Marketplace product import and curation workflow
|
||||
- Comprehensive order and payment processing
|
||||
- Customer management with vendor-scoped accounts
|
||||
- Team management with role-based permissions
|
||||
- Inventory tracking and management
|
||||
|
||||
### Advanced Features
|
||||
- Email notification system with vendor branding
|
||||
- File and media management with CDN integration
|
||||
- Advanced search with Elasticsearch
|
||||
- Multi-layer caching for performance
|
||||
- Comprehensive audit logging for compliance
|
||||
- Real-time monitoring and alerting
|
||||
- Automated backup and disaster recovery
|
||||
- Configuration management with feature flags
|
||||
|
||||
### Security Features
|
||||
- JWT-based authentication with vendor context
|
||||
- Role-based access control with granular permissions
|
||||
- Complete vendor data isolation at all layers
|
||||
- Audit trails for all operations
|
||||
- Secure file storage with access controls
|
||||
- Rate limiting and abuse prevention
|
||||
|
||||
### Integration Capabilities
|
||||
- Stripe Connect for multi-vendor payments
|
||||
- Marketplace integrations (Letzshop with extensible framework)
|
||||
- Multiple storage backends (Local, S3, GCS)
|
||||
- Search engines (Elasticsearch with database fallback)
|
||||
- Email providers (SendGrid, SMTP)
|
||||
- Monitoring and alerting systems
|
||||
|
||||
## Technology Integration
|
||||
|
||||
### Database Layer
|
||||
- **PostgreSQL**: Primary database with ACID compliance (SQLite for development)
|
||||
- **Redis**: Caching and session storage
|
||||
- **Elasticsearch**: Search and analytics (optional)
|
||||
|
||||
### Background Processing
|
||||
- **Celery**: Distributed task processing
|
||||
- **Redis**: Message broker and result backend
|
||||
- **Monitoring**: Task progress and error tracking
|
||||
|
||||
### External Services
|
||||
- **Stripe Connect**: Multi-vendor payment processing
|
||||
- **SendGrid/SMTP**: Email delivery with vendor branding
|
||||
- **AWS S3/GCS**: Cloud storage for media files
|
||||
- **CloudFront/CloudFlare**: CDN for static assets
|
||||
|
||||
### Monitoring & Compliance
|
||||
- **Admin Audit Logs**: Complete trail of all admin actions
|
||||
- **Platform Settings**: Centralized configuration management
|
||||
- **System Alerts**: Automated health monitoring
|
||||
- **Error Tracking**: Comprehensive error logging
|
||||
|
||||
## Deployment Modes
|
||||
|
||||
### Development Mode
|
||||
- **Path-based routing**: `localhost:8000/vendor/vendorname/`
|
||||
- **Admin access**: `localhost:8000/admin/`
|
||||
- **Easy context switching**: Clear vendor separation in URLs
|
||||
- **Local storage**: Files stored locally with hot reload
|
||||
|
||||
### Production Mode
|
||||
- **Subdomain routing**: `vendorname.platform.com`
|
||||
- **Admin subdomain**: `admin.platform.com`
|
||||
- **Custom domains**: `customdomain.com` (enterprise feature)
|
||||
- **CDN integration**: Optimized asset delivery
|
||||
- **Distributed caching**: Redis cluster for performance
|
||||
- **Automated backups**: Scheduled database and file backups
|
||||
|
||||
This structure provides a robust foundation for a scalable, multi-tenant ecommerce platform with enterprise-grade features while maintaining clean separation of concerns and supporting multiple deployment modes.
|
||||
392
docs/__temp/6.complete_naming_convention.md
Normal file
392
docs/__temp/6.complete_naming_convention.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# Multi-Tenant Ecommerce Platform - Complete Naming Convention Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document establishes consistent naming conventions across the entire multi-tenant ecommerce platform. Consistent naming improves code readability, reduces developer confusion, and ensures maintainable architecture.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Context-Based Naming
|
||||
- **Collections/Endpoints**: Use PLURAL (handle multiple items)
|
||||
- **Entities/Models**: Use SINGULAR (represent individual items)
|
||||
- **Domains/Services**: Use SINGULAR (focus on one domain area)
|
||||
|
||||
### 2. Terminology Standardization
|
||||
- Use **"inventory"** not "stock" (more business-friendly)
|
||||
- Use **"vendor"** not "shop" (multi-tenant architecture)
|
||||
- Use **"customer"** not "user" for end customers (clarity)
|
||||
|
||||
### 3. File Naming Patterns
|
||||
- **API files**: `entities.py` (plural)
|
||||
- **Model files**: `entity.py` (singular)
|
||||
- **Service files**: `entity_service.py` (singular + service)
|
||||
- **Exception files**: `entity.py` (singular domain)
|
||||
|
||||
## Detailed Naming Rules
|
||||
|
||||
### API Endpoint Files (PLURAL)
|
||||
**Rule**: API files handle collections of resources, so use plural names.
|
||||
|
||||
**Location**: `app/api/v1/*/`
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
app/api/v1/admin/
|
||||
├── vendors.py # Handles multiple vendors
|
||||
├── users.py # Handles multiple users
|
||||
└── dashboard.py # Exception: not a resource collection
|
||||
|
||||
app/api/v1/vendor/
|
||||
├── products.py # Handles vendor's products
|
||||
├── orders.py # Handles vendor's orders
|
||||
├── customers.py # Handles vendor's customers
|
||||
├── teams.py # Handles team members
|
||||
├── inventory.py # Handles inventory items
|
||||
└── settings.py # Exception: not a resource collection
|
||||
|
||||
app/api/v1/public/vendors/
|
||||
├── products.py # Public product catalog
|
||||
├── orders.py # Order placement
|
||||
└── auth.py # Exception: authentication service
|
||||
```
|
||||
|
||||
**Rationale**: REST endpoints typically operate on collections (`GET /products`, `POST /orders`).
|
||||
|
||||
### Database Model Files (SINGULAR)
|
||||
**Rule**: Model files represent individual entity definitions, so use singular names.
|
||||
|
||||
**Location**: `models/database/`
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
models/database/
|
||||
├── user.py # User, UserProfile classes
|
||||
├── vendor.py # Vendor, VendorUser, Role classes
|
||||
├── customer.py # Customer, CustomerAddress classes
|
||||
├── product.py # Product, ProductVariant classes
|
||||
├── order.py # Order, OrderItem classes
|
||||
├── inventory.py # Inventory, InventoryMovement classes
|
||||
├── marketplace.py # MarketplaceImportJob class
|
||||
└── admin.py # Admin-specific models
|
||||
```
|
||||
|
||||
**Class Names Within Files**:
|
||||
```python
|
||||
# models/database/product.py
|
||||
class Product(Base): # Singular
|
||||
class ProductVariant(Base): # Singular
|
||||
|
||||
# models/database/inventory.py
|
||||
class Inventory(Base): # Singular
|
||||
class InventoryMovement(Base): # Singular
|
||||
```
|
||||
|
||||
**Rationale**: Each model class represents a single entity instance in the database.
|
||||
|
||||
### Schema/Pydantic Model Files (SINGULAR)
|
||||
**Rule**: Schema files define validation for individual entities, so use singular names.
|
||||
|
||||
**Location**: `models/schema/`
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
models/schema/
|
||||
├── user.py # UserCreate, UserResponse classes
|
||||
├── vendor.py # VendorCreate, VendorResponse classes
|
||||
├── customer.py # CustomerCreate, CustomerResponse classes
|
||||
├── product.py # ProductCreate, ProductResponse classes
|
||||
├── order.py # OrderCreate, OrderResponse classes
|
||||
├── inventory.py # InventoryCreate, InventoryResponse classes
|
||||
├── marketplace.py # MarketplaceImportRequest class
|
||||
└── admin.py # Admin operation schemas
|
||||
```
|
||||
|
||||
**Class Names Within Files**:
|
||||
```python
|
||||
# models/schema/product.py
|
||||
class ProductCreate(BaseModel): # Singular entity
|
||||
class ProductUpdate(BaseModel): # Singular entity
|
||||
class ProductResponse(BaseModel): # Singular entity
|
||||
```
|
||||
|
||||
**Rationale**: Schema models validate individual entity data structures.
|
||||
|
||||
### Service Files (SINGULAR + "service")
|
||||
**Rule**: Service files handle business logic for one domain area, so use singular + "service".
|
||||
|
||||
**Location**: `services/`
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
services/
|
||||
├── auth_service.py # Authentication domain
|
||||
├── admin_service.py # Admin operations domain
|
||||
├── vendor_service.py # Vendor management domain
|
||||
├── customer_service.py # Customer operations domain
|
||||
├── team_service.py # Team management domain
|
||||
├── product_service.py # Product operations domain
|
||||
├── order_service.py # Order operations domain
|
||||
├── inventory_service.py # Inventory operations domain
|
||||
├── marketplace_service.py # Marketplace integration domain
|
||||
└── stats_service.py # Statistics domain
|
||||
```
|
||||
|
||||
**Class Names Within Files**:
|
||||
```python
|
||||
# services/product_service.py
|
||||
class ProductService: # Singular domain focus
|
||||
def create_product() # Operates on single product
|
||||
def get_products() # Can return multiple, but service is singular
|
||||
```
|
||||
|
||||
**Rationale**: Each service focuses on one business domain area.
|
||||
|
||||
### Exception Files (SINGULAR)
|
||||
**Rule**: Exception files handle errors for one domain area, so use singular names.
|
||||
|
||||
**Location**: `app/exceptions/`
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
app/exceptions/
|
||||
├── base.py # Base exception classes
|
||||
├── handler.py # Exception handlers
|
||||
├── auth.py # Authentication domain exceptions
|
||||
├── admin.py # Admin domain exceptions
|
||||
├── vendor.py # Vendor domain exceptions
|
||||
├── customer.py # Customer domain exceptions
|
||||
├── product.py # Product domain exceptions
|
||||
├── order.py # Order domain exceptions
|
||||
├── inventory.py # Inventory domain exceptions
|
||||
└── marketplace.py # Marketplace domain exceptions
|
||||
```
|
||||
|
||||
**Class Names Within Files**:
|
||||
```python
|
||||
# app/exceptions/product.py
|
||||
class ProductNotFoundException(ResourceNotFoundException):
|
||||
class ProductAlreadyExistsException(ConflictException):
|
||||
class ProductValidationException(ValidationException):
|
||||
```
|
||||
|
||||
**Rationale**: Exception files are domain-focused, not collection-focused.
|
||||
|
||||
### Middleware Files (DESCRIPTIVE)
|
||||
**Rule**: Middleware files use descriptive names based on their function.
|
||||
|
||||
**Location**: `middleware/`
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
middleware/
|
||||
├── auth.py # Authentication middleware
|
||||
├── vendor_context.py # Vendor context detection
|
||||
├── rate_limiter.py # Rate limiting functionality
|
||||
├── logging_middleware.py # Request logging
|
||||
└── decorators.py # Cross-cutting decorators
|
||||
```
|
||||
|
||||
**Rationale**: Middleware serves specific cross-cutting functions.
|
||||
|
||||
### Frontend Files
|
||||
**Rule**: Frontend files use context-appropriate naming.
|
||||
|
||||
**Location**: `frontend/`
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
frontend/
|
||||
├── admin/
|
||||
│ ├── vendors.html # PLURAL - lists multiple vendors
|
||||
│ ├── users.html # PLURAL - lists multiple users
|
||||
│ └── dashboard.html # SINGULAR - one dashboard
|
||||
├── vendor/admin/
|
||||
│ ├── products.html # PLURAL - lists multiple products
|
||||
│ ├── orders.html # PLURAL - lists multiple orders
|
||||
│ ├── teams.html # PLURAL - lists team members
|
||||
│ └── dashboard.html # SINGULAR - one dashboard
|
||||
└── shop/
|
||||
├── products.html # PLURAL - product catalog
|
||||
├── product.html # SINGULAR - single product detail
|
||||
├── orders.html # PLURAL - order history
|
||||
└── cart.html # SINGULAR - one shopping cart
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
- List views are plural (show collections)
|
||||
- Detail views are singular (show individual items)
|
||||
- Functional views use descriptive names
|
||||
|
||||
## Terminology Standards
|
||||
|
||||
### Core Business Terms
|
||||
|
||||
| Use This | Not This | Context |
|
||||
|----------|----------|---------|
|
||||
| inventory | stock | All inventory management |
|
||||
| vendor | shop | Multi-tenant architecture |
|
||||
| customer | user | End customers (buyers) |
|
||||
| user | member | Platform/vendor team members |
|
||||
| team | staff | Vendor team members |
|
||||
| order | purchase | Customer orders |
|
||||
| product | item | Catalog products |
|
||||
|
||||
### Database Naming
|
||||
|
||||
**Table Names**: Use singular, lowercase with underscores
|
||||
```sql
|
||||
-- Correct
|
||||
inventory
|
||||
inventory_movements
|
||||
vendor_users
|
||||
|
||||
-- Incorrect
|
||||
inventories
|
||||
inventory_movement
|
||||
vendorusers
|
||||
```
|
||||
|
||||
**Column Names**: Use singular, descriptive names
|
||||
```sql
|
||||
-- Correct
|
||||
vendor_id
|
||||
inventory_level
|
||||
created_at
|
||||
|
||||
-- Incorrect
|
||||
vendors_id
|
||||
inventory_levels
|
||||
creation_time
|
||||
```
|
||||
|
||||
### API Endpoint Patterns
|
||||
|
||||
**Resource Collections**: Use plural nouns
|
||||
```
|
||||
GET /api/v1/vendor/products # List products
|
||||
POST /api/v1/vendor/products # Create product
|
||||
GET /api/v1/vendor/orders # List orders
|
||||
POST /api/v1/vendor/orders # Create order
|
||||
```
|
||||
|
||||
**Individual Resources**: Use singular in URL structure
|
||||
```
|
||||
GET /api/v1/vendor/products/{id} # Get single product
|
||||
PUT /api/v1/vendor/products/{id} # Update single product
|
||||
DELETE /api/v1/vendor/products/{id} # Delete single product
|
||||
```
|
||||
|
||||
**Non-Resource Endpoints**: Use descriptive names
|
||||
```
|
||||
GET /api/v1/vendor/dashboard/stats # Dashboard statistics
|
||||
POST /api/v1/vendor/auth/login # Authentication
|
||||
GET /api/v1/vendor/settings # Vendor settings
|
||||
```
|
||||
|
||||
## Variable and Function Naming
|
||||
|
||||
### Function Names
|
||||
```python
|
||||
# Correct - verb + singular object
|
||||
def create_product()
|
||||
def get_customer()
|
||||
def update_order()
|
||||
def delete_inventory_item()
|
||||
|
||||
# Correct - verb + plural when operating on collections
|
||||
def get_products()
|
||||
def list_customers()
|
||||
def bulk_update_orders()
|
||||
|
||||
# Incorrect
|
||||
def create_products() # Creates one product
|
||||
def get_customers() # Gets one customer
|
||||
```
|
||||
|
||||
### Variable Names
|
||||
```python
|
||||
# Correct - context-appropriate singular/plural
|
||||
product = get_product(id)
|
||||
products = get_products()
|
||||
customer_list = get_all_customers()
|
||||
inventory_count = len(inventory_items)
|
||||
|
||||
# Incorrect
|
||||
products = get_product(id) # Single item, should be singular
|
||||
product = get_products() # Multiple items, should be plural
|
||||
```
|
||||
|
||||
### Class Attributes
|
||||
```python
|
||||
# Correct - descriptive and consistent
|
||||
class Vendor:
|
||||
id: int
|
||||
name: str
|
||||
subdomain: str
|
||||
owner_user_id: int # Singular reference
|
||||
created_at: datetime
|
||||
|
||||
class Customer:
|
||||
vendor_id: int # Belongs to one vendor
|
||||
total_orders: int # Aggregate count
|
||||
last_order_date: datetime # Most recent
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When applying these naming conventions to existing code:
|
||||
|
||||
### File Renames Required
|
||||
- [ ] `app/api/v1/stock.py` → `app/api/v1/inventory.py`
|
||||
- [ ] `models/database/stock.py` → `models/database/inventory.py`
|
||||
- [ ] `models/schema/stock.py` → `models/schema/inventory.py`
|
||||
- [ ] `services/stock_service.py` → `services/inventory_service.py`
|
||||
- [ ] `app/exceptions/stock.py` → `app/exceptions/inventory.py`
|
||||
|
||||
### Import Statement Updates
|
||||
- [ ] Update all `from models.database.stock import` statements
|
||||
- [ ] Update all `from services.stock_service import` statements
|
||||
- [ ] Update all `stock_` variable prefixes to `inventory_`
|
||||
|
||||
### Class Name Updates
|
||||
- [ ] `Stock` → `Inventory`
|
||||
- [ ] `StockMovement` → `InventoryMovement`
|
||||
- [ ] `StockService` → `InventoryService`
|
||||
|
||||
### Database Updates
|
||||
- [ ] Rename `stock` table to `inventory`
|
||||
- [ ] Rename `stock_movements` table to `inventory_movements`
|
||||
- [ ] Update all `stock_id` foreign keys to `inventory_id`
|
||||
|
||||
### Frontend Updates
|
||||
- [ ] Update all HTML files with stock terminology
|
||||
- [ ] Update JavaScript variable names
|
||||
- [ ] Update CSS class names if applicable
|
||||
|
||||
## Benefits of Consistent Naming
|
||||
|
||||
1. **Developer Productivity**: Predictable file locations and naming patterns
|
||||
2. **Code Readability**: Clear understanding of file purposes and contents
|
||||
3. **Team Communication**: Shared vocabulary and terminology
|
||||
4. **Maintenance**: Easier to locate and update related functionality
|
||||
5. **Onboarding**: New developers quickly understand the codebase structure
|
||||
6. **Documentation**: Consistent terminology across all documentation
|
||||
7. **API Usability**: Predictable and intuitive API endpoint structures
|
||||
|
||||
## Enforcement
|
||||
|
||||
### Code Review Checklist
|
||||
- [ ] File names follow singular/plural conventions
|
||||
- [ ] Class names use appropriate terminology (inventory vs stock)
|
||||
- [ ] API endpoints use plural resource names
|
||||
- [ ] Database models use singular names
|
||||
- [ ] Variables names match their content (singular vs plural)
|
||||
|
||||
### Automated Checks
|
||||
Consider implementing linting rules or pre-commit hooks to enforce:
|
||||
- File naming patterns
|
||||
- Import statement consistency
|
||||
- Variable naming conventions
|
||||
- API endpoint patterns
|
||||
|
||||
This naming convention guide ensures consistent, maintainable, and intuitive code across the entire multi-tenant ecommerce platform.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Recommended Statistics Service Architecture
|
||||
|
||||
## Principle: Single Responsibility
|
||||
|
||||
**stats_service** should handle ALL statistics - it's the single source of truth for metrics.
|
||||
**admin_service** should handle admin operations - user management, vendor management, etc.
|
||||
1799
docs/__temp/BACKEND/ADMIN_FEATURE_INTEGRATION_GUIDE.md
Normal file
1799
docs/__temp/BACKEND/ADMIN_FEATURE_INTEGRATION_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
649
docs/__temp/BACKEND/admin_integration_guide.md
Normal file
649
docs/__temp/BACKEND/admin_integration_guide.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# Admin Models Integration Guide
|
||||
|
||||
## What We've Added
|
||||
|
||||
You now have:
|
||||
|
||||
1. **Database Models** (`models/database/admin.py`):
|
||||
- `AdminAuditLog` - Track all admin actions
|
||||
- `AdminNotification` - System alerts for admins
|
||||
- `AdminSetting` - Platform-wide settings
|
||||
- `PlatformAlert` - System health alerts
|
||||
- `AdminSession` - Track admin login sessions
|
||||
|
||||
2. **Pydantic Schemas** (`models/schemas/admin.py`):
|
||||
- Request/response models for all admin operations
|
||||
- Validation for bulk operations
|
||||
- System health check schemas
|
||||
|
||||
3. **Services**:
|
||||
- `AdminAuditService` - Audit logging operations
|
||||
- `AdminSettingsService` - Platform settings management
|
||||
|
||||
4. **API Endpoints**:
|
||||
- `/api/v1/admin/audit` - Audit log endpoints
|
||||
- `/api/v1/admin/settings` - Settings management
|
||||
- `/api/v1/admin/notifications` - Notifications & alerts (stubs)
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Integration
|
||||
|
||||
### Step 1: Update Database
|
||||
|
||||
Add the new models to your database imports:
|
||||
|
||||
```python
|
||||
# models/database/__init__.py
|
||||
from .admin import (
|
||||
AdminAuditLog,
|
||||
AdminNotification,
|
||||
AdminSetting,
|
||||
PlatformAlert,
|
||||
AdminSession
|
||||
)
|
||||
```
|
||||
|
||||
Run database migration:
|
||||
```bash
|
||||
# Create migration
|
||||
alembic revision --autogenerate -m "Add admin models"
|
||||
|
||||
# Apply migration
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### Step 2: Update Admin API Router
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/__init__.py
|
||||
from fastapi import APIRouter
|
||||
from . import auth, vendors, users, dashboard, marketplace, audit, settings, notifications
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
# Include all admin routers
|
||||
router.include_router(auth.router)
|
||||
router.include_router(vendors.router)
|
||||
router.include_router(users.router)
|
||||
router.include_router(dashboard.router)
|
||||
router.include_router(marketplace.router)
|
||||
router.include_router(audit.router) # NEW
|
||||
router.include_router(settings.router) # NEW
|
||||
router.include_router(notifications.router) # NEW
|
||||
```
|
||||
|
||||
### Step 3: Add Audit Logging to Existing Admin Operations
|
||||
|
||||
Update your `admin_service.py` to log actions:
|
||||
|
||||
```python
|
||||
# app/services/admin_service.py
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
|
||||
class AdminService:
|
||||
|
||||
def create_vendor_with_owner(
|
||||
self, db: Session, vendor_data: VendorCreate
|
||||
) -> Tuple[Vendor, User, str]:
|
||||
"""Create vendor with owner user account."""
|
||||
|
||||
# ... existing code ...
|
||||
|
||||
vendor, owner_user, temp_password = # ... your creation logic
|
||||
|
||||
# LOG THE ACTION
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin_id, # You'll need to pass this
|
||||
action="create_vendor",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor.id),
|
||||
details={
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"owner_email": owner_user.email
|
||||
}
|
||||
)
|
||||
|
||||
return vendor, owner_user, temp_password
|
||||
|
||||
def toggle_vendor_status(
|
||||
self, db: Session, vendor_id: int, admin_user_id: int
|
||||
) -> Tuple[Vendor, str]:
|
||||
"""Toggle vendor status with audit logging."""
|
||||
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
old_status = vendor.is_active
|
||||
|
||||
# ... toggle logic ...
|
||||
|
||||
# LOG THE ACTION
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=admin_user_id,
|
||||
action="toggle_vendor_status",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor_id),
|
||||
details={
|
||||
"old_status": "active" if old_status else "inactive",
|
||||
"new_status": "active" if vendor.is_active else "inactive"
|
||||
}
|
||||
)
|
||||
|
||||
return vendor, message
|
||||
```
|
||||
|
||||
### Step 4: Update API Endpoints to Pass Admin User ID
|
||||
|
||||
Your API endpoints need to pass the current admin's ID to service methods:
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/vendors.py
|
||||
|
||||
@router.post("", response_model=VendorResponse)
|
||||
def create_vendor_with_owner(
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Create vendor with audit logging."""
|
||||
|
||||
vendor, owner_user, temp_password = admin_service.create_vendor_with_owner(
|
||||
db=db,
|
||||
vendor_data=vendor_data,
|
||||
admin_user_id=current_admin.id # Pass admin ID for audit logging
|
||||
)
|
||||
|
||||
# Audit log is automatically created inside the service
|
||||
|
||||
return {
|
||||
**VendorResponse.model_validate(vendor).model_dump(),
|
||||
"owner_email": owner_user.email,
|
||||
"owner_username": owner_user.username,
|
||||
"temporary_password": temp_password,
|
||||
"login_url": f"{vendor.subdomain}.platform.com/vendor/login"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{vendor_id}/status")
|
||||
def toggle_vendor_status(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Toggle vendor status with audit logging."""
|
||||
vendor, message = admin_service.toggle_vendor_status(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
admin_user_id=current_admin.id # Pass for audit
|
||||
)
|
||||
return {"message": message, "vendor": VendorResponse.model_validate(vendor)}
|
||||
```
|
||||
|
||||
### Step 5: Add Request Context to Audit Logs
|
||||
|
||||
To capture IP address and user agent, use FastAPI's Request object:
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/vendors.py
|
||||
from fastapi import Request
|
||||
|
||||
@router.delete("/{vendor_id}")
|
||||
def delete_vendor(
|
||||
vendor_id: int,
|
||||
request: Request, # Add request parameter
|
||||
confirm: bool = Query(False),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Delete vendor with full audit trail."""
|
||||
|
||||
if not confirm:
|
||||
raise HTTPException(status_code=400, detail="Confirmation required")
|
||||
|
||||
# Get request metadata
|
||||
ip_address = request.client.host if request.client else None
|
||||
user_agent = request.headers.get("user-agent")
|
||||
|
||||
message = admin_service.delete_vendor(db, vendor_id)
|
||||
|
||||
# Log with full context
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="delete_vendor",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor_id),
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
details={"confirm": True}
|
||||
)
|
||||
|
||||
return {"message": message}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example: Platform Settings Usage
|
||||
|
||||
### Creating Default Settings
|
||||
|
||||
```python
|
||||
# scripts/init_platform_settings.py
|
||||
from app.core.database import SessionLocal
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
from models.schemas.admin import AdminSettingCreate
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
# Create default platform settings
|
||||
settings = [
|
||||
AdminSettingCreate(
|
||||
key="max_vendors_allowed",
|
||||
value="1000",
|
||||
value_type="integer",
|
||||
category="system",
|
||||
description="Maximum number of vendors allowed on the platform",
|
||||
is_public=False
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="maintenance_mode",
|
||||
value="false",
|
||||
value_type="boolean",
|
||||
category="system",
|
||||
description="Enable maintenance mode (blocks all non-admin access)",
|
||||
is_public=True
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="vendor_trial_days",
|
||||
value="30",
|
||||
value_type="integer",
|
||||
category="system",
|
||||
description="Default trial period for new vendors (days)",
|
||||
is_public=False
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="stripe_publishable_key",
|
||||
value="pk_test_...",
|
||||
value_type="string",
|
||||
category="payments",
|
||||
description="Stripe publishable key",
|
||||
is_public=True
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="stripe_secret_key",
|
||||
value="sk_test_...",
|
||||
value_type="string",
|
||||
category="payments",
|
||||
description="Stripe secret key",
|
||||
is_encrypted=True,
|
||||
is_public=False
|
||||
)
|
||||
]
|
||||
|
||||
for setting_data in settings:
|
||||
try:
|
||||
admin_settings_service.upsert_setting(db, setting_data, admin_user_id=1)
|
||||
print(f"✓ Created setting: {setting_data.key}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to create {setting_data.key}: {e}")
|
||||
|
||||
db.close()
|
||||
```
|
||||
|
||||
### Using Settings in Your Code
|
||||
|
||||
```python
|
||||
# app/services/vendor_service.py
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
|
||||
def can_create_vendor(db: Session) -> bool:
|
||||
"""Check if platform allows creating more vendors."""
|
||||
|
||||
max_vendors = admin_settings_service.get_setting_value(
|
||||
db=db,
|
||||
key="max_vendors_allowed",
|
||||
default=1000
|
||||
)
|
||||
|
||||
current_count = db.query(Vendor).count()
|
||||
|
||||
return current_count < max_vendors
|
||||
|
||||
|
||||
def is_maintenance_mode(db: Session) -> bool:
|
||||
"""Check if platform is in maintenance mode."""
|
||||
return admin_settings_service.get_setting_value(
|
||||
db=db,
|
||||
key="maintenance_mode",
|
||||
default=False
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Admin Dashboard with Audit Logs
|
||||
|
||||
```html
|
||||
<!-- templates/admin/audit_logs.html -->
|
||||
<div x-data="auditLogs()" x-init="loadLogs()">
|
||||
<h1>Audit Logs</h1>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters">
|
||||
<select x-model="filters.action" @change="loadLogs()">
|
||||
<option value="">All Actions</option>
|
||||
<option value="create_vendor">Create Vendor</option>
|
||||
<option value="delete_vendor">Delete Vendor</option>
|
||||
<option value="toggle_vendor_status">Toggle Status</option>
|
||||
<option value="update_setting">Update Setting</option>
|
||||
</select>
|
||||
|
||||
<select x-model="filters.target_type" @change="loadLogs()">
|
||||
<option value="">All Targets</option>
|
||||
<option value="vendor">Vendors</option>
|
||||
<option value="user">Users</option>
|
||||
<option value="setting">Settings</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Logs Table -->
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Admin</th>
|
||||
<th>Action</th>
|
||||
<th>Target</th>
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="log in logs" :key="log.id">
|
||||
<tr>
|
||||
<td x-text="formatDate(log.created_at)"></td>
|
||||
<td x-text="log.admin_username"></td>
|
||||
<td>
|
||||
<span class="badge" x-text="log.action"></span>
|
||||
</td>
|
||||
<td x-text="`${log.target_type}:${log.target_id}`"></td>
|
||||
<td>
|
||||
<button @click="showDetails(log)">View</button>
|
||||
</td>
|
||||
<td x-text="log.ip_address"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination">
|
||||
<button @click="previousPage()" :disabled="skip === 0">Previous</button>
|
||||
<span x-text="`Page ${currentPage} of ${totalPages}`"></span>
|
||||
<button @click="nextPage()" :disabled="!hasMore">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function auditLogs() {
|
||||
return {
|
||||
logs: [],
|
||||
filters: {
|
||||
action: '',
|
||||
target_type: '',
|
||||
admin_user_id: null
|
||||
},
|
||||
skip: 0,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
|
||||
async loadLogs() {
|
||||
const params = new URLSearchParams({
|
||||
skip: this.skip,
|
||||
limit: this.limit,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
const response = await apiClient.get(`/api/v1/admin/audit/logs?${params}`);
|
||||
this.logs = response.logs;
|
||||
this.total = response.total;
|
||||
},
|
||||
|
||||
showDetails(log) {
|
||||
// Show modal with full details
|
||||
console.log('Details:', log.details);
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
},
|
||||
|
||||
get currentPage() {
|
||||
return Math.floor(this.skip / this.limit) + 1;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.total / this.limit);
|
||||
},
|
||||
|
||||
get hasMore() {
|
||||
return this.skip + this.limit < this.total;
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
this.skip += this.limit;
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
previousPage() {
|
||||
this.skip = Math.max(0, this.skip - this.limit);
|
||||
this.loadLogs();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Platform Settings Management
|
||||
|
||||
```html
|
||||
<!-- templates/admin/settings.html -->
|
||||
<div x-data="platformSettings()" x-init="loadSettings()">
|
||||
<h1>Platform Settings</h1>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<div class="tabs">
|
||||
<button
|
||||
@click="selectedCategory = 'system'"
|
||||
:class="{'active': selectedCategory === 'system'}"
|
||||
>System</button>
|
||||
<button
|
||||
@click="selectedCategory = 'security'"
|
||||
:class="{'active': selectedCategory === 'security'}"
|
||||
>Security</button>
|
||||
<button
|
||||
@click="selectedCategory = 'payments'"
|
||||
:class="{'active': selectedCategory === 'payments'}"
|
||||
>Payments</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings List -->
|
||||
<div class="settings-list">
|
||||
<template x-for="setting in filteredSettings" :key="setting.id">
|
||||
<div class="setting-item">
|
||||
<div class="setting-header">
|
||||
<h3 x-text="setting.key"></h3>
|
||||
<span class="badge" x-text="setting.value_type"></span>
|
||||
</div>
|
||||
<p class="setting-description" x-text="setting.description"></p>
|
||||
|
||||
<div class="setting-value">
|
||||
<input
|
||||
type="text"
|
||||
:value="setting.value"
|
||||
@change="updateSetting(setting.key, $event.target.value)"
|
||||
>
|
||||
<span class="updated-at" x-text="`Updated: ${formatDate(setting.updated_at)}`"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add New Setting -->
|
||||
<button @click="showAddModal = true" class="btn-primary">
|
||||
Add New Setting
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function platformSettings() {
|
||||
return {
|
||||
settings: [],
|
||||
selectedCategory: 'system',
|
||||
showAddModal: false,
|
||||
|
||||
async loadSettings() {
|
||||
const response = await apiClient.get('/api/v1/admin/settings');
|
||||
this.settings = response.settings;
|
||||
},
|
||||
|
||||
get filteredSettings() {
|
||||
if (!this.selectedCategory) return this.settings;
|
||||
return this.settings.filter(s => s.category === this.selectedCategory);
|
||||
},
|
||||
|
||||
async updateSetting(key, newValue) {
|
||||
try {
|
||||
await apiClient.put(`/api/v1/admin/settings/${key}`, {
|
||||
value: newValue
|
||||
});
|
||||
showNotification('Setting updated successfully', 'success');
|
||||
this.loadSettings();
|
||||
} catch (error) {
|
||||
showNotification('Failed to update setting', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing the New Features
|
||||
|
||||
### Test Audit Logging
|
||||
|
||||
```python
|
||||
# tests/test_admin_audit.py
|
||||
import pytest
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
|
||||
def test_log_admin_action(db_session, test_admin_user):
|
||||
"""Test logging admin actions."""
|
||||
log = admin_audit_service.log_action(
|
||||
db=db_session,
|
||||
admin_user_id=test_admin_user.id,
|
||||
action="create_vendor",
|
||||
target_type="vendor",
|
||||
target_id="123",
|
||||
details={"vendor_code": "TEST"}
|
||||
)
|
||||
|
||||
assert log is not None
|
||||
assert log.action == "create_vendor"
|
||||
assert log.target_type == "vendor"
|
||||
assert log.details["vendor_code"] == "TEST"
|
||||
|
||||
def test_query_audit_logs(db_session, test_admin_user):
|
||||
"""Test querying audit logs with filters."""
|
||||
# Create test logs
|
||||
for i in range(5):
|
||||
admin_audit_service.log_action(
|
||||
db=db_session,
|
||||
admin_user_id=test_admin_user.id,
|
||||
action=f"test_action_{i}",
|
||||
target_type="test",
|
||||
target_id=str(i)
|
||||
)
|
||||
|
||||
# Query logs
|
||||
from models.schemas.admin import AdminAuditLogFilters
|
||||
filters = AdminAuditLogFilters(limit=10)
|
||||
logs = admin_audit_service.get_audit_logs(db_session, filters)
|
||||
|
||||
assert len(logs) == 5
|
||||
```
|
||||
|
||||
### Test Platform Settings
|
||||
|
||||
```python
|
||||
# tests/test_admin_settings.py
|
||||
def test_create_setting(db_session, test_admin_user):
|
||||
"""Test creating platform setting."""
|
||||
from models.schemas.admin import AdminSettingCreate
|
||||
|
||||
setting_data = AdminSettingCreate(
|
||||
key="test_setting",
|
||||
value="test_value",
|
||||
value_type="string",
|
||||
category="test"
|
||||
)
|
||||
|
||||
result = admin_settings_service.create_setting(
|
||||
db=db_session,
|
||||
setting_data=setting_data,
|
||||
admin_user_id=test_admin_user.id
|
||||
)
|
||||
|
||||
assert result.key == "test_setting"
|
||||
assert result.value == "test_value"
|
||||
|
||||
def test_get_setting_value_with_type_conversion(db_session):
|
||||
"""Test getting setting values with proper type conversion."""
|
||||
# Create integer setting
|
||||
setting_data = AdminSettingCreate(
|
||||
key="max_vendors",
|
||||
value="100",
|
||||
value_type="integer",
|
||||
category="system"
|
||||
)
|
||||
admin_settings_service.create_setting(db_session, setting_data, 1)
|
||||
|
||||
# Get value (should be converted to int)
|
||||
value = admin_settings_service.get_setting_value(db_session, "max_vendors")
|
||||
assert isinstance(value, int)
|
||||
assert value == 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
You now have a complete admin infrastructure with:
|
||||
|
||||
✅ **Audit Logging**: Track all admin actions for compliance
|
||||
✅ **Platform Settings**: Manage global configuration
|
||||
✅ **Notifications**: System alerts for admins (structure ready)
|
||||
✅ **Platform Alerts**: Health monitoring (structure ready)
|
||||
✅ **Session Tracking**: Monitor admin logins (structure ready)
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. **Apply database migrations** to create new tables
|
||||
2. **Update admin API router** to include new endpoints
|
||||
3. **Add audit logging** to existing admin operations
|
||||
4. **Create default platform settings** using the script
|
||||
5. **Build frontend pages** for audit logs and settings
|
||||
6. **Implement notification service** (notifications.py stubs)
|
||||
7. **Add monitoring** for platform alerts
|
||||
|
||||
These additions make your platform production-ready with full compliance and monitoring capabilities!
|
||||
331
docs/__temp/FRONTEND/FRONTEND_ALPINE_PAGE_TEMPLATE.md
Normal file
331
docs/__temp/FRONTEND/FRONTEND_ALPINE_PAGE_TEMPLATE.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Alpine.js Page Template - Quick Reference
|
||||
|
||||
## ✅ Correct Page Structure
|
||||
|
||||
```javascript
|
||||
// static/admin/js/your-page.js
|
||||
|
||||
// 1. Setup logging (optional but recommended)
|
||||
const YOUR_PAGE_LOG_LEVEL = 3;
|
||||
|
||||
const yourPageLog = {
|
||||
error: (...args) => YOUR_PAGE_LOG_LEVEL >= 1 && console.error('❌ [YOUR_PAGE ERROR]', ...args),
|
||||
warn: (...args) => YOUR_PAGE_LOG_LEVEL >= 2 && console.warn('⚠️ [YOUR_PAGE WARN]', ...args),
|
||||
info: (...args) => YOUR_PAGE_LOG_LEVEL >= 3 && console.info('ℹ️ [YOUR_PAGE INFO]', ...args),
|
||||
debug: (...args) => YOUR_PAGE_LOG_LEVEL >= 4 && console.log('🔍 [YOUR_PAGE DEBUG]', ...args)
|
||||
};
|
||||
|
||||
// 2. Create your Alpine.js component
|
||||
function yourPageComponent() {
|
||||
return {
|
||||
// ✅ CRITICAL: Inherit base layout functionality
|
||||
...data(),
|
||||
|
||||
// ✅ CRITICAL: Set page identifier
|
||||
currentPage: 'your-page',
|
||||
|
||||
// Your page-specific state
|
||||
items: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// ✅ CRITICAL: Proper initialization with guard
|
||||
async init() {
|
||||
yourPageLog.info('=== YOUR PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._yourPageInitialized) {
|
||||
yourPageLog.warn('Page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._yourPageInitialized = true;
|
||||
|
||||
// Load your data
|
||||
await this.loadData();
|
||||
|
||||
yourPageLog.info('=== YOUR PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Your methods
|
||||
async loadData() {
|
||||
yourPageLog.info('Loading data...');
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
// ✅ CRITICAL: Use lowercase apiClient
|
||||
const response = await apiClient.get('/your/endpoint');
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.items = response.items || [];
|
||||
|
||||
yourPageLog.info(`Data loaded in ${duration}ms`, {
|
||||
count: this.items.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
yourPageLog.error('Failed to load data:', error);
|
||||
this.error = error.message;
|
||||
Utils.showToast('Failed to load data', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Format date helper (if needed)
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
return Utils.formatDate(dateString);
|
||||
},
|
||||
|
||||
// Your other methods...
|
||||
};
|
||||
}
|
||||
|
||||
yourPageLog.info('Your page module loaded');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Checklist for New Pages
|
||||
|
||||
### HTML Template
|
||||
```jinja2
|
||||
{# app/templates/admin/your-page.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Your Page{% endblock %}
|
||||
|
||||
{# ✅ CRITICAL: Link to your Alpine.js component #}
|
||||
{% block alpine_data %}yourPageComponent(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Your page content -->
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
{# ✅ CRITICAL: Load your JavaScript file #}
|
||||
<script src="{{ url_for('static', path='admin/js/your-page.js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### JavaScript File Checklist
|
||||
|
||||
- [ ] ✅ Logging setup (optional)
|
||||
- [ ] ✅ Function name matches `alpine_data` in template
|
||||
- [ ] ✅ `...data(),` at start of return object
|
||||
- [ ] ✅ `currentPage: 'your-page'` set
|
||||
- [ ] ✅ Initialization guard in `init()`
|
||||
- [ ] ✅ Use lowercase `apiClient` for API calls
|
||||
- [ ] ✅ Use your custom logger (not `Logger`)
|
||||
- [ ] ✅ Performance tracking with `Date.now()` (optional)
|
||||
- [ ] ✅ Module loaded log at end
|
||||
|
||||
---
|
||||
|
||||
## ❌ Common Mistakes to Avoid
|
||||
|
||||
### 1. Missing Base Inheritance
|
||||
```javascript
|
||||
// ❌ WRONG
|
||||
function myPage() {
|
||||
return {
|
||||
items: [],
|
||||
// Missing ...data()
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ CORRECT
|
||||
function myPage() {
|
||||
return {
|
||||
...data(), // Must be first!
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Wrong API Client Name
|
||||
```javascript
|
||||
// ❌ WRONG - Capital letters
|
||||
await ApiClient.get('/endpoint');
|
||||
await API_CLIENT.get('/endpoint');
|
||||
|
||||
// ✅ CORRECT - lowercase
|
||||
await apiClient.get('/endpoint');
|
||||
```
|
||||
|
||||
### 3. Missing Init Guard
|
||||
```javascript
|
||||
// ❌ WRONG
|
||||
async init() {
|
||||
await this.loadData();
|
||||
}
|
||||
|
||||
// ✅ CORRECT
|
||||
async init() {
|
||||
if (window._myPageInitialized) return;
|
||||
window._myPageInitialized = true;
|
||||
await this.loadData();
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Missing currentPage
|
||||
```javascript
|
||||
// ❌ WRONG
|
||||
return {
|
||||
...data(),
|
||||
items: [],
|
||||
// Missing currentPage
|
||||
};
|
||||
|
||||
// ✅ CORRECT
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'my-page', // Must set this!
|
||||
items: [],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 API Client Pattern
|
||||
|
||||
### GET Request
|
||||
```javascript
|
||||
try {
|
||||
const response = await apiClient.get('/endpoint');
|
||||
this.data = response;
|
||||
} catch (error) {
|
||||
console.error('Failed:', error);
|
||||
Utils.showToast('Failed to load', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
### POST Request
|
||||
```javascript
|
||||
try {
|
||||
const response = await apiClient.post('/endpoint', {
|
||||
name: 'value',
|
||||
// ... data
|
||||
});
|
||||
Utils.showToast('Created successfully', 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed:', error);
|
||||
Utils.showToast('Failed to create', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
### PUT Request
|
||||
```javascript
|
||||
try {
|
||||
const response = await apiClient.put('/endpoint/123', {
|
||||
name: 'updated value'
|
||||
});
|
||||
Utils.showToast('Updated successfully', 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed:', error);
|
||||
Utils.showToast('Failed to update', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE Request
|
||||
```javascript
|
||||
try {
|
||||
await apiClient.delete('/endpoint/123');
|
||||
Utils.showToast('Deleted successfully', 'success');
|
||||
await this.reloadData();
|
||||
} catch (error) {
|
||||
console.error('Failed:', error);
|
||||
Utils.showToast('Failed to delete', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Common UI Patterns
|
||||
|
||||
### Loading State
|
||||
```javascript
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const data = await apiClient.get('/endpoint');
|
||||
this.items = data;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```javascript
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const data = await apiClient.get('/endpoint');
|
||||
this.items = data;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
Utils.showToast('Failed to load', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh/Reload
|
||||
```javascript
|
||||
async refresh() {
|
||||
console.info('Refreshing...');
|
||||
await this.loadData();
|
||||
Utils.showToast('Refreshed successfully', 'success');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Available Utilities
|
||||
|
||||
### From `init-alpine.js` (via `...data()`)
|
||||
- `this.dark` - Dark mode state
|
||||
- `this.toggleTheme()` - Toggle theme
|
||||
- `this.isSideMenuOpen` - Side menu state
|
||||
- `this.toggleSideMenu()` - Toggle side menu
|
||||
- `this.closeSideMenu()` - Close side menu
|
||||
- `this.isNotificationsMenuOpen` - Notifications menu state
|
||||
- `this.toggleNotificationsMenu()` - Toggle notifications
|
||||
- `this.closeNotificationsMenu()` - Close notifications
|
||||
- `this.isProfileMenuOpen` - Profile menu state
|
||||
- `this.toggleProfileMenu()` - Toggle profile menu
|
||||
- `this.closeProfileMenu()` - Close profile menu
|
||||
- `this.isPagesMenuOpen` - Pages menu state
|
||||
- `this.togglePagesMenu()` - Toggle pages menu
|
||||
|
||||
### From `Utils` (global)
|
||||
- `Utils.showToast(message, type, duration)` - Show toast notification
|
||||
- `Utils.formatDate(dateString)` - Format date for display
|
||||
- `Utils.confirm(message, title)` - Show confirmation dialog (if available)
|
||||
|
||||
### From `apiClient` (global)
|
||||
- `apiClient.get(url)` - GET request
|
||||
- `apiClient.post(url, data)` - POST request
|
||||
- `apiClient.put(url, data)` - PUT request
|
||||
- `apiClient.delete(url)` - DELETE request
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start Template Files
|
||||
|
||||
Copy these to create a new page:
|
||||
|
||||
1. Copy `dashboard.js` → rename to `your-page.js`
|
||||
2. Replace function name: `adminDashboard()` → `yourPageComponent()`
|
||||
3. Update logging prefix: `dashLog` → `yourPageLog`
|
||||
4. Update init flag: `_dashboardInitialized` → `_yourPageInitialized`
|
||||
5. Update `currentPage`: `'dashboard'` → `'your-page'`
|
||||
6. Replace data loading logic with your endpoints
|
||||
7. Update HTML template to use your function name
|
||||
|
||||
Done! ✅
|
||||
239
docs/__temp/FRONTEND/FRONTEND_ARCHITECTURE_OVERVIEW.txt
Normal file
239
docs/__temp/FRONTEND/FRONTEND_ARCHITECTURE_OVERVIEW.txt
Normal file
@@ -0,0 +1,239 @@
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ ALPINE.JS PAGE ARCHITECTURE OVERVIEW ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
📂 FILE STRUCTURE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
static/admin/js/
|
||||
├── init-alpine.js ............. Base Alpine.js data & theme
|
||||
├── dashboard.js ............... Dashboard page (✅ WORKING)
|
||||
├── vendors.js ................. Vendor list page (✅ FIXED)
|
||||
└── vendor-edit.js ............. Vendor edit page (✅ FIXED)
|
||||
|
||||
|
||||
🔄 HOW PAGES INHERIT BASE FUNCTIONALITY
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ init-alpine.js │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ function data() { │ │
|
||||
│ │ return { │ │
|
||||
│ │ dark: ..., ← Theme state │ │
|
||||
│ │ toggleTheme() {...}, ← Theme toggle │ │
|
||||
│ │ isSideMenuOpen: false, ← Side menu state │ │
|
||||
│ │ toggleSideMenu() {...}, ← Side menu toggle │ │
|
||||
│ │ isProfileMenuOpen: false, ← Profile menu state │ │
|
||||
│ │ toggleProfileMenu() {...}, ← Profile menu toggle │ │
|
||||
│ │ currentPage: '' ← Page identifier │ │
|
||||
│ │ }; │ │
|
||||
│ │ } │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Uses ...data() spread operator
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ dashboard.js │ │ vendors.js │ │vendor-edit.js │
|
||||
├───────────────┤ ├───────────────┤ ├───────────────┤
|
||||
│function admin │ │function admin │ │function admin │
|
||||
│Dashboard() { │ │Vendors() { │ │VendorEdit() { │
|
||||
│ return { │ │ return { │ │ return { │
|
||||
│ ...data(), │ │ ...data(), │ │ ...data(), │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ └──────────┼───┼────┘ │ │ │ │
|
||||
│ Inherits │ │ Inherits │ │ Inherits │
|
||||
│ all base │ │ all base │ │ all base │
|
||||
│ functions │ │ functions │ │ functions │
|
||||
│ │ │ │ │ │
|
||||
│ // Page │ │ // Page │ │ // Page │
|
||||
│ specific │ │ specific │ │ specific │
|
||||
│ state │ │ state │ │ state │
|
||||
│ }; │ │ }; │ │ }; │
|
||||
│} │ │} │ │} │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
|
||||
|
||||
⚙️ API CLIENT USAGE PATTERN
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
All pages must use lowercase 'apiClient':
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ✅ CORRECT ❌ WRONG │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ apiClient.get(url) │ ApiClient.get(url) │
|
||||
│ apiClient.post(url, data) │ API_CLIENT.post(url, data) │
|
||||
│ apiClient.put(url, data) │ Apiclient.put(url, data) │
|
||||
│ apiClient.delete(url) │ APIClient.delete(url) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
🪵 LOGGING PATTERN
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Each page has its own logger object:
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ dashboard.js vendors.js vendor-edit.js │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ const dashLog = { const vendorsLog = const editLog = { │
|
||||
│ error: (...) => error: (...) => error: (...) => │
|
||||
│ warn: (...) => warn: (...) => warn: (...) => │
|
||||
│ info: (...) => info: (...) => info: (...) => │
|
||||
│ debug: (...) => debug: (...) => debug: (...) => │
|
||||
│ }; }; }; │
|
||||
│ │
|
||||
│ dashLog.info('...') vendorsLog.info() editLog.info() │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
🔒 INITIALIZATION GUARD PATTERN
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Prevents multiple Alpine.js initializations:
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ async init() { │
|
||||
│ // Check if already initialized │
|
||||
│ if (window._yourPageInitialized) { │
|
||||
│ log.warn('Already initialized, skipping...'); │
|
||||
│ return; // Exit early │
|
||||
│ } │
|
||||
│ window._yourPageInitialized = true; // Set flag │
|
||||
│ │
|
||||
│ // Continue with initialization │
|
||||
│ await this.loadData(); │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
📊 STATE MANAGEMENT
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Alpine.js reactive state structure:
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ function yourPage() { │
|
||||
│ return { │
|
||||
│ ...data(), ← Base UI state (inherited) │
|
||||
│ currentPage: 'name', ← Page identifier │
|
||||
│ │
|
||||
│ // Loading states │
|
||||
│ loading: false, ← General loading │
|
||||
│ loadingItem: false, ← Specific item loading │
|
||||
│ saving: false, ← Save operation state │
|
||||
│ │
|
||||
│ // Data │
|
||||
│ items: [], ← List data │
|
||||
│ item: null, ← Single item │
|
||||
│ stats: {}, ← Statistics │
|
||||
│ │
|
||||
│ // Error handling │
|
||||
│ error: null, ← Error message │
|
||||
│ errors: {}, ← Field-specific errors │
|
||||
│ │
|
||||
│ // Methods │
|
||||
│ async init() {...}, ← Initialization │
|
||||
│ async loadData() {...}, ← Data loading │
|
||||
│ async save() {...}, ← Save operation │
|
||||
│ formatDate(d) {...} ← Helper functions │
|
||||
│ }; │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
🎯 TEMPLATE BINDING
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
HTML template connects to Alpine.js component:
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ vendor-edit.html │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ {% extends "admin/base.html" %} │
|
||||
│ │
|
||||
│ {# This binds to the JavaScript function #} │
|
||||
│ {% block alpine_data %}adminVendorEdit(){% endblock %} │
|
||||
│ └──────────────────┐ │
|
||||
│ {% block content %} │ │
|
||||
│ <div x-show="loading">Loading...</div> │ │
|
||||
│ <div x-show="!loading"> │ │
|
||||
│ <p x-text="vendor.name"></p> ← Reactive binding │ │
|
||||
│ </div> │ │
|
||||
│ {% endblock %} │ │
|
||||
│ │ │
|
||||
│ {% block extra_scripts %} │ │
|
||||
│ <script src="...vendor-edit.js"></script> ──────────┐ │ │
|
||||
│ {% endblock %} │ │ │
|
||||
└───────────────────────────────────────────────────────│──│─┘
|
||||
│ │
|
||||
│ │
|
||||
┌───────────────────────────────────────────────────────│──│─┐
|
||||
│ vendor-edit.js │ │ │
|
||||
├───────────────────────────────────────────────────────│──│─┤
|
||||
│ function adminVendorEdit() { ◄────────────────────────┘ │ │
|
||||
│ return { │ │
|
||||
│ ...data(), │ │
|
||||
│ vendor: null, ← Bound to x-text="vendor.name"│ │
|
||||
│ loading: false, ← Bound to x-show="loading" │ │
|
||||
│ async init() {...} │ │
|
||||
│ }; │ │
|
||||
│ } │ │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
🔄 PAGE LIFECYCLE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Page Load
|
||||
↓
|
||||
2. Alpine.js Initialization
|
||||
↓
|
||||
3. x-data="yourPageComponent()" called
|
||||
↓
|
||||
4. Component function executes
|
||||
↓
|
||||
5. ...data() spreads base state
|
||||
↓
|
||||
6. Page-specific state added
|
||||
↓
|
||||
7. init() method runs
|
||||
↓
|
||||
8. Check initialization guard
|
||||
↓
|
||||
9. Load data from API
|
||||
↓
|
||||
10. Reactive bindings update UI
|
||||
|
||||
|
||||
✅ CHECKLIST FOR NEW PAGES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
JavaScript File:
|
||||
□ Create logger object (pageLog)
|
||||
□ Define component function
|
||||
□ Add ...data() at start of return object
|
||||
□ Set currentPage: 'page-name'
|
||||
□ Add initialization guard
|
||||
□ Use lowercase apiClient for API calls
|
||||
□ Add performance tracking (optional)
|
||||
□ Use page-specific logger
|
||||
|
||||
HTML Template:
|
||||
□ Extend admin/base.html
|
||||
□ Set alpine_data block with function name
|
||||
□ Add x-show for loading states
|
||||
□ Add x-text for reactive data
|
||||
□ Load JavaScript file in extra_scripts block
|
||||
|
||||
|
||||
══════════════════════════════════════════════════════════════════
|
||||
Your dashboard.js is perfect!
|
||||
Use it as the template for all new pages.
|
||||
══════════════════════════════════════════════════════════════════
|
||||
@@ -0,0 +1,444 @@
|
||||
# Complete Implementation Guide - Testing Hub, Components & Icons
|
||||
|
||||
## 🎉 What's Been Created
|
||||
|
||||
### ✅ All Files Follow Your Alpine.js Architecture Perfectly!
|
||||
|
||||
1. **Testing Hub** - Manual QA tools
|
||||
2. **Components Library** - UI component reference with navigation
|
||||
3. **Icons Browser** - Searchable icon library with copy-to-clipboard
|
||||
4. **Sidebar Fix** - Active menu indicator for all pages
|
||||
|
||||
---
|
||||
|
||||
## 📦 Files Created
|
||||
|
||||
### JavaScript Files (Alpine.js Components)
|
||||
1. **[testing-hub.js](computer:///mnt/user-data/outputs/testing-hub.js)** - Testing hub component
|
||||
2. **[components.js](computer:///mnt/user-data/outputs/components.js)** - Components library component
|
||||
3. **[icons-page.js](computer:///mnt/user-data/outputs/icons-page.js)** - Icons browser component
|
||||
|
||||
### HTML Templates
|
||||
1. **[testing-hub.html](computer:///mnt/user-data/outputs/testing-hub.html)** - Testing hub page
|
||||
2. **[components.html](computer:///mnt/user-data/outputs/components.html)** - Components library page
|
||||
3. **[icons.html](computer:///mnt/user-data/outputs/icons.html)** - Icons browser page
|
||||
|
||||
### Sidebar Update
|
||||
1. **[sidebar-fixed.html](computer:///mnt/user-data/outputs/sidebar-fixed.html)** - Fixed sidebar with active indicators
|
||||
|
||||
### Documentation
|
||||
1. **[ARCHITECTURE_CONFIRMATION.md](computer:///mnt/user-data/outputs/ARCHITECTURE_CONFIRMATION.md)** - Architecture confirmation
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation Steps
|
||||
|
||||
### Step 1: Install JavaScript Files
|
||||
|
||||
```bash
|
||||
# Copy to your static directory
|
||||
cp outputs/testing-hub.js static/admin/js/testing-hub.js
|
||||
cp outputs/components.js static/admin/js/components.js
|
||||
cp outputs/icons-page.js static/admin/js/icons-page.js
|
||||
```
|
||||
|
||||
### Step 2: Install HTML Templates
|
||||
|
||||
```bash
|
||||
# Copy to your templates directory
|
||||
cp outputs/testing-hub.html app/templates/admin/testing-hub.html
|
||||
cp outputs/components.html app/templates/admin/components.html
|
||||
cp outputs/icons.html app/templates/admin/icons.html
|
||||
```
|
||||
|
||||
### Step 3: Fix Sidebar (IMPORTANT!)
|
||||
|
||||
```bash
|
||||
# Replace your current sidebar
|
||||
cp outputs/sidebar-fixed.html app/templates/partials/sidebar.html
|
||||
```
|
||||
|
||||
### Step 4: Add Icons Route
|
||||
|
||||
Update `app/api/v1/admin/pages.py` - add this route:
|
||||
|
||||
```python
|
||||
@router.get("/icons", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_icons_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Render icons browser page.
|
||||
Browse and search all available icons.
|
||||
"""
|
||||
return templates.TemplateResponse(
|
||||
"admin/icons.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Step 5: Verify Icons Are Updated
|
||||
|
||||
Make sure you're using the updated `icons-updated.js` from earlier:
|
||||
|
||||
```bash
|
||||
cp outputs/icons-updated.js static/shared/js/icons.js
|
||||
```
|
||||
|
||||
### Step 6: Restart Server
|
||||
|
||||
```bash
|
||||
# Stop current server (Ctrl+C)
|
||||
# Start again
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Sidebar Active Indicator Fix
|
||||
|
||||
### The Problem
|
||||
|
||||
You noticed that only the Dashboard menu item showed the vertical purple bar on the left when active. Other menu items didn't show this indicator.
|
||||
|
||||
### The Root Cause
|
||||
|
||||
Each page's JavaScript component needs to set `currentPage` correctly, and the sidebar HTML needs to check for that value.
|
||||
|
||||
**Before (Only Dashboard worked):**
|
||||
```html
|
||||
<!-- Only dashboard had the x-show condition -->
|
||||
<span x-show="currentPage === 'dashboard'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg"></span>
|
||||
```
|
||||
|
||||
### The Fix
|
||||
|
||||
**1. Sidebar HTML** - Add the indicator `<span>` to EVERY menu item:
|
||||
|
||||
```html
|
||||
<!-- Vendors -->
|
||||
<li class="relative px-6 py-3">
|
||||
<!-- ✅ Add this span for the purple bar -->
|
||||
<span x-show="currentPage === 'vendors'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg"></span>
|
||||
<a :class="currentPage === 'vendors' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
href="/admin/vendors">
|
||||
<span x-html="$icon('shopping-bag')"></span>
|
||||
<span class="ml-4">Vendors</span>
|
||||
</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
**2. JavaScript Files** - Each component must set `currentPage`:
|
||||
|
||||
```javascript
|
||||
// vendors.js
|
||||
function adminVendors() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'vendors', // ✅ Must match sidebar check
|
||||
// ... rest of component
|
||||
};
|
||||
}
|
||||
|
||||
// users.js
|
||||
function adminUsers() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'users', // ✅ Must match sidebar check
|
||||
// ... rest of component
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Page Mapping
|
||||
|
||||
| Page | JavaScript `currentPage` | Sidebar Check | URL |
|
||||
|------|--------------------------|---------------|-----|
|
||||
| Dashboard | `'dashboard'` | `x-show="currentPage === 'dashboard'"` | `/admin/dashboard` |
|
||||
| Vendors | `'vendors'` | `x-show="currentPage === 'vendors'"` | `/admin/vendors` |
|
||||
| Users | `'users'` | `x-show="currentPage === 'users'"` | `/admin/users` |
|
||||
| Imports | `'imports'` | `x-show="currentPage === 'imports'"` | `/admin/imports` |
|
||||
| Components | `'components'` | `x-show="currentPage === 'components'"` | `/admin/components` |
|
||||
| Icons | `'icons'` | `x-show="currentPage === 'icons'"` | `/admin/icons` |
|
||||
| Testing | `'testing'` | `x-show="currentPage === 'testing'"` | `/admin/testing` |
|
||||
| Settings | `'settings'` | `x-show="currentPage === 'settings'"` | `/admin/settings` |
|
||||
|
||||
### Updated Sidebar Structure
|
||||
|
||||
The fixed sidebar now includes:
|
||||
|
||||
**Main Navigation:**
|
||||
- Dashboard
|
||||
- Vendors
|
||||
- Users
|
||||
- Import Jobs
|
||||
|
||||
**Developer Tools Section:** (NEW!)
|
||||
- Components
|
||||
- Icons
|
||||
- Testing Hub
|
||||
|
||||
**Settings Section:**
|
||||
- Settings
|
||||
|
||||
Each section is properly separated with dividers and all menu items have active indicators.
|
||||
|
||||
---
|
||||
|
||||
## ✨ New Features
|
||||
|
||||
### Testing Hub
|
||||
- **2 Test Suites**: Auth Flow and Data Migration
|
||||
- **Stats Cards**: Quick metrics overview
|
||||
- **Interactive Cards**: Click to run tests
|
||||
- **Best Practices**: Testing guidelines
|
||||
- **Resource Links**: To Components and Icons pages
|
||||
|
||||
### Components Library
|
||||
- **Sticky Section Navigation**: Jump to Forms, Buttons, Cards, etc.
|
||||
- **Hash-based URLs**: Bookmarkable sections (#forms, #buttons)
|
||||
- **Copy to Clipboard**: Click to copy component code
|
||||
- **Live Examples**: All components with real Alpine.js
|
||||
- **Dark Mode**: All examples support dark mode
|
||||
|
||||
### Icons Browser
|
||||
- **Search Functionality**: Filter icons by name
|
||||
- **Category Navigation**: Browse by category
|
||||
- **Live Preview**: See icons in multiple sizes
|
||||
- **Copy Icon Name**: Quick copy to clipboard
|
||||
- **Copy Usage Code**: Copy Alpine.js usage code
|
||||
- **Selected Icon Details**: Full preview and size examples
|
||||
- **Auto-categorization**: Icons organized automatically
|
||||
|
||||
---
|
||||
|
||||
## 🎯 How Each Feature Works
|
||||
|
||||
### Components Library Navigation
|
||||
|
||||
1. **Click a section** in the left sidebar
|
||||
2. **Page scrolls** to that section smoothly
|
||||
3. **URL updates** with hash (#forms)
|
||||
4. **Active section** is highlighted in purple
|
||||
5. **Bookmarkable**: Share URL with #section
|
||||
|
||||
```javascript
|
||||
// How it works
|
||||
goToSection(sectionId) {
|
||||
this.activeSection = sectionId;
|
||||
window.location.hash = sectionId;
|
||||
// Smooth scroll
|
||||
document.getElementById(sectionId).scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
```
|
||||
|
||||
### Icons Browser Search
|
||||
|
||||
1. **Type in search box** - filters as you type
|
||||
2. **Click category pill** - filters by category
|
||||
3. **Click any icon** - shows details panel
|
||||
4. **Hover icon** - shows copy buttons
|
||||
5. **Click copy** - copies to clipboard
|
||||
|
||||
```javascript
|
||||
// How it works
|
||||
filterIcons() {
|
||||
let icons = this.allIcons;
|
||||
|
||||
// Filter by category
|
||||
if (this.activeCategory !== 'all') {
|
||||
icons = icons.filter(icon => icon.category === this.activeCategory);
|
||||
}
|
||||
|
||||
// Filter by search
|
||||
if (this.searchQuery.trim()) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
icons = icons.filter(icon => icon.name.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
this.filteredIcons = icons;
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Hub Navigation
|
||||
|
||||
1. **View stats** at the top
|
||||
2. **Read test suite cards** with features
|
||||
3. **Click "Run Tests"** to go to test page
|
||||
4. **Read best practices** before testing
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
After installation, verify:
|
||||
|
||||
### General
|
||||
- [ ] Server starts without errors
|
||||
- [ ] All routes load successfully
|
||||
- [ ] Icons display correctly everywhere
|
||||
- [ ] Dark mode works on all pages
|
||||
|
||||
### Sidebar
|
||||
- [ ] Dashboard shows purple bar when active
|
||||
- [ ] Vendors shows purple bar when active
|
||||
- [ ] Users shows purple bar when active
|
||||
- [ ] Components shows purple bar when active
|
||||
- [ ] Icons shows purple bar when active
|
||||
- [ ] Testing shows purple bar when active
|
||||
- [ ] Text is bold/highlighted on active page
|
||||
|
||||
### Testing Hub
|
||||
- [ ] Page loads at `/admin/testing`
|
||||
- [ ] Stats cards display correctly
|
||||
- [ ] Test suite cards are clickable
|
||||
- [ ] Icons render properly
|
||||
- [ ] Links to other pages work
|
||||
|
||||
### Components Library
|
||||
- [ ] Page loads at `/admin/components`
|
||||
- [ ] Section navigation works
|
||||
- [ ] Clicking section scrolls to it
|
||||
- [ ] URL hash updates (#forms, etc.)
|
||||
- [ ] Copy buttons work
|
||||
- [ ] All form examples render
|
||||
- [ ] Toast examples work
|
||||
|
||||
### Icons Browser
|
||||
- [ ] Page loads at `/admin/icons`
|
||||
- [ ] Shows correct icon count
|
||||
- [ ] Search filters icons
|
||||
- [ ] Category pills filter icons
|
||||
- [ ] Clicking icon shows details
|
||||
- [ ] Copy name button works
|
||||
- [ ] Copy usage button works
|
||||
- [ ] Preview shows multiple sizes
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### Adding More Test Suites
|
||||
|
||||
Edit `testing-hub.js`:
|
||||
|
||||
```javascript
|
||||
testSuites: [
|
||||
// ... existing suites
|
||||
{
|
||||
id: 'new-suite',
|
||||
name: 'New Test Suite',
|
||||
description: 'Description here',
|
||||
url: '/admin/test/new-suite',
|
||||
icon: 'icon-name',
|
||||
color: 'blue', // blue, orange, green, purple
|
||||
testCount: 5,
|
||||
features: [
|
||||
'Feature 1',
|
||||
'Feature 2'
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Adding More Component Sections
|
||||
|
||||
Edit `components.js`:
|
||||
|
||||
```javascript
|
||||
sections: [
|
||||
// ... existing sections
|
||||
{ id: 'new-section', name: 'New Section', icon: 'icon-name' }
|
||||
]
|
||||
```
|
||||
|
||||
Then add the section HTML in `components.html`:
|
||||
|
||||
```html
|
||||
<section id="new-section">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2>New Section</h2>
|
||||
<!-- Your components here -->
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Adding More Icon Categories
|
||||
|
||||
Edit `icons-page.js`:
|
||||
|
||||
```javascript
|
||||
categories: [
|
||||
// ... existing categories
|
||||
{ id: 'new-category', name: 'New Category', icon: 'icon-name' }
|
||||
]
|
||||
```
|
||||
|
||||
And update the `categorizeIcon()` function:
|
||||
|
||||
```javascript
|
||||
categoryMap: {
|
||||
// ... existing mappings
|
||||
'new-category': ['keyword1', 'keyword2']
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Quick Reference
|
||||
|
||||
### Alpine.js Component Pattern
|
||||
|
||||
```javascript
|
||||
function yourPageComponent() {
|
||||
return {
|
||||
...data(), // ✅ Inherit base
|
||||
currentPage: 'name', // ✅ Set page ID
|
||||
|
||||
async init() {
|
||||
if (window._yourPageInitialized) return;
|
||||
window._yourPageInitialized = true;
|
||||
// Your init code
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Sidebar Menu Item Pattern
|
||||
|
||||
```html
|
||||
<li class="relative px-6 py-3">
|
||||
<!-- Active indicator -->
|
||||
<span x-show="currentPage === 'page-name'"
|
||||
class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg">
|
||||
</span>
|
||||
|
||||
<!-- Link -->
|
||||
<a :class="currentPage === 'page-name' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
href="/admin/page-name">
|
||||
<span x-html="$icon('icon-name')"></span>
|
||||
<span class="ml-4">Page Name</span>
|
||||
</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Summary
|
||||
|
||||
**Architecture:** ✅ All files follow your Alpine.js patterns perfectly
|
||||
**Sidebar:** ✅ Fixed - all menu items now show active indicator
|
||||
**Testing Hub:** ✅ Complete with test suites and navigation
|
||||
**Components:** ✅ Complete with section navigation and copy feature
|
||||
**Icons:** ✅ Complete with search, categories, and copy features
|
||||
|
||||
**Total Files:** 7 (3 JS + 3 HTML + 1 Sidebar)
|
||||
**Lines of Code:** ~2000+
|
||||
**Features Added:** 20+
|
||||
|
||||
Everything is ready to install! 🚀
|
||||
305
docs/__temp/FRONTEND/FRONTEND_UI_COMPONENTS.md
Normal file
305
docs/__temp/FRONTEND/FRONTEND_UI_COMPONENTS.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# UI Components Library Implementation Guide
|
||||
|
||||
## Overview
|
||||
This guide covers the implementation of:
|
||||
1. **Components reference page** - A library showcasing all your UI components
|
||||
2. **Updated vendor edit page** - Using proper form components
|
||||
3. **Updated vendor detail page** - Using proper card components
|
||||
4. **Sidebar navigation** - Adding "Components" menu item
|
||||
|
||||
## Files Created
|
||||
|
||||
### 1. Components Library Page
|
||||
**File:** `app/templates/admin/components.html`
|
||||
- Complete reference for all UI components
|
||||
- Quick navigation to sections (Forms, Buttons, Cards, etc.)
|
||||
- Copy-paste ready examples
|
||||
- Shows validation states, disabled states, helper text, etc.
|
||||
|
||||
### 2. Updated Vendor Edit Page
|
||||
**File:** `app/templates/admin/vendor-edit-updated.html`
|
||||
- Uses proper form components from your library
|
||||
- Improved visual hierarchy with card sections
|
||||
- Better validation state displays (red borders for errors)
|
||||
- Quick actions section at the top
|
||||
- Status badges showing current state
|
||||
- Clean, consistent styling throughout
|
||||
|
||||
### 3. Vendor Detail Page
|
||||
**File:** `app/templates/admin/vendor-detail.html`
|
||||
- NEW file (didn't exist before)
|
||||
- Uses card components to display vendor information
|
||||
- Status cards showing verification, active status, dates
|
||||
- Information organized in clear sections
|
||||
- All vendor data displayed in readable format
|
||||
- Delete action button
|
||||
|
||||
### 4. JavaScript for Detail Page
|
||||
**File:** `static/admin/js/vendor-detail.js`
|
||||
- Loads vendor data
|
||||
- Handles delete action with double confirmation
|
||||
- Logging for debugging
|
||||
- Error handling
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Add Components Menu to Sidebar
|
||||
|
||||
Update your `app/templates/admin/sidebar.html` (or wherever your sidebar is defined):
|
||||
|
||||
```html
|
||||
<!-- Add this menu item after "Settings" or wherever appropriate -->
|
||||
<li class="relative px-6 py-3">
|
||||
<a
|
||||
class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
|
||||
:class="{ 'text-gray-800 dark:text-gray-100': currentPage === 'components' }"
|
||||
href="/admin/components"
|
||||
>
|
||||
<span x-html="$icon('collection', 'w-5 h-5')"></span>
|
||||
<span class="ml-4">Components</span>
|
||||
</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
### Step 2: Add Components Page Route
|
||||
|
||||
Update your `app/api/v1/admin/pages.py`:
|
||||
|
||||
```python
|
||||
@router.get("/components", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_components_page(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Render UI components reference page.
|
||||
Shows all available UI components for easy reference.
|
||||
"""
|
||||
return templates.TemplateResponse(
|
||||
"admin/components.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Replace Vendor Edit Template
|
||||
|
||||
1. Backup your current: `app/templates/admin/vendor-edit.html`
|
||||
2. Replace it with: `vendor-edit-updated.html`
|
||||
3. Keep your existing `vendor-edit.js` (no changes needed)
|
||||
|
||||
### Step 4: Add Vendor Detail Template & JavaScript
|
||||
|
||||
1. Copy `vendor-detail.html` to `app/templates/admin/vendor-detail.html`
|
||||
2. Copy `vendor-detail.js` to `static/admin/js/vendor-detail.js`
|
||||
|
||||
## Component Usage Guide
|
||||
|
||||
### Form Components
|
||||
|
||||
#### Basic Input
|
||||
```html
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
Label Text <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.fieldName"
|
||||
required
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
/>
|
||||
</label>
|
||||
```
|
||||
|
||||
#### Input with Validation Error
|
||||
```html
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Field Label</span>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.fieldName }"
|
||||
/>
|
||||
<span x-show="errors.fieldName" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.fieldName"></span>
|
||||
</label>
|
||||
```
|
||||
|
||||
#### Disabled Input
|
||||
```html
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
value="Read-only value"
|
||||
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
|
||||
/>
|
||||
```
|
||||
|
||||
#### Textarea
|
||||
```html
|
||||
<textarea
|
||||
x-model="formData.description"
|
||||
rows="3"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
||||
></textarea>
|
||||
```
|
||||
|
||||
### Card Components
|
||||
|
||||
#### Stats Card
|
||||
```html
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
||||
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Users
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
1,234
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Info Card
|
||||
```html
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Card Title
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Field Label</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">Field Value</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Button Components
|
||||
|
||||
#### Primary Button
|
||||
```html
|
||||
<button class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
Button Text
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Button with Icon
|
||||
```html
|
||||
<button class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none">
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Add Item
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Secondary Button
|
||||
```html
|
||||
<button class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg hover:border-gray-400 focus:outline-none dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
|
||||
Cancel
|
||||
</button>
|
||||
```
|
||||
|
||||
## Features of Updated Pages
|
||||
|
||||
### Vendor Edit Page Improvements
|
||||
|
||||
1. **Quick Actions Section**
|
||||
- Verify/Unverify button
|
||||
- Activate/Deactivate button
|
||||
- Status badges showing current state
|
||||
|
||||
2. **Better Form Organization**
|
||||
- Clear sections with headers
|
||||
- Two-column layout on desktop
|
||||
- Helper text for all fields
|
||||
- Proper validation states
|
||||
|
||||
3. **Visual Consistency**
|
||||
- Uses standard form components
|
||||
- Consistent spacing and sizing
|
||||
- Dark mode support
|
||||
|
||||
4. **User Experience**
|
||||
- Disabled states for read-only fields
|
||||
- Clear indication of required fields
|
||||
- Loading states
|
||||
- Error messages inline with fields
|
||||
|
||||
### Vendor Detail Page Features
|
||||
|
||||
1. **Status Overview**
|
||||
- 4 stats cards at top showing key metrics
|
||||
- Visual status indicators (colors, icons)
|
||||
|
||||
2. **Information Organization**
|
||||
- Basic info card
|
||||
- Contact info card
|
||||
- Business details section
|
||||
- Owner information section
|
||||
- Marketplace URLs (if available)
|
||||
|
||||
3. **Actions**
|
||||
- Edit button (goes to edit page)
|
||||
- Delete button (with double confirmation)
|
||||
- Back to list button
|
||||
|
||||
## Quick Reference: Where to Find Components
|
||||
|
||||
When you need a component, visit `/admin/components` and you'll find:
|
||||
|
||||
- **Forms Section**: All input types, validation states, helper text
|
||||
- **Buttons Section**: All button styles and states
|
||||
- **Cards Section**: Stats cards, info cards
|
||||
- **Tables Section**: (from your tables.html)
|
||||
- **Modals Section**: (from your modals.html)
|
||||
- **Charts Section**: (from your charts.html)
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] `/admin/components` page loads and displays all components
|
||||
- [ ] Components menu item appears in sidebar
|
||||
- [ ] `/admin/vendors/{vendor_code}/edit` displays correctly
|
||||
- [ ] Form validation shows errors in red
|
||||
- [ ] Quick actions (verify/activate) work
|
||||
- [ ] `/admin/vendors/{vendor_code}` displays all vendor data
|
||||
- [ ] Status cards show correct information
|
||||
- [ ] Edit button navigates to edit page
|
||||
- [ ] Delete button shows double confirmation
|
||||
- [ ] All pages work in dark mode
|
||||
- [ ] All pages are responsive on mobile
|
||||
|
||||
## Color Scheme Reference
|
||||
|
||||
Your component library uses these color schemes:
|
||||
|
||||
- **Primary**: Purple (`bg-purple-600`, `text-purple-600`)
|
||||
- **Success**: Green (`bg-green-600`, `text-green-600`)
|
||||
- **Warning**: Orange (`bg-orange-600`, `text-orange-600`)
|
||||
- **Danger**: Red (`bg-red-600`, `text-red-600`)
|
||||
- **Info**: Blue (`bg-blue-600`, `text-blue-600`)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement the components page route
|
||||
2. Add menu item to sidebar
|
||||
3. Replace vendor-edit.html
|
||||
4. Add vendor-detail.html and .js
|
||||
5. Test all pages
|
||||
6. Apply same patterns to other admin pages (users, imports, etc.)
|
||||
|
||||
## Tips
|
||||
|
||||
- Always reference `/admin/components` when building new pages
|
||||
- Copy component HTML directly from the components page
|
||||
- Maintain consistent spacing and styling
|
||||
- Use Alpine.js x-model for form bindings
|
||||
- Use your icon system with `x-html="$icon('icon-name', 'w-5 h-5')"`
|
||||
- Test in both light and dark modes
|
||||
|
||||
Enjoy your new component library! 🎨
|
||||
228
docs/__temp/FRONTEND/FRONTEND_UI_COMPONENTS_QUICK_REFERENCE.md
Normal file
228
docs/__temp/FRONTEND/FRONTEND_UI_COMPONENTS_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# UI Components Quick Reference
|
||||
|
||||
## Most Common Patterns
|
||||
|
||||
### 📝 Form Field (Basic)
|
||||
```html
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Field Name</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.field"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
/>
|
||||
</label>
|
||||
```
|
||||
|
||||
### 📝 Required Field with Error
|
||||
```html
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
Field Name <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.field"
|
||||
required
|
||||
:class="{ 'border-red-600': errors.field }"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input"
|
||||
/>
|
||||
<span x-show="errors.field" class="text-xs text-red-600 dark:text-red-400" x-text="errors.field"></span>
|
||||
</label>
|
||||
```
|
||||
|
||||
### 📝 Read-Only Field
|
||||
```html
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Field Name</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="data.field"
|
||||
disabled
|
||||
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
|
||||
/>
|
||||
</label>
|
||||
```
|
||||
|
||||
### 🃏 Stats Card
|
||||
```html
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
|
||||
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Label</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200">Value</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 🃏 Info Card
|
||||
```html
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Title</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Label</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">Value</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 🔘 Primary Button
|
||||
```html
|
||||
<button class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none">
|
||||
Click Me
|
||||
</button>
|
||||
```
|
||||
|
||||
### 🔘 Button with Icon
|
||||
```html
|
||||
<button class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700">
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Add Item
|
||||
</button>
|
||||
```
|
||||
|
||||
### 🔘 Secondary Button
|
||||
```html
|
||||
<button class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg hover:border-gray-400 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
|
||||
Cancel
|
||||
</button>
|
||||
```
|
||||
|
||||
### 🏷️ Status Badge (Success)
|
||||
```html
|
||||
<span class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
|
||||
<span x-html="$icon('check-circle', 'w-3 h-3 mr-1')"></span>
|
||||
Active
|
||||
</span>
|
||||
```
|
||||
|
||||
### 🏷️ Status Badge (Warning)
|
||||
```html
|
||||
<span class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-orange-700 bg-orange-100 rounded-full dark:bg-orange-700 dark:text-orange-100">
|
||||
<span x-html="$icon('clock', 'w-3 h-3 mr-1')"></span>
|
||||
Pending
|
||||
</span>
|
||||
```
|
||||
|
||||
### 🏷️ Status Badge (Danger)
|
||||
```html
|
||||
<span class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">
|
||||
<span x-html="$icon('x-circle', 'w-3 h-3 mr-1')"></span>
|
||||
Inactive
|
||||
</span>
|
||||
```
|
||||
|
||||
## Grid Layouts
|
||||
|
||||
### 2 Columns (Desktop)
|
||||
```html
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<!-- Column 1 -->
|
||||
<div>...</div>
|
||||
<!-- Column 2 -->
|
||||
<div>...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 4 Columns (Responsive)
|
||||
```html
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Cards -->
|
||||
</div>
|
||||
```
|
||||
|
||||
## Color Classes
|
||||
|
||||
### Background Colors
|
||||
- Primary: `bg-purple-600`
|
||||
- Success: `bg-green-600`
|
||||
- Warning: `bg-orange-600`
|
||||
- Danger: `bg-red-600`
|
||||
- Info: `bg-blue-600`
|
||||
|
||||
### Text Colors
|
||||
- Primary: `text-purple-600`
|
||||
- Success: `text-green-600`
|
||||
- Warning: `text-orange-600`
|
||||
- Danger: `text-red-600`
|
||||
- Info: `text-blue-600`
|
||||
|
||||
### Icon Colors
|
||||
- Primary: `text-purple-500 bg-purple-100`
|
||||
- Success: `text-green-500 bg-green-100`
|
||||
- Warning: `text-orange-500 bg-orange-100`
|
||||
- Danger: `text-red-500 bg-red-100`
|
||||
- Info: `text-blue-500 bg-blue-100`
|
||||
|
||||
## Common Icons
|
||||
- `user-group` - Users/Teams
|
||||
- `badge-check` - Verified
|
||||
- `check-circle` - Success
|
||||
- `x-circle` - Error/Inactive
|
||||
- `clock` - Pending
|
||||
- `calendar` - Dates
|
||||
- `refresh` - Update
|
||||
- `edit` - Edit
|
||||
- `delete` - Delete
|
||||
- `plus` - Add
|
||||
- `arrow-left` - Back
|
||||
- `exclamation` - Warning
|
||||
|
||||
## Spacing
|
||||
- Small gap: `gap-3`
|
||||
- Medium gap: `gap-6`
|
||||
- Large gap: `gap-8`
|
||||
- Margin bottom: `mb-4`, `mb-6`, `mb-8`
|
||||
- Padding: `p-3`, `p-4`, `px-4 py-3`
|
||||
|
||||
## Quick Copy-Paste: Page Structure
|
||||
|
||||
```html
|
||||
{# app/templates/admin/your-page.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Your Page{% endblock %}
|
||||
|
||||
{% block alpine_data %}yourPageData(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
<div class="my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Page Title
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading...</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div x-show="!loading">
|
||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<!-- Your content here -->
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
1. Always use `dark:` variants for dark mode
|
||||
2. Add `:disabled="saving"` to buttons during operations
|
||||
3. Use `x-show` for conditional display
|
||||
4. Use `x-text` for dynamic text
|
||||
5. Use `x-html="$icon(...)"` for icons
|
||||
6. Validation errors: `border-red-600` class
|
||||
7. Helper text: `text-xs text-gray-600`
|
||||
8. Error text: `text-xs text-red-600`
|
||||
|
||||
## Reference Page
|
||||
|
||||
Visit `/admin/components` for full component library with live examples!
|
||||
297
docs/__temp/FRONTEND/PAGINATION_DOCUMENTATION.md
Normal file
297
docs/__temp/FRONTEND/PAGINATION_DOCUMENTATION.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# Vendor Page Pagination Implementation
|
||||
|
||||
## 🎯 What's New
|
||||
|
||||
Your vendors page now includes:
|
||||
1. ✅ **Edit button** - Already present (pencil icon next to view icon)
|
||||
2. ✅ **Pagination** - New pagination controls at the bottom of the table
|
||||
|
||||
---
|
||||
|
||||
## 📦 Files Updated
|
||||
|
||||
### 1. vendors.html
|
||||
**Changes:**
|
||||
- Changed `x-for="vendor in vendors"` → `x-for="vendor in paginatedVendors"`
|
||||
- Added pagination footer with controls
|
||||
- Added "Showing X-Y of Z" results display
|
||||
|
||||
### 2. vendors.js
|
||||
**Changes:**
|
||||
- Added pagination state: `currentPage`, `itemsPerPage`
|
||||
- Added computed properties:
|
||||
- `paginatedVendors` - Returns current page's vendors
|
||||
- `totalPages` - Calculates total number of pages
|
||||
- `startIndex` / `endIndex` - For "Showing X-Y" display
|
||||
- `pageNumbers` - Generates smart page number array with ellipsis
|
||||
- Added pagination methods:
|
||||
- `goToPage(page)` - Navigate to specific page
|
||||
- `nextPage()` - Go to next page
|
||||
- `previousPage()` - Go to previous page
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Pagination Features
|
||||
|
||||
### Smart Page Number Display
|
||||
The pagination intelligently shows page numbers:
|
||||
|
||||
**Example 1: Few pages (≤7)**
|
||||
```
|
||||
← 1 2 3 4 5 6 7 →
|
||||
```
|
||||
|
||||
**Example 2: Many pages, current = 1**
|
||||
```
|
||||
← 1 2 3 ... 9 10 →
|
||||
```
|
||||
|
||||
**Example 3: Many pages, current = 5**
|
||||
```
|
||||
← 1 ... 4 5 6 ... 10 →
|
||||
```
|
||||
|
||||
**Example 4: Many pages, current = 10**
|
||||
```
|
||||
← 1 ... 8 9 10 →
|
||||
```
|
||||
|
||||
### Configurable Items Per Page
|
||||
Default: 10 vendors per page
|
||||
|
||||
To change, edit in `vendors.js`:
|
||||
```javascript
|
||||
itemsPerPage: 10, // Change this number
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 How It Works
|
||||
|
||||
### 1. Computed Properties (Alpine.js)
|
||||
Alpine.js computes these values reactively:
|
||||
|
||||
```javascript
|
||||
get paginatedVendors() {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = start + this.itemsPerPage;
|
||||
return this.vendors.slice(start, end);
|
||||
}
|
||||
```
|
||||
|
||||
When `currentPage` changes, `paginatedVendors` automatically updates!
|
||||
|
||||
### 2. Template Binding
|
||||
The HTML template uses the computed property:
|
||||
```html
|
||||
<template x-for="vendor in paginatedVendors" :key="vendor.vendor_code">
|
||||
<!-- Vendor row -->
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. Pagination Controls
|
||||
Buttons call the pagination methods:
|
||||
```html
|
||||
<button @click="previousPage()" :disabled="currentPage === 1">
|
||||
<button @click="goToPage(page)">
|
||||
<button @click="nextPage()" :disabled="currentPage === totalPages">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Visual Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Vendor Management [+ Create Vendor] │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ [Total] [Verified] [Pending] [Inactive] ← Stats Cards │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ Vendor │ Subdomain │ Status │ Created │ Actions │
|
||||
├────────┼───────────┼────────┼─────────┼─────────────────┤
|
||||
│ Acme │ acme │ ✓ │ Jan 1 │ 👁 ✏️ 🗑 │
|
||||
│ Beta │ beta │ ⏰ │ Jan 2 │ 👁 ✏️ 🗑 │
|
||||
│ ... │ ... │ ... │ ... │ ... │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ Showing 1-10 of 45 ← 1 2 [3] 4 ... 9 → │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
↑
|
||||
Pagination controls
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎭 Action Buttons Explained
|
||||
|
||||
Each vendor row has 3 action buttons:
|
||||
|
||||
1. **View Button** (👁 Eye icon - Blue)
|
||||
- Shows vendor details
|
||||
- Calls: `viewVendor(vendor.vendor_code)`
|
||||
- Navigates to: `/admin/vendors/{code}`
|
||||
|
||||
2. **Edit Button** (✏️ Pencil icon - Purple) ⭐ NEW
|
||||
- Opens edit form
|
||||
- Calls: `editVendor(vendor.vendor_code)`
|
||||
- Navigates to: `/admin/vendors/{code}/edit`
|
||||
|
||||
3. **Delete Button** (🗑 Trash icon - Red)
|
||||
- Deletes vendor with confirmation
|
||||
- Calls: `deleteVendor(vendor)`
|
||||
- Shows confirmation dialog first
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing the Pagination
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
**Test 1: With few vendors (<10)**
|
||||
- Pagination should not show if only 1 page
|
||||
- All vendors visible on page 1
|
||||
|
||||
**Test 2: With many vendors (>10)**
|
||||
- Pagination controls appear
|
||||
- Can navigate between pages
|
||||
- "Previous" disabled on page 1
|
||||
- "Next" disabled on last page
|
||||
|
||||
**Test 3: Navigation**
|
||||
- Click page numbers to jump to specific page
|
||||
- Click "Previous" arrow to go back
|
||||
- Click "Next" arrow to go forward
|
||||
- Page numbers update dynamically
|
||||
|
||||
**Test 4: After Delete**
|
||||
- Delete a vendor
|
||||
- Data reloads
|
||||
- Returns to page 1
|
||||
- Pagination updates
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Styling
|
||||
|
||||
The pagination uses the same Windmill theme as your dashboard:
|
||||
|
||||
- **Normal buttons**: Gray with hover effect
|
||||
- **Current page**: Purple background (matches your theme)
|
||||
- **Disabled buttons**: 50% opacity, cursor not-allowed
|
||||
- **Hover effects**: Light gray background
|
||||
- **Dark mode**: Full support
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Customization Options
|
||||
|
||||
### Change Items Per Page
|
||||
In `vendors.js`:
|
||||
```javascript
|
||||
itemsPerPage: 20, // Show 20 vendors per page
|
||||
```
|
||||
|
||||
### Change Page Number Display Logic
|
||||
In `vendors.js`, modify the `pageNumbers` computed property:
|
||||
```javascript
|
||||
get pageNumbers() {
|
||||
// Modify this logic to change how page numbers appear
|
||||
}
|
||||
```
|
||||
|
||||
### Change Pagination Position
|
||||
In `vendors.html`, the pagination footer is at line ~220.
|
||||
Move this entire `<div class="grid px-4 py-3...">` section.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Code Comparison
|
||||
|
||||
### Before (No Pagination)
|
||||
```javascript
|
||||
// vendors.js
|
||||
vendors: [], // All vendors displayed at once
|
||||
|
||||
// vendors.html
|
||||
<template x-for="vendor in vendors">
|
||||
```
|
||||
|
||||
### After (With Pagination)
|
||||
```javascript
|
||||
// vendors.js
|
||||
vendors: [],
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
get paginatedVendors() {
|
||||
return this.vendors.slice(start, end);
|
||||
}
|
||||
|
||||
// vendors.html
|
||||
<template x-for="vendor in paginatedVendors">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Installation Checklist
|
||||
|
||||
Replace these files in your project:
|
||||
|
||||
- [ ] `static/admin/js/vendors.js` → Use `vendors-with-pagination.js`
|
||||
- [ ] `templates/admin/vendors.html` → Use `vendors-with-pagination.html`
|
||||
- [ ] Clear browser cache (Ctrl+Shift+R)
|
||||
- [ ] Test with >10 vendors to see pagination
|
||||
- [ ] Test edit button works
|
||||
- [ ] Test page navigation
|
||||
- [ ] Test in dark mode
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Pagination not showing?
|
||||
- Make sure you have more than 10 vendors
|
||||
- Check `itemsPerPage` value in vendors.js
|
||||
- Verify `paginatedVendors` is used in template
|
||||
|
||||
### Edit button not working?
|
||||
- Check browser console for errors
|
||||
- Verify `editVendor()` method exists in vendors.js
|
||||
- Check the route exists on your backend: `/admin/vendors/{code}/edit`
|
||||
|
||||
### Page numbers not updating?
|
||||
- This is a computed property - it should update automatically
|
||||
- Check Alpine.js is loaded correctly
|
||||
- Clear browser cache
|
||||
|
||||
### "Showing X-Y of Z" incorrect?
|
||||
- Check `startIndex` and `endIndex` computed properties
|
||||
- Verify `vendors.length` is correct
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips
|
||||
|
||||
1. **Performance**: Pagination is client-side. For 1000+ vendors, consider server-side pagination.
|
||||
|
||||
2. **State Preservation**: When returning from edit page, user goes back to page 1. To preserve page state, you'd need to:
|
||||
- Store `currentPage` in localStorage
|
||||
- Restore it on page load
|
||||
|
||||
3. **Search/Filter**: To add search, filter `vendors` array first, then paginate the filtered results.
|
||||
|
||||
4. **Sorting**: Add sorting before pagination to maintain consistent page content.
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Features Summary
|
||||
|
||||
✅ Edit button added (pencil icon)
|
||||
✅ Smart pagination (10 items per page)
|
||||
✅ Dynamic page numbers with ellipsis
|
||||
✅ Previous/Next navigation
|
||||
✅ Disabled states for boundary pages
|
||||
✅ "Showing X-Y of Z" display
|
||||
✅ Dark mode support
|
||||
✅ Fully responsive
|
||||
✅ Follows Windmill design theme
|
||||
|
||||
Happy paginating! 📖
|
||||
201
docs/__temp/FRONTEND/PAGINATION_QUICK_START.txt
Normal file
201
docs/__temp/FRONTEND/PAGINATION_QUICK_START.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ PAGINATION FEATURE - QUICK START ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
✨ WHAT'S NEW
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
1. ✅ Edit Button - Pencil icon already present in table
|
||||
2. ✅ Pagination - Smart pagination with 10 items per page
|
||||
3. ✅ Page Navigation - Previous/Next buttons + page numbers
|
||||
4. ✅ Smart Display - Shows "..." for large page ranges
|
||||
|
||||
|
||||
📦 INSTALLATION (3 STEPS)
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Step 1: Backup current files
|
||||
$ cp templates/admin/vendors.html templates/admin/vendors.html.backup
|
||||
$ cp static/admin/js/vendors.js static/admin/js/vendors.js.backup
|
||||
|
||||
Step 2: Replace with new versions
|
||||
$ cp vendors.html templates/admin/vendors.html
|
||||
$ cp vendors.js static/admin/js/vendors.js
|
||||
|
||||
Step 3: Clear cache and test
|
||||
- Hard refresh: Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
|
||||
- Test with >10 vendors to see pagination
|
||||
|
||||
|
||||
🎨 VISUAL PREVIEW
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Before (No Pagination):
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Vendor │ Subdomain │ Status │ Created │ Actions │
|
||||
├────────┼───────────┼────────┼─────────┼───────────────────┤
|
||||
│ Vendor1│ vendor1 │ ✓ │ Jan 1 │ 👁 🗑 │ ❌ No edit
|
||||
│ Vendor2│ vendor2 │ ⏰ │ Jan 2 │ 👁 🗑 │
|
||||
│ ...50 more vendors... │
|
||||
│ Vendor52│vendor52 │ ✓ │ Jan 52 │ 👁 🗑 │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
❌ All vendors on one page (hard to scroll through!)
|
||||
|
||||
|
||||
After (With Pagination):
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Vendor │ Subdomain │ Status │ Created │ Actions │
|
||||
├────────┼───────────┼────────┼─────────┼───────────────────┤
|
||||
│ Vendor1│ vendor1 │ ✓ │ Jan 1 │ 👁 ✏️ 🗑 │ ✅ Edit added!
|
||||
│ Vendor2│ vendor2 │ ⏰ │ Jan 2 │ 👁 ✏️ 🗑 │
|
||||
│ ...8 more vendors... │
|
||||
│Vendor10│ vendor10 │ ✓ │ Jan 10 │ 👁 ✏️ 🗑 │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Showing 1-10 of 52 ← 1 [2] 3 4 5 ... 9 → │ ✅ Pagination!
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
🎯 ACTION BUTTONS (3 BUTTONS PER ROW)
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
1. 👁 View (Blue) ─────────→ View vendor details
|
||||
2. ✏️ Edit (Purple) ⭐ NEW → Edit vendor form
|
||||
3. 🗑 Delete (Red) ────────→ Delete with confirmation
|
||||
|
||||
|
||||
🔢 PAGINATION CONTROLS
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
Page 1 of 10:
|
||||
[←] 1 2 3 4 5 ... 9 10 [→]
|
||||
^ ^ ^ ^ ^
|
||||
| | | | └─ Next button
|
||||
| | | └──── Last page
|
||||
| | └─────── Ellipsis (...)
|
||||
| └───────────────────── Page numbers
|
||||
└──────────────────────── Previous button (disabled)
|
||||
|
||||
Page 5 of 10:
|
||||
[←] 1 ... 4 [5] 6 ... 10 [→]
|
||||
↑ ↑ ↑
|
||||
| | └─ Next page
|
||||
| └───── Current page (purple)
|
||||
└───────── Previous pages
|
||||
|
||||
|
||||
📊 HOW IT WORKS
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
1. Template displays: paginatedVendors (not all vendors)
|
||||
2. Alpine.js computes: Which 10 vendors to show
|
||||
3. User clicks page: currentPage updates
|
||||
4. Template refreshes: Shows new 10 vendors
|
||||
|
||||
Simple and reactive! 🎉
|
||||
|
||||
|
||||
⚙️ CONFIGURATION
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
Change items per page in vendors.js:
|
||||
|
||||
// Find this line:
|
||||
itemsPerPage: 10,
|
||||
|
||||
// Change to any number:
|
||||
itemsPerPage: 25, // Show 25 per page
|
||||
itemsPerPage: 50, // Show 50 per page
|
||||
|
||||
|
||||
🧪 TESTING CHECKLIST
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
After installation:
|
||||
|
||||
□ Vendors page loads
|
||||
□ If <10 vendors: No pagination shows (correct!)
|
||||
□ If >10 vendors: Pagination appears
|
||||
□ Can click page numbers to navigate
|
||||
□ Can click ← → arrows
|
||||
□ "Showing X-Y of Z" updates correctly
|
||||
□ Edit button (pencil icon) appears
|
||||
□ Edit button navigates to edit page
|
||||
□ View button still works
|
||||
□ Delete button still works
|
||||
□ Dark mode works
|
||||
□ Pagination styling matches theme
|
||||
|
||||
|
||||
🎨 FEATURES
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
✅ Smart pagination (10 per page)
|
||||
✅ Edit button added (purple pencil)
|
||||
✅ Dynamic page numbers with "..."
|
||||
✅ Previous/Next disabled at boundaries
|
||||
✅ Shows "X-Y of Total" count
|
||||
✅ Dark mode compatible
|
||||
✅ Windmill theme styling
|
||||
✅ Fully responsive
|
||||
✅ Client-side pagination (fast!)
|
||||
✅ Auto-reset to page 1 after data reload
|
||||
|
||||
|
||||
💡 TIPS
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
1. Need more vendors per page?
|
||||
Change itemsPerPage in vendors.js
|
||||
|
||||
2. Want server-side pagination?
|
||||
For 1000+ vendors, consider API pagination
|
||||
|
||||
3. Want to preserve page on refresh?
|
||||
Add localStorage for currentPage
|
||||
|
||||
4. Want to add search/filter?
|
||||
Filter vendors array first, then paginate
|
||||
|
||||
5. Page numbers look weird?
|
||||
Check you have enough vendors (need >10 to see pagination)
|
||||
|
||||
|
||||
🆘 TROUBLESHOOTING
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
Problem: Pagination not showing
|
||||
→ Need more than 10 vendors to see it
|
||||
→ Check itemsPerPage value
|
||||
|
||||
Problem: Edit button doesn't work
|
||||
→ Check backend route exists: /admin/vendors/{code}/edit
|
||||
→ Check browser console for errors
|
||||
|
||||
Problem: Page numbers stuck
|
||||
→ Clear browser cache (Ctrl+Shift+R)
|
||||
→ Check Alpine.js loaded correctly
|
||||
|
||||
Problem: "Showing 0-0 of 0"
|
||||
→ Vendors not loading from API
|
||||
→ Check API endpoint /admin/vendors
|
||||
|
||||
|
||||
📁 FILES INCLUDED
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
vendors.html ................. Updated template with pagination
|
||||
vendors.js ................... Updated script with pagination logic
|
||||
PAGINATION_DOCUMENTATION.md .. Full technical documentation
|
||||
PAGINATION_QUICK_START.txt ... This file (quick reference)
|
||||
|
||||
|
||||
📖 LEARN MORE
|
||||
═════════════════════════════════════════════════════════════
|
||||
|
||||
For detailed technical explanation, see:
|
||||
→ PAGINATION_DOCUMENTATION.md
|
||||
|
||||
|
||||
══════════════════════════════════════════════════════════════
|
||||
Pagination made easy! 📖✨
|
||||
══════════════════════════════════════════════════════════════
|
||||
128
docs/__temp/FRONTEND/frontend-structure.txt
Normal file
128
docs/__temp/FRONTEND/frontend-structure.txt
Normal file
@@ -0,0 +1,128 @@
|
||||
Frontend Folder Structure
|
||||
Generated: 25/10/2025 16:45:08.55
|
||||
==============================================================================
|
||||
|
||||
Folder PATH listing for volume Data2
|
||||
Volume serial number is 0000007B A008:CC27
|
||||
E:\FASTAPI-MULTITENANT-ECOMMERCE\STATIC
|
||||
+---admin
|
||||
| | marketplace.html
|
||||
| | monitoring.html
|
||||
| | users.html
|
||||
| | vendor-edit.html
|
||||
| | vendors.html
|
||||
| |
|
||||
| +---css
|
||||
| | tailwind.output.css
|
||||
| |
|
||||
| +---img
|
||||
| | create-account-office-dark.jpeg
|
||||
| | create-account-office.jpeg
|
||||
| | forgot-password-office-dark.jpeg
|
||||
| | forgot-password-office.jpeg
|
||||
| | login-office-dark.jpeg
|
||||
| | login-office.jpeg
|
||||
| |
|
||||
| +---js
|
||||
| | analytics.js
|
||||
| | components.js
|
||||
| | dashboard.js
|
||||
| | icons-page.js
|
||||
| | init-alpine.js
|
||||
| | login.js
|
||||
| | monitoring.js
|
||||
| | testing-hub.js
|
||||
| | users.js
|
||||
| | vendor-detail.js
|
||||
| | vendor-edit.js
|
||||
| | vendors.js
|
||||
| |
|
||||
| \---partials
|
||||
| base-layout.html
|
||||
|
|
||||
+---css
|
||||
| +---admin
|
||||
| | admin.css
|
||||
| |
|
||||
| +---shared
|
||||
| | auth.css
|
||||
| | base.css
|
||||
| | components.css
|
||||
| | modals.css
|
||||
| | responsive-utilities.css
|
||||
| |
|
||||
| +---shop
|
||||
| +---themes
|
||||
| \---vendor
|
||||
| vendor.css
|
||||
|
|
||||
+---js
|
||||
| +---shared
|
||||
| | alpine-components.js
|
||||
| | media-upload.js
|
||||
| | modal-system.js
|
||||
| | modal-templates.js
|
||||
| | notification.js
|
||||
| | search.js
|
||||
| | vendor-context.js
|
||||
| |
|
||||
| +---shop
|
||||
| | account.js
|
||||
| | cart.js
|
||||
| | catalog.js
|
||||
| | checkout.js
|
||||
| | search.js
|
||||
| | shop-layout-templates.js
|
||||
| |
|
||||
| \---vendor
|
||||
| dashboard.js
|
||||
| login.js
|
||||
| marketplace.js
|
||||
| media.js
|
||||
| orders.js
|
||||
| payments.js
|
||||
| products.js
|
||||
| vendor-layout-templates.js
|
||||
|
|
||||
+---shared
|
||||
| \---js
|
||||
| api-client.js
|
||||
| icons.js
|
||||
| utils.js
|
||||
|
|
||||
+---shop
|
||||
| | cart.html
|
||||
| | checkout.html
|
||||
| | home.html
|
||||
| | product.html
|
||||
| | products.html
|
||||
| | search.html
|
||||
| |
|
||||
| \---account
|
||||
| addresses.html
|
||||
| login.html
|
||||
| orders.html
|
||||
| profile.html
|
||||
| register.html
|
||||
|
|
||||
\---vendor
|
||||
| dashboard.html
|
||||
| login.html
|
||||
|
|
||||
\---admin
|
||||
| customers.html
|
||||
| inventory.html
|
||||
| media.html
|
||||
| notifications.html
|
||||
| orders.html
|
||||
| payments.html
|
||||
| products.html
|
||||
| settings.html
|
||||
| teams.html
|
||||
|
|
||||
\---marketplace
|
||||
browse.html
|
||||
config.html
|
||||
imports.html
|
||||
selected.html
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
# Multi-Domain Architecture Diagram
|
||||
|
||||
## Current vs New Architecture
|
||||
|
||||
### BEFORE (Current Setup)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Your FastAPI Application │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Vendor Context Middleware │ │
|
||||
│ │ │ │
|
||||
│ │ Check Host header: │ │
|
||||
│ │ • vendor1.platform.com → Query Vendor.subdomain │ │
|
||||
│ │ • /vendor/vendor1/ → Query Vendor.subdomain │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Database: vendors table │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ id │ subdomain │ name │ is_active │ │ │
|
||||
│ │ ├────┼───────────┼─────────────┼─────────────────────┤ │ │
|
||||
│ │ │ 1 │ vendor1 │ Shop Alpha │ true │ │ │
|
||||
│ │ │ 2 │ vendor2 │ Shop Beta │ true │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Customers access via:
|
||||
→ vendor1.platform.com (production)
|
||||
→ /vendor/vendor1/ (development)
|
||||
```
|
||||
|
||||
### AFTER (With Custom Domains)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Your FastAPI Application │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Enhanced Vendor Context Middleware │ │
|
||||
│ │ │ │
|
||||
│ │ Priority 1: Check if custom domain │ │
|
||||
│ │ • customdomain1.com → Query VendorDomain.domain │ │
|
||||
│ │ │ │
|
||||
│ │ Priority 2: Check if subdomain │ │
|
||||
│ │ • vendor1.platform.com → Query Vendor.subdomain │ │
|
||||
│ │ │ │
|
||||
│ │ Priority 3: Check if path-based │ │
|
||||
│ │ • /vendor/vendor1/ → Query Vendor.subdomain │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Database: vendors table │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ id │ subdomain │ name │ is_active │ │ │
|
||||
│ │ ├────┼───────────┼─────────────┼─────────────────────┤ │ │
|
||||
│ │ │ 1 │ vendor1 │ Shop Alpha │ true │ │ │
|
||||
│ │ │ 2 │ vendor2 │ Shop Beta │ true │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ NEW TABLE: vendor_domains │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ id │ vendor_id │ domain │ is_verified │ │ │
|
||||
│ │ ├────┼───────────┼───────────────────┼───────────────┤ │ │
|
||||
│ │ │ 1 │ 1 │ customdomain1.com │ true │ │ │
|
||||
│ │ │ 2 │ 1 │ shop.alpha.com │ true │ │ │
|
||||
│ │ │ 3 │ 2 │ customdomain2.com │ true │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Customers can now access via:
|
||||
→ customdomain1.com (custom domain - Vendor 1)
|
||||
→ shop.alpha.com (custom domain - Vendor 1)
|
||||
→ customdomain2.com (custom domain - Vendor 2)
|
||||
→ vendor1.platform.com (subdomain - still works!)
|
||||
→ /vendor/vendor1/ (path-based - still works!)
|
||||
```
|
||||
|
||||
## Request Flow Diagram
|
||||
|
||||
### Scenario 1: Customer visits customdomain1.com
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Customer Browser │
|
||||
│ │
|
||||
│ Visit: │
|
||||
│ customdomain1.com │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
│ HTTP Request
|
||||
│ Host: customdomain1.com
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ DNS Resolution │
|
||||
│ │
|
||||
│ customdomain1.com │
|
||||
│ ↓ │
|
||||
│ 123.45.67.89 │ (Your server IP)
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
│ Routes to server
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Nginx/Web Server │
|
||||
│ │
|
||||
│ Receives request │
|
||||
│ server_name _; │ (Accept ALL domains)
|
||||
│ │
|
||||
│ Proxy to FastAPI │
|
||||
│ with Host header │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
│ proxy_set_header Host $host
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ FastAPI Application │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ Vendor Context Middleware │ │
|
||||
│ │ │ │
|
||||
│ │ host = "customdomain1.com" │ │
|
||||
│ │ │ │
|
||||
│ │ Step 1: Is it a custom domain? │ │
|
||||
│ │ not host.endswith("platform.com") → YES │ │
|
||||
│ │ │ │
|
||||
│ │ Step 2: Query vendor_domains table │ │
|
||||
│ │ SELECT * FROM vendor_domains │ │
|
||||
│ │ WHERE domain = 'customdomain1.com' │ │
|
||||
│ │ AND is_active = true │ │
|
||||
│ │ AND is_verified = true │ │
|
||||
│ │ │ │
|
||||
│ │ Result: vendor_id = 1 │ │
|
||||
│ │ │ │
|
||||
│ │ Step 3: Load Vendor 1 │ │
|
||||
│ │ SELECT * FROM vendors WHERE id = 1 │ │
|
||||
│ │ │ │
|
||||
│ │ Step 4: Set request state │ │
|
||||
│ │ request.state.vendor = Vendor(id=1, ...) │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ Route Handler │ │
|
||||
│ │ │ │
|
||||
│ │ @router.get("/") │ │
|
||||
│ │ def shop_home(request): │ │
|
||||
│ │ vendor = request.state.vendor # Vendor 1 │ │
|
||||
│ │ │ │
|
||||
│ │ # All queries auto-scoped to Vendor 1 │ │
|
||||
│ │ products = get_products(vendor.id) │ │
|
||||
│ │ │ │
|
||||
│ │ return render("shop.html", { │ │
|
||||
│ │ "vendor": vendor, │ │
|
||||
│ │ "products": products │ │
|
||||
│ │ }) │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ HTML Response
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Customer Browser │
|
||||
│ │
|
||||
│ Sees: │
|
||||
│ Vendor 1's shop │
|
||||
│ at customdomain1.com│
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Scenario 2: Customer visits vendor1.platform.com (subdomain)
|
||||
```
|
||||
Customer → DNS → Server → Nginx → FastAPI
|
||||
|
||||
FastAPI Middleware:
|
||||
host = "vendor1.platform.com"
|
||||
|
||||
Step 1: Custom domain? NO (ends with .platform.com)
|
||||
Step 2: Subdomain? YES
|
||||
Extract "vendor1"
|
||||
Query: SELECT * FROM vendors
|
||||
WHERE subdomain = 'vendor1'
|
||||
Result: Vendor 1
|
||||
|
||||
request.state.vendor = Vendor 1
|
||||
|
||||
Route → Render Vendor 1's shop
|
||||
```
|
||||
|
||||
### Scenario 3: Development - localhost:8000/vendor/vendor1/
|
||||
```
|
||||
Customer → localhost:8000/vendor/vendor1/
|
||||
|
||||
FastAPI Middleware:
|
||||
host = "localhost:8000"
|
||||
path = "/vendor/vendor1/"
|
||||
|
||||
Step 1: Custom domain? NO (localhost)
|
||||
Step 2: Subdomain? NO (localhost has no subdomain)
|
||||
Step 3: Path-based? YES
|
||||
Extract "vendor1" from path
|
||||
Query: SELECT * FROM vendors
|
||||
WHERE subdomain = 'vendor1'
|
||||
Result: Vendor 1
|
||||
|
||||
request.state.vendor = Vendor 1
|
||||
request.state.clean_path = "/" (strip /vendor/vendor1)
|
||||
|
||||
Route → Render Vendor 1's shop
|
||||
```
|
||||
|
||||
## Database Relationships
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ vendors │
|
||||
├─────────────────────────────────────────┤
|
||||
│ id (PK) │
|
||||
│ subdomain (UNIQUE) │
|
||||
│ name │
|
||||
│ is_active │
|
||||
│ ... │
|
||||
└─────────────────┬───────────────────────┘
|
||||
│
|
||||
│ One-to-Many
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
│ │
|
||||
↓ ↓
|
||||
┌───────────────────┐ ┌─────────────────────┐
|
||||
│ vendor_domains │ │ products │
|
||||
├───────────────────┤ ├─────────────────────┤
|
||||
│ id (PK) │ │ id (PK) │
|
||||
│ vendor_id (FK) │ │ vendor_id (FK) │
|
||||
│ domain (UNIQUE) │ │ name │
|
||||
│ is_primary │ │ price │
|
||||
│ is_active │ │ ... │
|
||||
│ is_verified │ └─────────────────────┘
|
||||
│ verification_token│
|
||||
│ ... │
|
||||
└───────────────────┘
|
||||
|
||||
Example Data:
|
||||
|
||||
vendors:
|
||||
id=1, subdomain='vendor1', name='Shop Alpha'
|
||||
id=2, subdomain='vendor2', name='Shop Beta'
|
||||
|
||||
vendor_domains:
|
||||
id=1, vendor_id=1, domain='customdomain1.com', is_verified=true
|
||||
id=2, vendor_id=1, domain='shop.alpha.com', is_verified=true
|
||||
id=3, vendor_id=2, domain='customdomain2.com', is_verified=true
|
||||
|
||||
products:
|
||||
id=1, vendor_id=1, name='Product A' ← Belongs to Vendor 1
|
||||
id=2, vendor_id=1, name='Product B' ← Belongs to Vendor 1
|
||||
id=3, vendor_id=2, name='Product C' ← Belongs to Vendor 2
|
||||
```
|
||||
|
||||
## Middleware Decision Tree
|
||||
|
||||
```
|
||||
[HTTP Request Received]
|
||||
│
|
||||
↓
|
||||
┌───────────────┐
|
||||
│ Extract Host │
|
||||
│ from headers │
|
||||
└───────┬───────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────┐
|
||||
│ Is admin request? │
|
||||
│ (admin.* or /admin) │
|
||||
└────┬────────────────┬───┘
|
||||
│ YES │ NO
|
||||
↓ │
|
||||
[Skip vendor detection] │
|
||||
Admin routing │
|
||||
↓
|
||||
┌────────────────────────────┐
|
||||
│ Does host end with │
|
||||
│ .platform.com or localhost?│
|
||||
└────┬───────────────────┬───┘
|
||||
│ NO │ YES
|
||||
│ │
|
||||
↓ ↓
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ CUSTOM DOMAIN │ │ Check for subdomain │
|
||||
│ │ │ or path prefix │
|
||||
│ Query: │ │ │
|
||||
│ vendor_domains table │ │ Query: │
|
||||
│ WHERE domain = host │ │ vendors table │
|
||||
│ │ │ WHERE subdomain = X │
|
||||
└──────────┬───────────┘ └──────────┬───────────┘
|
||||
│ │
|
||||
│ │
|
||||
└─────────┬───────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ Vendor found? │
|
||||
└────┬────────┬───┘
|
||||
│ YES │ NO
|
||||
↓ ↓
|
||||
[Set request.state.vendor] [404 or homepage]
|
||||
│
|
||||
↓
|
||||
[Continue to route handler]
|
||||
```
|
||||
|
||||
## Full System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Internet │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
↓ ↓ ↓
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ customdomain1. │ │ vendor1. │ │ admin. │
|
||||
│ com │ │ platform.com │ │ platform.com │
|
||||
│ │ │ │ │ │
|
||||
│ DNS → Server IP │ │ DNS → Server IP │ │ DNS → Server IP │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
└───────────────────┼───────────────────┘
|
||||
│
|
||||
↓
|
||||
┌──────────────────────────────┐
|
||||
│ Cloudflare / Load Balancer │
|
||||
│ (Optional) │
|
||||
│ - SSL Termination │
|
||||
│ - DDoS Protection │
|
||||
│ - CDN │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
↓
|
||||
┌──────────────────────────────┐
|
||||
│ Nginx / Web Server │
|
||||
│ │
|
||||
│ server_name _; │ ← Accept ALL domains
|
||||
│ proxy_pass FastAPI; │
|
||||
│ proxy_set_header Host; │ ← Pass domain info
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
↓
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ FastAPI Application │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Middleware Stack │ │
|
||||
│ │ 1. CORS │ │
|
||||
│ │ 2. Vendor Context ← Detects vendor from domain │ │
|
||||
│ │ 3. Auth │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Route Handlers │ │
|
||||
│ │ - Shop pages (vendor-scoped) │ │
|
||||
│ │ - Admin pages │ │
|
||||
│ │ - API endpoints │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Database Queries │ │
|
||||
│ │ All queries filtered by: │ │
|
||||
│ │ WHERE vendor_id = request.state.vendor.id │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌──────────────────────────────┐
|
||||
│ PostgreSQL Database │
|
||||
│ │
|
||||
│ Tables: │
|
||||
│ - vendors │
|
||||
│ - vendor_domains ← NEW! │
|
||||
│ - products │
|
||||
│ - customers │
|
||||
│ - orders │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
## DNS Configuration Examples
|
||||
|
||||
### Vendor 1 wants to use customdomain1.com
|
||||
|
||||
**At Domain Registrar (GoDaddy/Namecheap/etc):**
|
||||
```
|
||||
Type: A
|
||||
Name: @
|
||||
Value: 123.45.67.89 (your server IP)
|
||||
TTL: 3600
|
||||
|
||||
Type: A
|
||||
Name: www
|
||||
Value: 123.45.67.89
|
||||
TTL: 3600
|
||||
|
||||
Type: TXT
|
||||
Name: _letzshop-verify
|
||||
Value: abc123xyz (verification token from your platform)
|
||||
TTL: 3600
|
||||
```
|
||||
|
||||
**After DNS propagates (5-15 mins):**
|
||||
1. Customer visits customdomain1.com
|
||||
2. DNS resolves to your server
|
||||
3. Nginx accepts request
|
||||
4. FastAPI middleware queries vendor_domains table
|
||||
5. Finds vendor_id = 1
|
||||
6. Shows Vendor 1's shop
|
||||
@@ -0,0 +1,489 @@
|
||||
# Custom Domain Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to implement custom domain support for your multi-tenant e-commerce platform, allowing vendors to use their own domains (e.g., `customdomain1.com`) instead of subdomains (`vendor1.platform.com`).
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
### Current Routing (What You Have)
|
||||
```
|
||||
Development: localhost:8000/vendor/vendor1/ → Vendor 1
|
||||
Production: vendor1.platform.com → Vendor 1
|
||||
Admin: admin.platform.com → Admin Panel
|
||||
```
|
||||
|
||||
### New Routing (What You're Adding)
|
||||
```
|
||||
Custom Domain: customdomain1.com → Vendor 1
|
||||
Custom Domain: shop.mybrand.com → Vendor 2
|
||||
Subdomain: vendor1.platform.com → Vendor 1 (still works!)
|
||||
Path-based: localhost:8000/vendor/vendor1/ → Vendor 1 (still works!)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Domain Mapping Flow
|
||||
|
||||
```
|
||||
Customer visits customdomain1.com
|
||||
↓
|
||||
DNS routes to your server
|
||||
↓
|
||||
Middleware reads Host header: "customdomain1.com"
|
||||
↓
|
||||
Queries vendor_domains table: WHERE domain = 'customdomain1.com'
|
||||
↓
|
||||
Finds VendorDomain record → vendor_id = 1
|
||||
↓
|
||||
Loads Vendor 1 data
|
||||
↓
|
||||
Sets request.state.vendor = Vendor 1
|
||||
↓
|
||||
All queries automatically scoped to Vendor 1
|
||||
↓
|
||||
Customer sees Vendor 1's shop
|
||||
```
|
||||
|
||||
### 2. Priority Order
|
||||
|
||||
The middleware checks in this order:
|
||||
|
||||
1. **Custom Domain** (highest priority)
|
||||
- Checks if host is NOT platform.com/localhost
|
||||
- Queries `vendor_domains` table
|
||||
|
||||
2. **Subdomain**
|
||||
- Checks if host matches `*.platform.com`
|
||||
- Queries `vendors.subdomain` field
|
||||
|
||||
3. **Path-based** (lowest priority - dev only)
|
||||
- Checks if path starts with `/vendor/`
|
||||
- Queries `vendors.subdomain` field
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Add Database Table
|
||||
|
||||
Create `vendor_domains` table to store custom domain mappings:
|
||||
|
||||
```sql
|
||||
CREATE TABLE vendor_domains (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vendor_id INTEGER NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||||
domain VARCHAR(255) NOT NULL UNIQUE,
|
||||
is_primary BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_verified BOOLEAN DEFAULT FALSE,
|
||||
verification_token VARCHAR(100) UNIQUE,
|
||||
verified_at TIMESTAMP WITH TIME ZONE,
|
||||
ssl_status VARCHAR(50) DEFAULT 'pending',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_domain_active ON vendor_domains(domain, is_active);
|
||||
```
|
||||
|
||||
**Files to create:**
|
||||
- `models/database/vendor_domain.py` - Model definition
|
||||
- `alembic/versions/XXX_add_vendor_domains.py` - Migration
|
||||
|
||||
### Step 2: Update Vendor Model
|
||||
|
||||
Add relationship to domains:
|
||||
|
||||
```python
|
||||
# models/database/vendor.py
|
||||
class Vendor(Base):
|
||||
# ... existing fields ...
|
||||
|
||||
domains = relationship(
|
||||
"VendorDomain",
|
||||
back_populates="vendor",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Update Middleware
|
||||
|
||||
The middleware needs to check custom domains BEFORE subdomains.
|
||||
|
||||
**Key changes in `middleware/vendor_context.py`:**
|
||||
|
||||
```python
|
||||
def detect_vendor_context(request: Request):
|
||||
host = request.headers.get("host", "")
|
||||
|
||||
# NEW: Check if it's a custom domain
|
||||
if is_custom_domain(host):
|
||||
return {
|
||||
"domain": normalize_domain(host),
|
||||
"detection_method": "custom_domain"
|
||||
}
|
||||
|
||||
# EXISTING: Check subdomain
|
||||
if is_subdomain(host):
|
||||
return {
|
||||
"subdomain": extract_subdomain(host),
|
||||
"detection_method": "subdomain"
|
||||
}
|
||||
|
||||
# EXISTING: Check path
|
||||
if is_path_based(request.path):
|
||||
return {
|
||||
"subdomain": extract_from_path(request.path),
|
||||
"detection_method": "path"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Create Admin Interface
|
||||
|
||||
Create API endpoints for managing domains:
|
||||
|
||||
```python
|
||||
POST /api/v1/admin/vendors/{vendor_id}/domains - Add domain
|
||||
GET /api/v1/admin/vendors/{vendor_id}/domains - List domains
|
||||
PUT /api/v1/admin/domains/{domain_id} - Update domain
|
||||
DELETE /api/v1/admin/domains/{domain_id} - Remove domain
|
||||
POST /api/v1/admin/domains/{domain_id}/verify - Verify ownership
|
||||
```
|
||||
|
||||
**Files to create:**
|
||||
- `app/api/v1/admin/vendor_domains.py` - API endpoints
|
||||
- `app/templates/admin/vendor_domains.html` - HTML interface
|
||||
|
||||
### Step 5: DNS Verification
|
||||
|
||||
To prevent domain hijacking, verify the vendor owns the domain:
|
||||
|
||||
**Verification Flow:**
|
||||
1. Vendor adds domain in admin panel
|
||||
2. System generates verification token: `abc123xyz`
|
||||
3. Vendor adds DNS TXT record:
|
||||
```
|
||||
Name: _letzshop-verify.customdomain1.com
|
||||
Type: TXT
|
||||
Value: abc123xyz
|
||||
```
|
||||
4. Admin clicks "Verify" button
|
||||
5. System queries DNS, checks for token
|
||||
6. If found, marks domain as verified
|
||||
|
||||
**Code:**
|
||||
```python
|
||||
@router.post("/domains/{domain_id}/verify")
|
||||
def verify_domain(domain_id: int, db: Session):
|
||||
domain = db.query(VendorDomain).get(domain_id)
|
||||
|
||||
# Query DNS for TXT record
|
||||
txt_records = dns.resolver.resolve(
|
||||
f"_letzshop-verify.{domain.domain}",
|
||||
'TXT'
|
||||
)
|
||||
|
||||
# Check if token matches
|
||||
for txt in txt_records:
|
||||
if txt.to_text() == domain.verification_token:
|
||||
domain.is_verified = True
|
||||
db.commit()
|
||||
return {"message": "Verified!"}
|
||||
|
||||
raise HTTPException(400, "Token not found")
|
||||
```
|
||||
|
||||
### Step 6: DNS Configuration
|
||||
|
||||
Vendor must configure their domain's DNS:
|
||||
|
||||
**Option A: Point to Platform (Simple)**
|
||||
```
|
||||
Type: A
|
||||
Name: @
|
||||
Value: 123.45.67.89 (your server IP)
|
||||
|
||||
Type: A
|
||||
Name: www
|
||||
Value: 123.45.67.89
|
||||
```
|
||||
|
||||
**Option B: CNAME to Platform (Better)**
|
||||
```
|
||||
Type: CNAME
|
||||
Name: @
|
||||
Value: platform.com
|
||||
|
||||
Type: CNAME
|
||||
Name: www
|
||||
Value: platform.com
|
||||
```
|
||||
|
||||
**Option C: Cloudflare Proxy (Best)**
|
||||
```
|
||||
Enable Cloudflare proxy → Automatic SSL + CDN
|
||||
```
|
||||
|
||||
### Step 7: SSL/TLS Certificates
|
||||
|
||||
For HTTPS on custom domains, you need SSL certificates.
|
||||
|
||||
**Option A: Wildcard Certificate (Simplest for subdomains)**
|
||||
```bash
|
||||
# Only works for *.platform.com
|
||||
certbot certonly --dns-cloudflare \
|
||||
-d "*.platform.com" \
|
||||
-d "platform.com"
|
||||
```
|
||||
|
||||
**Option B: Let's Encrypt with Certbot (Per-domain)**
|
||||
```bash
|
||||
# For each custom domain
|
||||
certbot certonly --webroot \
|
||||
-w /var/www/html \
|
||||
-d customdomain1.com \
|
||||
-d www.customdomain1.com
|
||||
```
|
||||
|
||||
**Option C: Cloudflare (Recommended)**
|
||||
- Vendor uses Cloudflare for their domain
|
||||
- Cloudflare provides SSL automatically
|
||||
- No server-side certificate management needed
|
||||
|
||||
**Option D: AWS Certificate Manager**
|
||||
- Use with AWS ALB/CloudFront
|
||||
- Automatic certificate provisioning
|
||||
- Free SSL certificates
|
||||
|
||||
### Step 8: Web Server Configuration
|
||||
|
||||
Configure Nginx/Apache to handle multiple domains:
|
||||
|
||||
**Nginx Configuration:**
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/platform
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl http2;
|
||||
|
||||
# Accept ANY domain
|
||||
server_name _;
|
||||
|
||||
# SSL configuration
|
||||
ssl_certificate /etc/letsencrypt/live/platform.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/platform.com/privkey.pem;
|
||||
|
||||
# Pass Host header to FastAPI
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- `server_name _;` accepts ALL domains
|
||||
- `proxy_set_header Host $host;` passes domain to FastAPI
|
||||
- FastAPI middleware reads the Host header to identify vendor
|
||||
|
||||
## Testing Guide
|
||||
|
||||
### Test 1: Subdomain (Existing)
|
||||
```bash
|
||||
# Development
|
||||
curl -H "Host: vendor1.localhost:8000" http://localhost:8000/
|
||||
|
||||
# Production
|
||||
curl https://vendor1.platform.com/
|
||||
```
|
||||
|
||||
### Test 2: Custom Domain (New)
|
||||
```bash
|
||||
# Add to /etc/hosts for testing:
|
||||
127.0.0.1 customdomain1.com
|
||||
|
||||
# Test locally
|
||||
curl -H "Host: customdomain1.com" http://localhost:8000/
|
||||
|
||||
# Production
|
||||
curl https://customdomain1.com/
|
||||
```
|
||||
|
||||
### Test 3: Path-based (Development)
|
||||
```bash
|
||||
curl http://localhost:8000/vendor/vendor1/
|
||||
```
|
||||
|
||||
## Data Flow Example
|
||||
|
||||
### Example: Customer visits customdomain1.com
|
||||
|
||||
**Step 1: DNS Resolution**
|
||||
```
|
||||
customdomain1.com → 123.45.67.89 (your server)
|
||||
```
|
||||
|
||||
**Step 2: HTTP Request**
|
||||
```
|
||||
GET / HTTP/1.1
|
||||
Host: customdomain1.com
|
||||
```
|
||||
|
||||
**Step 3: Nginx Proxy**
|
||||
```
|
||||
Nginx receives request
|
||||
↓
|
||||
Passes to FastAPI with Host header intact
|
||||
↓
|
||||
FastAPI receives: Host = "customdomain1.com"
|
||||
```
|
||||
|
||||
**Step 4: Middleware Processing**
|
||||
```python
|
||||
# vendor_context_middleware runs
|
||||
host = "customdomain1.com"
|
||||
|
||||
# Detect it's a custom domain
|
||||
is_custom = not host.endswith("platform.com") # True
|
||||
|
||||
# Query database
|
||||
vendor_domain = db.query(VendorDomain).filter(
|
||||
VendorDomain.domain == "customdomain1.com",
|
||||
VendorDomain.is_active == True,
|
||||
VendorDomain.is_verified == True
|
||||
).first()
|
||||
|
||||
# vendor_domain.vendor_id = 5
|
||||
# Load vendor
|
||||
vendor = db.query(Vendor).get(5)
|
||||
|
||||
# Set in request state
|
||||
request.state.vendor = vendor # Vendor 5
|
||||
```
|
||||
|
||||
**Step 5: Route Handler**
|
||||
```python
|
||||
@router.get("/")
|
||||
def shop_home(request: Request):
|
||||
vendor = request.state.vendor # Vendor 5
|
||||
|
||||
# All queries automatically filtered to Vendor 5
|
||||
products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor.id
|
||||
).all()
|
||||
|
||||
return templates.TemplateResponse("shop/home.html", {
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"products": products
|
||||
})
|
||||
```
|
||||
|
||||
**Step 6: Response**
|
||||
```
|
||||
Customer sees Vendor 5's shop at customdomain1.com
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. Domain Verification
|
||||
- ALWAYS verify domain ownership via DNS TXT records
|
||||
- Never allow unverified domains to go live
|
||||
- Prevents domain hijacking
|
||||
|
||||
### 2. SSL/TLS
|
||||
- Require HTTPS for custom domains
|
||||
- Validate SSL certificates
|
||||
- Monitor certificate expiration
|
||||
|
||||
### 3. Vendor Isolation
|
||||
- Double-check vendor_id in all queries
|
||||
- Never trust user input for vendor selection
|
||||
- Always use `request.state.vendor`
|
||||
|
||||
### 4. Rate Limiting
|
||||
- Limit domain additions per vendor
|
||||
- Prevent DNS verification spam
|
||||
- Monitor failed verification attempts
|
||||
|
||||
### 5. Reserved Domains
|
||||
- Block platform.com and subdomains
|
||||
- Block reserved names (admin, api, www, mail)
|
||||
- Validate domain format
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue 1: "Domain not found"
|
||||
**Cause:** DNS not pointing to your server
|
||||
**Solution:** Check DNS A/CNAME records
|
||||
|
||||
### Issue 2: SSL certificate error
|
||||
**Cause:** No certificate for custom domain
|
||||
**Solution:** Use Cloudflare or provision certificate
|
||||
|
||||
### Issue 3: Vendor not detected
|
||||
**Cause:** Domain not in vendor_domains table
|
||||
**Solution:** Add domain via admin panel and verify
|
||||
|
||||
### Issue 4: Wrong vendor loaded
|
||||
**Cause:** Multiple vendors with same domain
|
||||
**Solution:** Enforce unique constraint on domain column
|
||||
|
||||
### Issue 5: Verification fails
|
||||
**Cause:** DNS TXT record not found
|
||||
**Solution:** Wait for DNS propagation (5-15 minutes)
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
- [ ] Database migration applied
|
||||
- [ ] Vendor model updated with domains relationship
|
||||
- [ ] Middleware updated to check custom domains
|
||||
- [ ] Admin API endpoints created
|
||||
- [ ] Admin UI created for domain management
|
||||
- [ ] DNS verification implemented
|
||||
- [ ] Web server configured to accept all domains
|
||||
- [ ] SSL certificate strategy decided
|
||||
- [ ] Testing completed (subdomain + custom domain)
|
||||
- [ ] Documentation updated
|
||||
- [ ] Monitoring configured
|
||||
- [ ] Rate limiting added
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### 1. Automatic SSL Provisioning
|
||||
Use certbot or ACME protocol to automatically provision SSL certificates when vendor adds domain.
|
||||
|
||||
### 2. Domain Status Dashboard
|
||||
Show vendors their domain status:
|
||||
- DNS configuration status
|
||||
- SSL certificate status
|
||||
- Traffic analytics per domain
|
||||
|
||||
### 3. Multiple Domains per Vendor
|
||||
Allow vendors to have multiple custom domains pointing to same shop.
|
||||
|
||||
### 4. Domain Transfer
|
||||
Allow transferring domain from one vendor to another.
|
||||
|
||||
### 5. Subdomain Customization
|
||||
Let vendors choose their subdomain: `mybrand.platform.com`
|
||||
|
||||
## Summary
|
||||
|
||||
**What you're adding:**
|
||||
1. Database table to store domain → vendor mappings
|
||||
2. Middleware logic to detect custom domains
|
||||
3. Admin interface to manage domains
|
||||
4. DNS verification system
|
||||
5. Web server configuration for multi-domain
|
||||
|
||||
**What stays the same:**
|
||||
- Existing subdomain routing still works
|
||||
- Path-based routing still works for development
|
||||
- Vendor isolation and security
|
||||
- All existing functionality
|
||||
|
||||
**Result:**
|
||||
Vendors can use their own domains while you maintain a single codebase with multi-tenant architecture!
|
||||
@@ -0,0 +1,400 @@
|
||||
# Custom Domain Support - Executive Summary
|
||||
|
||||
## What You Asked For
|
||||
|
||||
> "I want to deploy multiple shops on multiple different domains like domain1.com/shop1, domain2.com/shop2"
|
||||
|
||||
## What You Currently Have
|
||||
|
||||
Your FastAPI multi-tenant e-commerce platform currently supports:
|
||||
|
||||
1. **Subdomain routing** (production): `vendor1.platform.com` → Vendor 1
|
||||
2. **Path-based routing** (development): `localhost:8000/vendor/vendor1/` → Vendor 1
|
||||
|
||||
## What's Missing
|
||||
|
||||
You **cannot** currently route custom domains like:
|
||||
- `customdomain1.com` → Vendor 1
|
||||
- `shop.mybrand.com` → Vendor 2
|
||||
|
||||
## The Solution
|
||||
|
||||
Add a **domain mapping table** that links custom domains to vendors:
|
||||
|
||||
```
|
||||
customdomain1.com → Vendor 1
|
||||
customdomain2.com → Vendor 2
|
||||
```
|
||||
|
||||
Your middleware checks this table BEFORE checking subdomains.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
### Current Flow
|
||||
```
|
||||
Customer → vendor1.platform.com → Middleware checks subdomain → Finds Vendor 1
|
||||
```
|
||||
|
||||
### New Flow
|
||||
```
|
||||
Customer → customdomain1.com → Middleware checks domain mapping → Finds Vendor 1
|
||||
```
|
||||
|
||||
### Priority Order (after implementation)
|
||||
1. **Custom domain** (check `vendor_domains` table)
|
||||
2. **Subdomain** (check `vendors.subdomain` - still works!)
|
||||
3. **Path-based** (development mode - still works!)
|
||||
|
||||
## What Changes
|
||||
|
||||
### ✅ Changes Required
|
||||
|
||||
1. **Database**: Add `vendor_domains` table
|
||||
2. **Model**: Create `VendorDomain` model
|
||||
3. **Middleware**: Update to check custom domains first
|
||||
4. **Config**: Add `platform_domain` setting
|
||||
|
||||
### ❌ What Stays the Same
|
||||
|
||||
- Existing subdomain routing (still works!)
|
||||
- Path-based development routing (still works!)
|
||||
- Vendor isolation and security
|
||||
- All existing functionality
|
||||
- API endpoints
|
||||
- Admin panel
|
||||
|
||||
## Implementation Overview
|
||||
|
||||
### Database Schema
|
||||
```sql
|
||||
vendor_domains
|
||||
├── id (PK)
|
||||
├── vendor_id (FK → vendors.id)
|
||||
├── domain (UNIQUE) - e.g., "customdomain1.com"
|
||||
├── is_active
|
||||
├── is_verified (prevents domain hijacking)
|
||||
└── verification_token (for DNS verification)
|
||||
```
|
||||
|
||||
### Middleware Logic
|
||||
```python
|
||||
def detect_vendor(request):
|
||||
host = request.host
|
||||
|
||||
# NEW: Check if custom domain
|
||||
if not host.endswith("platform.com"):
|
||||
vendor = find_by_custom_domain(host)
|
||||
if vendor:
|
||||
return vendor
|
||||
|
||||
# EXISTING: Check subdomain
|
||||
if is_subdomain(host):
|
||||
vendor = find_by_subdomain(host)
|
||||
return vendor
|
||||
|
||||
# EXISTING: Check path
|
||||
if is_path_based(request.path):
|
||||
vendor = find_by_path(request.path)
|
||||
return vendor
|
||||
```
|
||||
|
||||
## Real-World Example
|
||||
|
||||
### Vendor Setup Process
|
||||
|
||||
**Step 1: Admin adds domain**
|
||||
- Admin logs in
|
||||
- Navigates to Vendor → Domains
|
||||
- Adds "customdomain1.com"
|
||||
- Gets verification token: `abc123xyz`
|
||||
|
||||
**Step 2: Vendor configures DNS**
|
||||
At their domain registrar (GoDaddy/Namecheap):
|
||||
```
|
||||
Type: A
|
||||
Name: @
|
||||
Value: 123.45.67.89 (your server IP)
|
||||
|
||||
Type: TXT
|
||||
Name: _letzshop-verify
|
||||
Value: abc123xyz
|
||||
```
|
||||
|
||||
**Step 3: Verification**
|
||||
- Wait 5-15 minutes for DNS propagation
|
||||
- Admin clicks "Verify Domain"
|
||||
- System checks DNS for TXT record
|
||||
- Domain marked as verified ✓
|
||||
|
||||
**Step 4: Go Live**
|
||||
- Customer visits `customdomain1.com`
|
||||
- Sees Vendor 1's shop
|
||||
- Everything just works!
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### Server Side
|
||||
- **Nginx**: Accept all domains (`server_name _;`)
|
||||
- **FastAPI**: Updated middleware
|
||||
- **Database**: New table for domain mappings
|
||||
|
||||
### DNS Side
|
||||
- Vendor points domain to your server
|
||||
- A record: `@` → Your server IP
|
||||
- Verification: TXT record for ownership proof
|
||||
|
||||
### SSL/TLS
|
||||
Three options:
|
||||
1. **Cloudflare** (easiest - automatic SSL)
|
||||
2. **Let's Encrypt** (per-domain certificates)
|
||||
3. **Wildcard** (subdomains only)
|
||||
|
||||
## Security
|
||||
|
||||
### Domain Verification
|
||||
✅ Prevents domain hijacking
|
||||
✅ Requires DNS TXT record
|
||||
✅ Token-based verification
|
||||
✅ Vendor must prove ownership
|
||||
|
||||
### Vendor Isolation
|
||||
✅ All queries filtered by vendor_id
|
||||
✅ No cross-vendor data leakage
|
||||
✅ Middleware sets request.state.vendor
|
||||
✅ Enforced at database level
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Platform Owner (You)
|
||||
- ✅ More professional offering
|
||||
- ✅ Enterprise feature for premium vendors
|
||||
- ✅ Competitive advantage
|
||||
- ✅ Higher vendor retention
|
||||
- ✅ Still maintain single codebase
|
||||
|
||||
### For Vendors
|
||||
- ✅ Use their own brand domain
|
||||
- ✅ Better SEO (own domain)
|
||||
- ✅ Professional appearance
|
||||
- ✅ Customer trust
|
||||
- ✅ Marketing benefits
|
||||
|
||||
### For Customers
|
||||
- ✅ Seamless experience
|
||||
- ✅ Trust familiar domain
|
||||
- ✅ No platform branding visible
|
||||
- ✅ Better user experience
|
||||
|
||||
## Implementation Effort
|
||||
|
||||
### Minimal Changes
|
||||
- **New table**: 1 table (`vendor_domains`)
|
||||
- **New model**: 1 file (`vendor_domain.py`)
|
||||
- **Updated middleware**: Modify existing file
|
||||
- **Config**: Add 1 setting
|
||||
|
||||
### Time Estimate
|
||||
- **Core functionality**: 4-6 hours
|
||||
- **Testing**: 2-3 hours
|
||||
- **Production deployment**: 2-4 hours
|
||||
- **Total**: 1-2 days
|
||||
|
||||
### Risk Level
|
||||
- **Low risk**: New feature, doesn't break existing
|
||||
- **Backward compatible**: Old methods still work
|
||||
- **Rollback plan**: Simple database rollback
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Development Testing
|
||||
```bash
|
||||
# Add to /etc/hosts
|
||||
127.0.0.1 testdomain.local
|
||||
|
||||
# Add test data
|
||||
INSERT INTO vendor_domains (vendor_id, domain, is_verified)
|
||||
VALUES (1, 'testdomain.local', true);
|
||||
|
||||
# Test
|
||||
curl http://testdomain.local:8000/
|
||||
```
|
||||
|
||||
### Production Testing
|
||||
1. Add test domain for one vendor
|
||||
2. Configure DNS
|
||||
3. Verify detection works
|
||||
4. Monitor logs
|
||||
5. Roll out to more vendors
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Phase 1: Database
|
||||
- [ ] Create migration
|
||||
- [ ] Apply to development
|
||||
- [ ] Test data insertion
|
||||
- [ ] Apply to production
|
||||
|
||||
### Phase 2: Code
|
||||
- [ ] Create model
|
||||
- [ ] Update middleware
|
||||
- [ ] Update config
|
||||
- [ ] Deploy to development
|
||||
- [ ] Test thoroughly
|
||||
- [ ] Deploy to production
|
||||
|
||||
### Phase 3: Infrastructure
|
||||
- [ ] Update Nginx config
|
||||
- [ ] Test domain acceptance
|
||||
- [ ] Configure SSL strategy
|
||||
- [ ] Set up monitoring
|
||||
|
||||
### Phase 4: Launch
|
||||
- [ ] Document vendor process
|
||||
- [ ] Train admin team
|
||||
- [ ] Test with pilot vendor
|
||||
- [ ] Roll out gradually
|
||||
|
||||
## Monitoring & Maintenance
|
||||
|
||||
### Key Metrics
|
||||
- Number of active custom domains
|
||||
- Domain verification success rate
|
||||
- Traffic per domain
|
||||
- SSL certificate status
|
||||
- Failed domain lookups
|
||||
|
||||
### Regular Tasks
|
||||
- Review unverified domains (weekly)
|
||||
- Check SSL certificates (monthly)
|
||||
- Clean up inactive domains (quarterly)
|
||||
- Monitor DNS changes (automated)
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### ❌ Separate Deployments Per Vendor
|
||||
- **Pros**: Complete isolation
|
||||
- **Cons**: High maintenance, expensive, difficult to update
|
||||
- **Verdict**: Not scalable
|
||||
|
||||
### ❌ Nginx-Only Routing
|
||||
- **Pros**: No application changes
|
||||
- **Cons**: Hard to manage, no database tracking, no verification
|
||||
- **Verdict**: Not maintainable
|
||||
|
||||
### ✅ Database-Driven Domain Mapping (Recommended)
|
||||
- **Pros**: Scalable, maintainable, secure, trackable
|
||||
- **Cons**: Requires implementation effort
|
||||
- **Verdict**: Best solution
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Can add custom domain via admin panel
|
||||
- [ ] DNS verification works
|
||||
- [ ] Custom domain routes to correct vendor
|
||||
- [ ] Subdomain routing still works
|
||||
- [ ] Path-based routing still works
|
||||
- [ ] No cross-vendor data leakage
|
||||
- [ ] SSL works on custom domains
|
||||
- [ ] Monitoring in place
|
||||
- [ ] Documentation complete
|
||||
|
||||
## ROI Analysis
|
||||
|
||||
### Costs
|
||||
- Development: 1-2 days (one-time)
|
||||
- Testing: 0.5 day (one-time)
|
||||
- Maintenance: 1-2 hours/month
|
||||
|
||||
### Benefits
|
||||
- Premium feature for enterprise vendors
|
||||
- Higher vendor retention
|
||||
- Competitive advantage
|
||||
- Professional appearance
|
||||
- Better vendor acquisition
|
||||
|
||||
### Break-Even
|
||||
If even 5 vendors are willing to pay $50/month extra for custom domain feature:
|
||||
- Revenue: $250/month = $3,000/year
|
||||
- Cost: 2 days development ≈ $2,000
|
||||
- **Break-even in ~8 months**
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate (This Week)
|
||||
1. Review implementation guides
|
||||
2. Test locally with demo domain
|
||||
3. Verify approach with team
|
||||
|
||||
### Short-Term (This Month)
|
||||
1. Implement database changes
|
||||
2. Update middleware
|
||||
3. Deploy to staging
|
||||
4. Test with pilot vendor
|
||||
|
||||
### Long-Term (This Quarter)
|
||||
1. Add admin UI
|
||||
2. Document vendor process
|
||||
3. Roll out to all vendors
|
||||
4. Market as premium feature
|
||||
|
||||
## Resources Provided
|
||||
|
||||
### Documentation
|
||||
1. **QUICK_START.md** - Get started in 30 minutes
|
||||
2. **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md** - Complete guide
|
||||
3. **ARCHITECTURE_DIAGRAMS.md** - Visual architecture
|
||||
4. **IMPLEMENTATION_CHECKLIST.md** - Step-by-step tasks
|
||||
|
||||
### Code Files
|
||||
1. **vendor_domain_model.py** - Database model
|
||||
2. **updated_vendor_context.py** - Updated middleware
|
||||
3. **vendor_domains_api.py** - Admin API endpoints
|
||||
4. **migration_vendor_domains.py** - Database migration
|
||||
5. **config_updates.py** - Configuration changes
|
||||
|
||||
## Questions & Answers
|
||||
|
||||
**Q: Will this break existing functionality?**
|
||||
A: No! Subdomain and path-based routing still work. This adds a new layer on top.
|
||||
|
||||
**Q: What about SSL certificates?**
|
||||
A: Recommend Cloudflare (automatic) or Let's Encrypt (per-domain). See full guide.
|
||||
|
||||
**Q: How do vendors point their domain?**
|
||||
A: They add an A record pointing to your server IP. Simple DNS configuration.
|
||||
|
||||
**Q: What prevents domain hijacking?**
|
||||
A: DNS verification via TXT record. Vendor must prove ownership before domain goes live.
|
||||
|
||||
**Q: Can one vendor have multiple domains?**
|
||||
A: Yes! The `vendor_domains` table supports multiple domains per vendor.
|
||||
|
||||
**Q: What if vendor removes domain later?**
|
||||
A: Just mark as `is_active = false` in database. Easy to deactivate.
|
||||
|
||||
**Q: Do I need separate servers per domain?**
|
||||
A: No! Single server accepts all domains. Middleware routes to correct vendor.
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Is your current architecture capable?**
|
||||
✅ Yes! Your multi-tenant architecture is perfect for this.
|
||||
|
||||
**What needs to change?**
|
||||
✅ Minimal changes: 1 table, 1 model, middleware update
|
||||
|
||||
**Is it worth it?**
|
||||
✅ Yes! Enterprise feature, competitive advantage, premium pricing
|
||||
|
||||
**Risk level?**
|
||||
✅ Low! Backward compatible, rollback-friendly
|
||||
|
||||
**Implementation complexity?**
|
||||
✅ Medium! 1-2 days for experienced FastAPI developer
|
||||
|
||||
**Recommendation:**
|
||||
✅ **GO FOR IT!** This is a valuable feature that fits naturally into your architecture.
|
||||
|
||||
---
|
||||
|
||||
**Ready to start?** Begin with `QUICK_START.md` for a 30-minute implementation!
|
||||
@@ -0,0 +1,466 @@
|
||||
# Custom Domain Implementation Checklist
|
||||
|
||||
## Phase 1: Database Setup
|
||||
|
||||
### Step 1.1: Create VendorDomain Model
|
||||
- [ ] Create file: `models/database/vendor_domain.py`
|
||||
- [ ] Copy model code from `vendor_domain_model.py`
|
||||
- [ ] Import in `models/database/__init__.py`
|
||||
|
||||
### Step 1.2: Update Vendor Model
|
||||
- [ ] Open `models/database/vendor.py`
|
||||
- [ ] Add `domains` relationship
|
||||
- [ ] Add `primary_domain` and `all_domains` properties
|
||||
|
||||
### Step 1.3: Create Migration
|
||||
- [ ] Generate migration: `alembic revision -m "add vendor domains"`
|
||||
- [ ] Copy upgrade/downgrade code from `migration_vendor_domains.py`
|
||||
- [ ] Apply migration: `alembic upgrade head`
|
||||
- [ ] Verify table exists: `psql -c "\d vendor_domains"`
|
||||
|
||||
## Phase 2: Configuration
|
||||
|
||||
### Step 2.1: Update Settings
|
||||
- [ ] Open `app/core/config.py`
|
||||
- [ ] Add `platform_domain = "platform.com"` (change to your actual domain)
|
||||
- [ ] Add custom domain settings from `config_updates.py`
|
||||
- [ ] Update `.env` file with new settings
|
||||
|
||||
### Step 2.2: Test Settings
|
||||
```bash
|
||||
# In Python shell
|
||||
from app.core.config import settings
|
||||
print(settings.platform_domain) # Should print your domain
|
||||
```
|
||||
|
||||
## Phase 3: Middleware Update
|
||||
|
||||
### Step 3.1: Backup Current Middleware
|
||||
```bash
|
||||
cp middleware/vendor_context.py middleware/vendor_context.py.backup
|
||||
```
|
||||
|
||||
### Step 3.2: Update Middleware
|
||||
- [ ] Open `middleware/vendor_context.py`
|
||||
- [ ] Replace with code from `updated_vendor_context.py`
|
||||
- [ ] Review the three detection methods (custom domain, subdomain, path)
|
||||
- [ ] Check imports are correct
|
||||
|
||||
### Step 3.3: Test Middleware Detection
|
||||
Create test file `tests/test_vendor_context.py`:
|
||||
```python
|
||||
def test_custom_domain_detection():
|
||||
# Mock request with custom domain
|
||||
request = MockRequest(host="customdomain1.com")
|
||||
context = VendorContextManager.detect_vendor_context(request)
|
||||
assert context["detection_method"] == "custom_domain"
|
||||
assert context["domain"] == "customdomain1.com"
|
||||
|
||||
def test_subdomain_detection():
|
||||
request = MockRequest(host="vendor1.platform.com")
|
||||
context = VendorContextManager.detect_vendor_context(request)
|
||||
assert context["detection_method"] == "subdomain"
|
||||
assert context["subdomain"] == "vendor1"
|
||||
|
||||
def test_path_detection():
|
||||
request = MockRequest(host="localhost", path="/vendor/vendor1/")
|
||||
context = VendorContextManager.detect_vendor_context(request)
|
||||
assert context["detection_method"] == "path"
|
||||
assert context["subdomain"] == "vendor1"
|
||||
```
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
pytest tests/test_vendor_context.py -v
|
||||
```
|
||||
|
||||
## Phase 4: Admin API Endpoints
|
||||
|
||||
### Step 4.1: Create Vendor Domains Router
|
||||
- [ ] Create file: `app/api/v1/admin/vendor_domains.py`
|
||||
- [ ] Copy code from `vendor_domains_api.py`
|
||||
- [ ] Verify imports work
|
||||
|
||||
### Step 4.2: Register Router
|
||||
Edit `app/api/v1/admin/__init__.py`:
|
||||
```python
|
||||
from .vendor_domains import router as vendor_domains_router
|
||||
|
||||
# In your admin router setup:
|
||||
admin_router.include_router(
|
||||
vendor_domains_router,
|
||||
prefix="/vendors",
|
||||
tags=["vendor-domains"]
|
||||
)
|
||||
```
|
||||
|
||||
### Step 4.3: Test API Endpoints
|
||||
```bash
|
||||
# Start server
|
||||
uvicorn main:app --reload
|
||||
|
||||
# Test endpoints (use Postman or curl)
|
||||
# 1. List vendor domains
|
||||
curl -H "Authorization: Bearer {admin_token}" \
|
||||
http://localhost:8000/api/v1/admin/vendors/1/domains
|
||||
|
||||
# 2. Add domain
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer {admin_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"vendor_id": 1, "domain": "test.com"}' \
|
||||
http://localhost:8000/api/v1/admin/vendors/1/domains
|
||||
```
|
||||
|
||||
## Phase 5: DNS Verification (Optional but Recommended)
|
||||
|
||||
### Step 5.1: Install DNS Library
|
||||
```bash
|
||||
pip install dnspython
|
||||
```
|
||||
|
||||
### Step 5.2: Test DNS Verification
|
||||
```python
|
||||
# In Python shell
|
||||
import dns.resolver
|
||||
|
||||
# Test querying TXT record
|
||||
answers = dns.resolver.resolve("_letzshop-verify.example.com", "TXT")
|
||||
for txt in answers:
|
||||
print(txt.to_text())
|
||||
```
|
||||
|
||||
## Phase 6: Local Testing
|
||||
|
||||
### Step 6.1: Test with /etc/hosts
|
||||
Edit `/etc/hosts`:
|
||||
```
|
||||
127.0.0.1 testdomain1.local
|
||||
127.0.0.1 testdomain2.local
|
||||
```
|
||||
|
||||
### Step 6.2: Add Test Data
|
||||
```sql
|
||||
-- Add test vendor
|
||||
INSERT INTO vendors (subdomain, name, is_active)
|
||||
VALUES ('testvendor', 'Test Vendor', true);
|
||||
|
||||
-- Add test domain
|
||||
INSERT INTO vendor_domains (vendor_id, domain, is_verified, is_active)
|
||||
VALUES (1, 'testdomain1.local', true, true);
|
||||
```
|
||||
|
||||
### Step 6.3: Test in Browser
|
||||
```bash
|
||||
# Start server
|
||||
uvicorn main:app --reload
|
||||
|
||||
# Visit in browser:
|
||||
# http://testdomain1.local:8000/
|
||||
|
||||
# Check logs for:
|
||||
# "✓ Vendor found via custom domain: testdomain1.local → Test Vendor"
|
||||
```
|
||||
|
||||
## Phase 7: Web Server Configuration
|
||||
|
||||
### Step 7.1: Update Nginx Configuration
|
||||
Edit `/etc/nginx/sites-available/your-site`:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl http2;
|
||||
|
||||
# Accept ALL domains
|
||||
server_name _;
|
||||
|
||||
# SSL configuration (update paths)
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host; # CRITICAL: Pass domain to FastAPI
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7.2: Test Nginx Config
|
||||
```bash
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Phase 8: Production Deployment
|
||||
|
||||
### Step 8.1: Pre-deployment Checklist
|
||||
- [ ] All tests passing
|
||||
- [ ] Database migration applied
|
||||
- [ ] Configuration updated in production .env
|
||||
- [ ] Nginx configured to accept all domains
|
||||
- [ ] SSL certificate strategy decided
|
||||
- [ ] Monitoring configured
|
||||
- [ ] Rollback plan ready
|
||||
|
||||
### Step 8.2: Deploy
|
||||
```bash
|
||||
# 1. Pull latest code
|
||||
git pull origin main
|
||||
|
||||
# 2. Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. Run migrations
|
||||
alembic upgrade head
|
||||
|
||||
# 4. Restart application
|
||||
sudo systemctl restart your-app
|
||||
|
||||
# 5. Restart nginx
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Step 8.3: Verify Deployment
|
||||
```bash
|
||||
# Test health endpoint
|
||||
curl https://platform.com/health
|
||||
|
||||
# Check logs
|
||||
tail -f /var/log/your-app/app.log
|
||||
|
||||
# Look for:
|
||||
# "✓ Vendor found via custom domain: ..."
|
||||
```
|
||||
|
||||
## Phase 9: Vendor Setup Process
|
||||
|
||||
### Step 9.1: Admin Adds Domain for Vendor
|
||||
1. Log into admin panel
|
||||
2. Go to Vendors → Select Vendor → Domains
|
||||
3. Click "Add Domain"
|
||||
4. Enter domain: `customdomain1.com`
|
||||
5. Click Save
|
||||
6. Copy verification instructions
|
||||
|
||||
### Step 9.2: Vendor Configures DNS
|
||||
Vendor goes to their domain registrar and adds:
|
||||
|
||||
**A Record:**
|
||||
```
|
||||
Type: A
|
||||
Name: @
|
||||
Value: [your server IP]
|
||||
TTL: 3600
|
||||
```
|
||||
|
||||
**Verification TXT Record:**
|
||||
```
|
||||
Type: TXT
|
||||
Name: _letzshop-verify
|
||||
Value: [token from step 9.1]
|
||||
TTL: 3600
|
||||
```
|
||||
|
||||
### Step 9.3: Admin Verifies Domain
|
||||
1. Wait 5-15 minutes for DNS propagation
|
||||
2. In admin panel → Click "Verify Domain"
|
||||
3. System checks DNS for TXT record
|
||||
4. If found → Domain marked as verified
|
||||
5. Domain is now active!
|
||||
|
||||
### Step 9.4: Test Custom Domain
|
||||
```bash
|
||||
# Visit vendor's custom domain
|
||||
curl https://customdomain1.com/
|
||||
|
||||
# Should show vendor's shop
|
||||
# Check server logs for confirmation
|
||||
```
|
||||
|
||||
## Phase 10: SSL/TLS Setup
|
||||
|
||||
### Option A: Let's Encrypt (Per-Domain)
|
||||
```bash
|
||||
# For each custom domain
|
||||
sudo certbot certonly --webroot \
|
||||
-w /var/www/html \
|
||||
-d customdomain1.com \
|
||||
-d www.customdomain1.com
|
||||
|
||||
# Auto-renewal
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Option B: Cloudflare (Recommended)
|
||||
1. Vendor adds domain to Cloudflare
|
||||
2. Cloudflare provides SSL automatically
|
||||
3. Points Cloudflare DNS to your server
|
||||
4. No server-side certificate needed
|
||||
|
||||
### Option C: Wildcard (Subdomains Only)
|
||||
```bash
|
||||
# Only for *.platform.com
|
||||
sudo certbot certonly --dns-cloudflare \
|
||||
-d "*.platform.com" \
|
||||
-d "platform.com"
|
||||
```
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
### Issue: Vendor not detected
|
||||
**Check:**
|
||||
```sql
|
||||
-- Is domain in database?
|
||||
SELECT * FROM vendor_domains WHERE domain = 'customdomain1.com';
|
||||
|
||||
-- Is domain verified?
|
||||
SELECT * FROM vendor_domains
|
||||
WHERE domain = 'customdomain1.com'
|
||||
AND is_verified = true
|
||||
AND is_active = true;
|
||||
|
||||
-- Is vendor active?
|
||||
SELECT v.* FROM vendors v
|
||||
JOIN vendor_domains vd ON v.id = vd.vendor_id
|
||||
WHERE vd.domain = 'customdomain1.com';
|
||||
```
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
# Look for middleware debug logs
|
||||
grep "Vendor context" /var/log/your-app/app.log
|
||||
|
||||
# Should see:
|
||||
# "🏪 Vendor context: Shop Name (subdomain) via custom_domain"
|
||||
```
|
||||
|
||||
### Issue: Wrong vendor loaded
|
||||
**Check for duplicates:**
|
||||
```sql
|
||||
-- Should be no duplicates
|
||||
SELECT domain, COUNT(*)
|
||||
FROM vendor_domains
|
||||
GROUP BY domain
|
||||
HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
### Issue: DNS verification fails
|
||||
**Check DNS propagation:**
|
||||
```bash
|
||||
# Check if TXT record exists
|
||||
dig _letzshop-verify.customdomain1.com TXT
|
||||
|
||||
# Should show verification token
|
||||
```
|
||||
|
||||
### Issue: SSL certificate error
|
||||
**Options:**
|
||||
1. Use Cloudflare (easiest)
|
||||
2. Get Let's Encrypt certificate for domain
|
||||
3. Tell vendor to use their own SSL proxy
|
||||
|
||||
## Monitoring & Maintenance
|
||||
|
||||
### Add Logging
|
||||
```python
|
||||
# In middleware
|
||||
logger.info(
|
||||
f"Request received for {host}",
|
||||
extra={
|
||||
"host": host,
|
||||
"vendor_id": vendor.id if vendor else None,
|
||||
"detection_method": context.get("detection_method")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Monitor Metrics
|
||||
- [ ] Number of active custom domains
|
||||
- [ ] Failed domain verifications
|
||||
- [ ] Traffic per domain
|
||||
- [ ] SSL certificate expirations
|
||||
|
||||
### Regular Maintenance
|
||||
- [ ] Review unverified domains (> 7 days old)
|
||||
- [ ] Check SSL certificate status
|
||||
- [ ] Monitor DNS changes
|
||||
- [ ] Clean up inactive domains
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Existing subdomain routing still works
|
||||
- [ ] New custom domains can be added via admin
|
||||
- [ ] DNS verification works
|
||||
- [ ] Multiple domains can point to same vendor
|
||||
- [ ] Middleware correctly identifies vendor
|
||||
- [ ] All vendor queries properly scoped
|
||||
- [ ] No security vulnerabilities (domain hijacking prevented)
|
||||
- [ ] Monitoring in place
|
||||
- [ ] Documentation updated
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If something goes wrong:
|
||||
|
||||
1. **Database:**
|
||||
```bash
|
||||
alembic downgrade -1 # Rollback migration
|
||||
```
|
||||
|
||||
2. **Code:**
|
||||
```bash
|
||||
git checkout HEAD~1 # Revert to previous commit
|
||||
sudo systemctl restart your-app
|
||||
```
|
||||
|
||||
3. **Middleware:**
|
||||
```bash
|
||||
cp middleware/vendor_context.py.backup middleware/vendor_context.py
|
||||
sudo systemctl restart your-app
|
||||
```
|
||||
|
||||
4. **Nginx:**
|
||||
```bash
|
||||
# Restore previous nginx config from backup
|
||||
sudo cp /etc/nginx/sites-available/your-site.backup /etc/nginx/sites-available/your-site
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Next Steps After Implementation
|
||||
|
||||
1. **Create Admin UI**
|
||||
- HTML page for domain management
|
||||
- Show verification status
|
||||
- DNS configuration help
|
||||
|
||||
2. **Vendor Dashboard**
|
||||
- Let vendors see their domains
|
||||
- Domain analytics
|
||||
- SSL status
|
||||
|
||||
3. **Automation**
|
||||
- Auto-verify domains via webhook
|
||||
- Auto-provision SSL certificates
|
||||
- Auto-renewal monitoring
|
||||
|
||||
4. **Documentation**
|
||||
- Vendor help docs
|
||||
- Admin guide
|
||||
- API documentation
|
||||
|
||||
5. **Testing**
|
||||
- Load testing with multiple domains
|
||||
- Security audit
|
||||
- Penetration testing
|
||||
|
||||
---
|
||||
|
||||
**Estimated Implementation Time:**
|
||||
- Phase 1-4: 4-6 hours (core functionality)
|
||||
- Phase 5-7: 2-3 hours (testing and deployment)
|
||||
- Phase 8-10: 2-4 hours (production setup and SSL)
|
||||
|
||||
**Total: 1-2 days for full implementation**
|
||||
@@ -0,0 +1,460 @@
|
||||
# Custom Domain Support - Complete Documentation Package
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
This package contains everything you need to add custom domain support to your FastAPI multi-tenant e-commerce platform.
|
||||
|
||||
## 🚀 Where to Start
|
||||
|
||||
### If you want to understand the concept first:
|
||||
👉 **Start here:** [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
|
||||
- High-level overview
|
||||
- What changes and what stays the same
|
||||
- Benefits and ROI analysis
|
||||
- Decision-making guidance
|
||||
|
||||
### If you want to implement quickly:
|
||||
👉 **Start here:** [QUICK_START.md](QUICK_START.md)
|
||||
- Get working in 30 minutes
|
||||
- Minimal steps
|
||||
- Local testing included
|
||||
- Perfect for proof-of-concept
|
||||
|
||||
### If you want complete understanding:
|
||||
👉 **Start here:** [CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md](CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md)
|
||||
- Comprehensive guide
|
||||
- All technical details
|
||||
- Security considerations
|
||||
- Production deployment
|
||||
|
||||
## 📚 Documentation Files
|
||||
|
||||
### 1. EXECUTIVE_SUMMARY.md
|
||||
**What it covers:**
|
||||
- Problem statement and solution
|
||||
- High-level architecture
|
||||
- Benefits analysis
|
||||
- Risk assessment
|
||||
- ROI calculation
|
||||
- Q&A section
|
||||
|
||||
**Read this if:**
|
||||
- You need to explain the feature to stakeholders
|
||||
- You want to understand if it's worth implementing
|
||||
- You need a business case
|
||||
- You want to see the big picture
|
||||
|
||||
**Reading time:** 10 minutes
|
||||
|
||||
---
|
||||
|
||||
### 2. QUICK_START.md
|
||||
**What it covers:**
|
||||
- 5-step implementation (30 minutes)
|
||||
- Database setup
|
||||
- Model creation
|
||||
- Middleware update
|
||||
- Local testing
|
||||
- Troubleshooting
|
||||
|
||||
**Read this if:**
|
||||
- You want to test the concept quickly
|
||||
- You need a working demo
|
||||
- You prefer hands-on learning
|
||||
- You want to validate the approach
|
||||
|
||||
**Implementation time:** 30 minutes
|
||||
|
||||
---
|
||||
|
||||
### 3. CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
|
||||
**What it covers:**
|
||||
- Complete architecture explanation
|
||||
- Step-by-step implementation
|
||||
- DNS configuration
|
||||
- SSL/TLS setup
|
||||
- Security best practices
|
||||
- Testing strategies
|
||||
- Common issues and solutions
|
||||
- Future enhancements
|
||||
|
||||
**Read this if:**
|
||||
- You're doing production implementation
|
||||
- You need comprehensive documentation
|
||||
- You want to understand every detail
|
||||
- You need reference material
|
||||
|
||||
**Reading time:** 45 minutes
|
||||
|
||||
---
|
||||
|
||||
### 4. ARCHITECTURE_DIAGRAMS.md
|
||||
**What it covers:**
|
||||
- Visual architecture diagrams
|
||||
- Request flow illustrations
|
||||
- Database relationship diagrams
|
||||
- Before/after comparisons
|
||||
- DNS configuration examples
|
||||
- Decision tree diagrams
|
||||
|
||||
**Read this if:**
|
||||
- You're a visual learner
|
||||
- You need to explain to others
|
||||
- You want to see data flow
|
||||
- You prefer diagrams to text
|
||||
|
||||
**Reading time:** 20 minutes
|
||||
|
||||
---
|
||||
|
||||
### 5. IMPLEMENTATION_CHECKLIST.md
|
||||
**What it covers:**
|
||||
- Phase-by-phase implementation plan
|
||||
- Detailed task checklist
|
||||
- Testing procedures
|
||||
- Deployment steps
|
||||
- Troubleshooting guide
|
||||
- Rollback plan
|
||||
- Monitoring setup
|
||||
|
||||
**Read this if:**
|
||||
- You're ready to implement in production
|
||||
- You need a project plan
|
||||
- You want to track progress
|
||||
- You need a deployment guide
|
||||
|
||||
**Implementation time:** 1-2 days
|
||||
|
||||
---
|
||||
|
||||
## 💻 Code Files
|
||||
|
||||
All generated code files are in `/home/claude/`:
|
||||
|
||||
### Core Implementation Files
|
||||
|
||||
**1. vendor_domain_model.py**
|
||||
- Complete `VendorDomain` SQLAlchemy model
|
||||
- Domain normalization logic
|
||||
- Relationships and constraints
|
||||
- Ready to use in your project
|
||||
|
||||
**2. updated_vendor_context.py**
|
||||
- Enhanced middleware with custom domain support
|
||||
- Three detection methods (custom domain, subdomain, path)
|
||||
- Vendor lookup logic
|
||||
- Drop-in replacement for your current middleware
|
||||
|
||||
**3. vendor_domains_api.py**
|
||||
- Admin API endpoints for domain management
|
||||
- CRUD operations for domains
|
||||
- DNS verification endpoint
|
||||
- Verification instructions endpoint
|
||||
|
||||
**4. migration_vendor_domains.py**
|
||||
- Alembic migration script
|
||||
- Creates vendor_domains table
|
||||
- All indexes and constraints
|
||||
- Upgrade and downgrade functions
|
||||
|
||||
**5. config_updates.py**
|
||||
- Configuration additions for Settings class
|
||||
- Platform domain setting
|
||||
- Custom domain features toggle
|
||||
- DNS verification settings
|
||||
|
||||
**6. vendor_model_update.py**
|
||||
- Updates to Vendor model
|
||||
- Domains relationship
|
||||
- Helper properties for domain access
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Navigation Guide
|
||||
|
||||
### I want to...
|
||||
|
||||
**...understand what custom domains are**
|
||||
→ Read: EXECUTIVE_SUMMARY.md (Section: "What You Asked For")
|
||||
|
||||
**...see if my architecture can support this**
|
||||
→ Read: EXECUTIVE_SUMMARY.md (Section: "Is your current architecture capable?")
|
||||
→ Answer: ✅ YES! You have exactly what you need.
|
||||
|
||||
**...test it locally in 30 minutes**
|
||||
→ Follow: QUICK_START.md
|
||||
|
||||
**...understand the technical architecture**
|
||||
→ Read: ARCHITECTURE_DIAGRAMS.md
|
||||
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "How It Works")
|
||||
|
||||
**...implement in production**
|
||||
→ Follow: IMPLEMENTATION_CHECKLIST.md
|
||||
→ Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
|
||||
|
||||
**...see the database schema**
|
||||
→ Check: ARCHITECTURE_DIAGRAMS.md (Section: "Database Relationships")
|
||||
→ Use: migration_vendor_domains.py
|
||||
|
||||
**...update my middleware**
|
||||
→ Use: updated_vendor_context.py
|
||||
→ Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "Middleware Update")
|
||||
|
||||
**...add admin endpoints**
|
||||
→ Use: vendor_domains_api.py
|
||||
→ Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "Create Admin Endpoints")
|
||||
|
||||
**...configure DNS**
|
||||
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "DNS Configuration")
|
||||
→ Check: ARCHITECTURE_DIAGRAMS.md (Section: "DNS Configuration Examples")
|
||||
|
||||
**...set up SSL/TLS**
|
||||
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "SSL/TLS Certificates")
|
||||
→ Check: IMPLEMENTATION_CHECKLIST.md (Phase 10)
|
||||
|
||||
**...verify a domain**
|
||||
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "DNS Verification")
|
||||
→ Use: vendor_domains_api.py (verify_domain endpoint)
|
||||
|
||||
**...troubleshoot issues**
|
||||
→ Check: QUICK_START.md (Section: "Common Issues & Fixes")
|
||||
→ Check: IMPLEMENTATION_CHECKLIST.md (Section: "Troubleshooting Guide")
|
||||
|
||||
**...roll back if something goes wrong**
|
||||
→ Follow: IMPLEMENTATION_CHECKLIST.md (Section: "Rollback Plan")
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Recommended Reading Order
|
||||
|
||||
### For First-Time Readers:
|
||||
1. **EXECUTIVE_SUMMARY.md** - Understand the concept (10 min)
|
||||
2. **ARCHITECTURE_DIAGRAMS.md** - See visual flow (20 min)
|
||||
3. **QUICK_START.md** - Test locally (30 min)
|
||||
4. **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md** - Deep dive (45 min)
|
||||
5. **IMPLEMENTATION_CHECKLIST.md** - Plan deployment (review time)
|
||||
|
||||
### For Hands-On Implementers:
|
||||
1. **QUICK_START.md** - Get started immediately
|
||||
2. **ARCHITECTURE_DIAGRAMS.md** - Understand the flow
|
||||
3. **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md** - Reference as needed
|
||||
4. **IMPLEMENTATION_CHECKLIST.md** - Follow for production
|
||||
|
||||
### For Decision Makers:
|
||||
1. **EXECUTIVE_SUMMARY.md** - Complete overview
|
||||
2. **ARCHITECTURE_DIAGRAMS.md** - Visual understanding
|
||||
3. **IMPLEMENTATION_CHECKLIST.md** - See effort required
|
||||
|
||||
---
|
||||
|
||||
## ✅ What You'll Have After Implementation
|
||||
|
||||
### Technical Capabilities:
|
||||
- ✅ Custom domains route to correct vendors
|
||||
- ✅ Subdomain routing still works (backward compatible)
|
||||
- ✅ Path-based routing still works (dev mode)
|
||||
- ✅ DNS verification prevents domain hijacking
|
||||
- ✅ SSL/TLS support for custom domains
|
||||
- ✅ Admin panel for domain management
|
||||
- ✅ Vendor isolation maintained
|
||||
|
||||
### Business Benefits:
|
||||
- ✅ Enterprise feature for premium vendors
|
||||
- ✅ Professional appearance for vendor shops
|
||||
- ✅ Competitive advantage in market
|
||||
- ✅ Higher vendor retention
|
||||
- ✅ Additional revenue stream
|
||||
|
||||
---
|
||||
|
||||
## 📊 Implementation Summary
|
||||
|
||||
### Complexity: **Medium**
|
||||
- New database table: 1
|
||||
- New models: 1
|
||||
- Modified files: 2-3
|
||||
- New API endpoints: 5-7
|
||||
|
||||
### Time Required:
|
||||
- **Quick test**: 30 minutes
|
||||
- **Development**: 4-6 hours
|
||||
- **Testing**: 2-3 hours
|
||||
- **Production deployment**: 2-4 hours
|
||||
- **Total**: 1-2 days
|
||||
|
||||
### Risk Level: **Low**
|
||||
- Backward compatible
|
||||
- No breaking changes
|
||||
- Easy rollback
|
||||
- Well-documented
|
||||
|
||||
### ROI: **High**
|
||||
- Premium feature
|
||||
- Low maintenance
|
||||
- Scalable solution
|
||||
- Competitive advantage
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technology Stack
|
||||
|
||||
Your existing stack works perfectly:
|
||||
- ✅ FastAPI (Python web framework)
|
||||
- ✅ PostgreSQL (database)
|
||||
- ✅ SQLAlchemy (ORM)
|
||||
- ✅ Jinja2 (templates)
|
||||
- ✅ Alpine.js (frontend)
|
||||
- ✅ Nginx (web server)
|
||||
|
||||
No new technologies required!
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Support & Troubleshooting
|
||||
|
||||
### Common Questions
|
||||
All answered in **EXECUTIVE_SUMMARY.md** (Q&A section)
|
||||
|
||||
### Common Issues
|
||||
Listed in **QUICK_START.md** and **IMPLEMENTATION_CHECKLIST.md**
|
||||
|
||||
### Testing Problems
|
||||
Covered in **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md**
|
||||
|
||||
### Production Issues
|
||||
See **IMPLEMENTATION_CHECKLIST.md** (Troubleshooting Guide)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Key Concepts
|
||||
|
||||
### Multi-Tenant Architecture
|
||||
Your app already supports this! Each vendor is a tenant with isolated data.
|
||||
|
||||
### Domain Mapping
|
||||
New concept: Links custom domains to vendors via database table.
|
||||
|
||||
### Request Flow Priority
|
||||
1. Custom domain (NEW)
|
||||
2. Subdomain (EXISTING)
|
||||
3. Path-based (EXISTING)
|
||||
|
||||
### DNS Verification
|
||||
Security feature: Proves vendor owns the domain before activation.
|
||||
|
||||
### Vendor Isolation
|
||||
Already working! Custom domains just add another entry point.
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Path
|
||||
|
||||
### Beginner (New to concept):
|
||||
1. Read EXECUTIVE_SUMMARY.md
|
||||
2. Look at ARCHITECTURE_DIAGRAMS.md
|
||||
3. Try QUICK_START.md locally
|
||||
|
||||
### Intermediate (Ready to implement):
|
||||
1. Follow QUICK_START.md
|
||||
2. Read CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
|
||||
3. Use IMPLEMENTATION_CHECKLIST.md
|
||||
|
||||
### Advanced (Production deployment):
|
||||
1. Review all documentation
|
||||
2. Follow IMPLEMENTATION_CHECKLIST.md
|
||||
3. Implement monitoring and maintenance
|
||||
|
||||
---
|
||||
|
||||
## 📈 Success Metrics
|
||||
|
||||
After implementation, you can track:
|
||||
- Number of custom domains active
|
||||
- Vendor adoption rate
|
||||
- Domain verification success rate
|
||||
- Traffic per domain
|
||||
- SSL certificate status
|
||||
- Failed domain lookups
|
||||
|
||||
---
|
||||
|
||||
## 🚦 Status Indicators
|
||||
|
||||
Throughout the documentation, you'll see these indicators:
|
||||
|
||||
✅ - Recommended approach
|
||||
❌ - Not recommended
|
||||
⚠️ - Warning or caution
|
||||
🔧 - Technical detail
|
||||
💡 - Tip or best practice
|
||||
📝 - Note or important info
|
||||
|
||||
---
|
||||
|
||||
## 📞 Next Actions
|
||||
|
||||
### To Get Started:
|
||||
1. Read EXECUTIVE_SUMMARY.md
|
||||
2. Follow QUICK_START.md for local test
|
||||
3. Review implementation approach with team
|
||||
|
||||
### To Deploy:
|
||||
1. Complete QUICK_START.md test
|
||||
2. Follow IMPLEMENTATION_CHECKLIST.md
|
||||
3. Reference CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
|
||||
|
||||
### To Understand:
|
||||
1. Read EXECUTIVE_SUMMARY.md
|
||||
2. Study ARCHITECTURE_DIAGRAMS.md
|
||||
3. Review code files
|
||||
|
||||
---
|
||||
|
||||
## 📦 Package Contents
|
||||
|
||||
```
|
||||
custom-domain-support/
|
||||
├── Documentation/
|
||||
│ ├── EXECUTIVE_SUMMARY.md (This provides overview)
|
||||
│ ├── QUICK_START.md (30-minute implementation)
|
||||
│ ├── CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Complete guide)
|
||||
│ ├── ARCHITECTURE_DIAGRAMS.md (Visual diagrams)
|
||||
│ ├── IMPLEMENTATION_CHECKLIST.md (Step-by-step tasks)
|
||||
│ └── INDEX.md (This file)
|
||||
│
|
||||
└── Code Files/
|
||||
├── vendor_domain_model.py (Database model)
|
||||
├── updated_vendor_context.py (Enhanced middleware)
|
||||
├── vendor_domains_api.py (Admin API)
|
||||
├── migration_vendor_domains.py (Database migration)
|
||||
├── config_updates.py (Configuration changes)
|
||||
└── vendor_model_update.py (Vendor model updates)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ Final Notes
|
||||
|
||||
**This is a complete, production-ready solution.**
|
||||
|
||||
Everything you need is included:
|
||||
- ✅ Complete documentation
|
||||
- ✅ Working code examples
|
||||
- ✅ Database migrations
|
||||
- ✅ Security considerations
|
||||
- ✅ Testing strategies
|
||||
- ✅ Deployment guides
|
||||
- ✅ Troubleshooting help
|
||||
- ✅ Rollback plans
|
||||
|
||||
**Your current architecture is perfect for this feature!**
|
||||
|
||||
No major changes needed - just add domain mapping layer on top of existing vendor detection.
|
||||
|
||||
**Start with QUICK_START.md and you'll have a working demo in 30 minutes!**
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Every document has troubleshooting and Q&A sections!
|
||||
|
||||
**Ready to begin?** Start with [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md) or jump straight to [QUICK_START.md](QUICK_START.md)!
|
||||
@@ -0,0 +1,341 @@
|
||||
# Quick Start: Custom Domains in 30 Minutes
|
||||
|
||||
This guide gets you from zero to working custom domains in 30 minutes.
|
||||
|
||||
## What You're Building
|
||||
|
||||
**Before:**
|
||||
- Vendors only accessible via `vendor1.platform.com`
|
||||
|
||||
**After:**
|
||||
- Vendors accessible via custom domains: `customdomain1.com` → Vendor 1
|
||||
- Old subdomain method still works!
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- FastAPI application with vendor context middleware (you have this ✓)
|
||||
- PostgreSQL database (you have this ✓)
|
||||
- Admin privileges to add database tables
|
||||
|
||||
## 5-Step Setup
|
||||
|
||||
### Step 1: Add Database Table (5 minutes)
|
||||
|
||||
Create and run this migration:
|
||||
|
||||
```sql
|
||||
-- Create vendor_domains table
|
||||
CREATE TABLE vendor_domains (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vendor_id INTEGER NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||||
domain VARCHAR(255) NOT NULL UNIQUE,
|
||||
is_primary BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_verified BOOLEAN DEFAULT FALSE,
|
||||
verification_token VARCHAR(100) UNIQUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_domain_active ON vendor_domains(domain, is_active);
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```bash
|
||||
psql your_database -c "\d vendor_domains"
|
||||
```
|
||||
|
||||
### Step 2: Create Model (5 minutes)
|
||||
|
||||
Create `models/database/vendor_domain.py`:
|
||||
|
||||
```python
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.core.database import Base
|
||||
from datetime import datetime, timezone
|
||||
|
||||
class VendorDomain(Base):
|
||||
__tablename__ = "vendor_domains"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"))
|
||||
domain = Column(String(255), nullable=False, unique=True)
|
||||
is_primary = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
verification_token = Column(String(100))
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
vendor = relationship("Vendor", back_populates="domains")
|
||||
|
||||
@classmethod
|
||||
def normalize_domain(cls, domain: str) -> str:
|
||||
return domain.replace("https://", "").replace("http://", "").rstrip("/").lower()
|
||||
```
|
||||
|
||||
Update `models/database/vendor.py`:
|
||||
```python
|
||||
# Add to Vendor class
|
||||
domains = relationship("VendorDomain", back_populates="vendor")
|
||||
```
|
||||
|
||||
### Step 3: Update Middleware (10 minutes)
|
||||
|
||||
Replace your `middleware/vendor_context.py` with this key section:
|
||||
|
||||
```python
|
||||
from models.database.vendor_domain import VendorDomain
|
||||
|
||||
class VendorContextManager:
|
||||
@staticmethod
|
||||
def detect_vendor_context(request: Request) -> Optional[dict]:
|
||||
host = request.headers.get("host", "").split(":")[0]
|
||||
path = request.url.path
|
||||
|
||||
# NEW: Priority 1 - Custom domain check
|
||||
from app.core.config import settings
|
||||
platform_domain = getattr(settings, 'platform_domain', 'platform.com')
|
||||
|
||||
is_custom_domain = (
|
||||
host and
|
||||
not host.endswith(f".{platform_domain}") and
|
||||
host != platform_domain and
|
||||
"localhost" not in host and
|
||||
not host.startswith("admin.")
|
||||
)
|
||||
|
||||
if is_custom_domain:
|
||||
return {
|
||||
"domain": VendorDomain.normalize_domain(host),
|
||||
"detection_method": "custom_domain",
|
||||
"host": host
|
||||
}
|
||||
|
||||
# EXISTING: Priority 2 - Subdomain check
|
||||
if "." in host and not "localhost" in host:
|
||||
parts = host.split(".")
|
||||
if len(parts) >= 2 and parts[0] not in ["www", "admin", "api"]:
|
||||
return {
|
||||
"subdomain": parts[0],
|
||||
"detection_method": "subdomain",
|
||||
"host": host
|
||||
}
|
||||
|
||||
# EXISTING: Priority 3 - Path-based check
|
||||
if path.startswith("/vendor/"):
|
||||
path_parts = path.split("/")
|
||||
if len(path_parts) >= 3:
|
||||
return {
|
||||
"subdomain": path_parts[2],
|
||||
"detection_method": "path",
|
||||
"path_prefix": f"/vendor/{path_parts[2]}"
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_vendor_from_context(db: Session, context: dict) -> Optional[Vendor]:
|
||||
if not context:
|
||||
return None
|
||||
|
||||
# NEW: Custom domain lookup
|
||||
if context.get("detection_method") == "custom_domain":
|
||||
vendor_domain = (
|
||||
db.query(VendorDomain)
|
||||
.filter(VendorDomain.domain == context["domain"])
|
||||
.filter(VendorDomain.is_active == True)
|
||||
.filter(VendorDomain.is_verified == True)
|
||||
.first()
|
||||
)
|
||||
if vendor_domain and vendor_domain.vendor.is_active:
|
||||
return vendor_domain.vendor
|
||||
|
||||
# EXISTING: Subdomain/path lookup
|
||||
if "subdomain" in context:
|
||||
return (
|
||||
db.query(Vendor)
|
||||
.filter(func.lower(Vendor.subdomain) == context["subdomain"].lower())
|
||||
.filter(Vendor.is_active == True)
|
||||
.first()
|
||||
)
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
### Step 4: Add Config (2 minutes)
|
||||
|
||||
Edit `app/core/config.py`:
|
||||
|
||||
```python
|
||||
class Settings(BaseSettings):
|
||||
# ... existing settings ...
|
||||
|
||||
# Add this
|
||||
platform_domain: str = "platform.com" # Change to YOUR domain
|
||||
```
|
||||
|
||||
Update `.env`:
|
||||
```
|
||||
PLATFORM_DOMAIN=platform.com # Change to YOUR domain
|
||||
```
|
||||
|
||||
### Step 5: Test Locally (8 minutes)
|
||||
|
||||
**Add test data:**
|
||||
```sql
|
||||
-- Assuming you have a vendor with id=1
|
||||
INSERT INTO vendor_domains (vendor_id, domain, is_active, is_verified)
|
||||
VALUES (1, 'testdomain.local', true, true);
|
||||
```
|
||||
|
||||
**Edit /etc/hosts:**
|
||||
```bash
|
||||
sudo nano /etc/hosts
|
||||
# Add:
|
||||
127.0.0.1 testdomain.local
|
||||
```
|
||||
|
||||
**Start server:**
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
**Test in browser:**
|
||||
```
|
||||
http://testdomain.local:8000/
|
||||
```
|
||||
|
||||
**Check logs for:**
|
||||
```
|
||||
✓ Vendor found via custom domain: testdomain.local → [Vendor Name]
|
||||
```
|
||||
|
||||
## Done! 🎉
|
||||
|
||||
You now have custom domain support!
|
||||
|
||||
## Quick Test Checklist
|
||||
|
||||
- [ ] Can access vendor via custom domain: `testdomain.local:8000`
|
||||
- [ ] Can still access via subdomain: `vendor1.localhost:8000`
|
||||
- [ ] Can still access via path: `localhost:8000/vendor/vendor1/`
|
||||
- [ ] Logs show correct detection method
|
||||
- [ ] Database has vendor_domains table
|
||||
- [ ] Model imports work
|
||||
|
||||
## What Works Now
|
||||
|
||||
✅ **Custom domains** → Checks vendor_domains table
|
||||
✅ **Subdomains** → Checks vendors.subdomain (existing)
|
||||
✅ **Path-based** → Development mode (existing)
|
||||
✅ **Admin** → Still accessible
|
||||
✅ **Vendor isolation** → Each vendor sees only their data
|
||||
|
||||
## Next Steps
|
||||
|
||||
### For Development:
|
||||
1. Test with multiple custom domains
|
||||
2. Add more test vendors
|
||||
3. Verify queries are scoped correctly
|
||||
|
||||
### For Production:
|
||||
1. Add admin API endpoints (see full guide)
|
||||
2. Add DNS verification (see full guide)
|
||||
3. Configure Nginx to accept all domains
|
||||
4. Set up SSL strategy
|
||||
|
||||
## Common Issues & Fixes
|
||||
|
||||
**Issue:** "Vendor not found"
|
||||
```sql
|
||||
-- Check if domain exists and is verified
|
||||
SELECT * FROM vendor_domains WHERE domain = 'testdomain.local';
|
||||
|
||||
-- Should show is_verified = true and is_active = true
|
||||
```
|
||||
|
||||
**Issue:** Wrong vendor loaded
|
||||
```sql
|
||||
-- Check for duplicate domains
|
||||
SELECT domain, COUNT(*) FROM vendor_domains GROUP BY domain HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
**Issue:** Still using subdomain detection
|
||||
```python
|
||||
# Check middleware logs - should show:
|
||||
# detection_method: "custom_domain"
|
||||
# Not "subdomain"
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
Before going live:
|
||||
|
||||
- [ ] Update `PLATFORM_DOMAIN` in production .env
|
||||
- [ ] Configure Nginx: `server_name _;`
|
||||
- [ ] Set up SSL strategy (Cloudflare recommended)
|
||||
- [ ] Add proper DNS verification (security!)
|
||||
- [ ] Add admin UI for domain management
|
||||
- [ ] Test with real custom domain
|
||||
- [ ] Monitor logs for errors
|
||||
- [ ] Set up rollback plan
|
||||
|
||||
## Files Changed
|
||||
|
||||
**New files:**
|
||||
- `models/database/vendor_domain.py`
|
||||
|
||||
**Modified files:**
|
||||
- `middleware/vendor_context.py` (added custom domain logic)
|
||||
- `app/core/config.py` (added platform_domain setting)
|
||||
- `models/database/vendor.py` (added domains relationship)
|
||||
|
||||
**Database:**
|
||||
- Added `vendor_domains` table
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
Request: customdomain1.com
|
||||
↓
|
||||
Nginx: Accepts all domains
|
||||
↓
|
||||
FastAPI Middleware:
|
||||
Is it custom domain? YES
|
||||
Query vendor_domains table
|
||||
Find vendor_id = 1
|
||||
Load Vendor 1
|
||||
↓
|
||||
Route Handler:
|
||||
Use request.state.vendor (Vendor 1)
|
||||
All queries scoped to Vendor 1
|
||||
↓
|
||||
Response: Vendor 1's shop
|
||||
```
|
||||
|
||||
## Help & Resources
|
||||
|
||||
**Full Guides:**
|
||||
- `/mnt/user-data/outputs/CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md` - Complete guide
|
||||
- `/mnt/user-data/outputs/ARCHITECTURE_DIAGRAMS.md` - Visual diagrams
|
||||
- `/mnt/user-data/outputs/IMPLEMENTATION_CHECKLIST.md` - Detailed checklist
|
||||
|
||||
**Files Generated:**
|
||||
- `vendor_domain_model.py` - Complete model code
|
||||
- `updated_vendor_context.py` - Complete middleware code
|
||||
- `vendor_domains_api.py` - Admin API endpoints
|
||||
- `migration_vendor_domains.py` - Database migration
|
||||
|
||||
## Timeline
|
||||
|
||||
- ✅ Step 1: 5 minutes (database)
|
||||
- ✅ Step 2: 5 minutes (model)
|
||||
- ✅ Step 3: 10 minutes (middleware)
|
||||
- ✅ Step 4: 2 minutes (config)
|
||||
- ✅ Step 5: 8 minutes (testing)
|
||||
|
||||
**Total: 30 minutes** ⏱️
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Check the full implementation guide or review the architecture diagrams!
|
||||
@@ -0,0 +1,425 @@
|
||||
# Custom Domain Support - Complete Implementation Package
|
||||
|
||||
## 🎯 Your Question Answered
|
||||
|
||||
**Q: "Can my FastAPI multi-tenant app support multiple shops on different custom domains like customdomain1.com and customdomain2.com?"**
|
||||
|
||||
**A: YES! ✅** Your architecture is **perfectly suited** for this. You just need to add a domain mapping layer.
|
||||
|
||||
---
|
||||
|
||||
## 📊 What You Have vs What You Need
|
||||
|
||||
### ✅ What You Already Have (Perfect!)
|
||||
- Multi-tenant architecture with vendor isolation
|
||||
- Vendor context middleware
|
||||
- Subdomain routing: `vendor1.platform.com`
|
||||
- Path-based routing: `/vendor/vendor1/`
|
||||
- Request state management
|
||||
|
||||
### ➕ What You Need to Add (Simple!)
|
||||
- Database table: `vendor_domains` (1 table)
|
||||
- Enhanced middleware: Check custom domains first
|
||||
- DNS verification: Prevent domain hijacking
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Add database table (5 minutes)
|
||||
CREATE TABLE vendor_domains (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vendor_id INTEGER REFERENCES vendors(id),
|
||||
domain VARCHAR(255) UNIQUE,
|
||||
is_verified BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
# 2. Update middleware (10 minutes)
|
||||
# See: updated_vendor_context.py
|
||||
|
||||
# 3. Test locally (5 minutes)
|
||||
# Add to /etc/hosts:
|
||||
127.0.0.1 testdomain.local
|
||||
|
||||
# Add test data:
|
||||
INSERT INTO vendor_domains (vendor_id, domain, is_verified)
|
||||
VALUES (1, 'testdomain.local', true);
|
||||
|
||||
# Visit: http://testdomain.local:8000/
|
||||
# ✅ Should show Vendor 1's shop!
|
||||
```
|
||||
|
||||
**Total time: 30 minutes** ⏱️
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
### Before (Current)
|
||||
```
|
||||
Customer → vendor1.platform.com → Middleware checks subdomain → Vendor 1
|
||||
```
|
||||
|
||||
### After (With Custom Domains)
|
||||
```
|
||||
Customer → customdomain1.com → Middleware checks domain table → Vendor 1
|
||||
(subdomain still works too!)
|
||||
```
|
||||
|
||||
### How It Works
|
||||
```
|
||||
1. Request arrives: Host = "customdomain1.com"
|
||||
2. Middleware: Is it custom domain? YES
|
||||
3. Query: SELECT vendor_id FROM vendor_domains WHERE domain = 'customdomain1.com'
|
||||
4. Result: vendor_id = 1
|
||||
5. Load: Vendor 1 data
|
||||
6. Render: Vendor 1's shop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Structure
|
||||
|
||||
This package contains 6 comprehensive guides:
|
||||
|
||||
### Start Here → [INDEX.md](INDEX.md)
|
||||
Master navigation guide to all documentation
|
||||
|
||||
### Quick Implementation → [QUICK_START.md](QUICK_START.md)
|
||||
- ⏱️ 30 minutes to working demo
|
||||
- 🔧 Hands-on implementation
|
||||
- ✅ Local testing included
|
||||
|
||||
### Complete Guide → [CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md](CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md)
|
||||
- 📖 Comprehensive documentation
|
||||
- 🔐 Security best practices
|
||||
- 🚀 Production deployment
|
||||
- ⚠️ Troubleshooting guide
|
||||
|
||||
### Visual Learning → [ARCHITECTURE_DIAGRAMS.md](ARCHITECTURE_DIAGRAMS.md)
|
||||
- 📊 Architecture diagrams
|
||||
- 🔄 Request flow illustrations
|
||||
- 🗄️ Database relationships
|
||||
- 🌐 DNS configuration examples
|
||||
|
||||
### Project Planning → [IMPLEMENTATION_CHECKLIST.md](IMPLEMENTATION_CHECKLIST.md)
|
||||
- ✅ Phase-by-phase tasks
|
||||
- 🧪 Testing procedures
|
||||
- 📦 Deployment steps
|
||||
- 🔄 Rollback plan
|
||||
|
||||
### Decision Making → [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
|
||||
- 💼 Business case
|
||||
- 💰 ROI analysis
|
||||
- ⚖️ Risk assessment
|
||||
- ❓ Q&A section
|
||||
|
||||
---
|
||||
|
||||
## 💻 Code Files Included
|
||||
|
||||
All ready-to-use code in `/home/claude/`:
|
||||
|
||||
| File | Purpose | Status |
|
||||
|------|---------|--------|
|
||||
| `vendor_domain_model.py` | SQLAlchemy model | ✅ Ready |
|
||||
| `updated_vendor_context.py` | Enhanced middleware | ✅ Ready |
|
||||
| `vendor_domains_api.py` | Admin API endpoints | ✅ Ready |
|
||||
| `migration_vendor_domains.py` | Database migration | ✅ Ready |
|
||||
| `config_updates.py` | Configuration | ✅ Ready |
|
||||
| `vendor_model_update.py` | Vendor model update | ✅ Ready |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### ✅ What You Get
|
||||
|
||||
**Custom Domain Support:**
|
||||
- Vendors can use their own domains
|
||||
- Multiple domains per vendor
|
||||
- Professional branding
|
||||
|
||||
**Security:**
|
||||
- DNS verification required
|
||||
- Prevents domain hijacking
|
||||
- Vendor ownership proof
|
||||
|
||||
**Backward Compatible:**
|
||||
- Subdomain routing still works
|
||||
- Path-based routing still works
|
||||
- No breaking changes
|
||||
|
||||
**Scalability:**
|
||||
- Single server handles all domains
|
||||
- Database-driven routing
|
||||
- Easy to manage
|
||||
|
||||
---
|
||||
|
||||
## 📈 Implementation Metrics
|
||||
|
||||
### Complexity
|
||||
- **Database**: +1 table
|
||||
- **Models**: +1 new, ~1 modified
|
||||
- **Middleware**: ~1 file modified
|
||||
- **Endpoints**: +5-7 API routes
|
||||
- **Risk Level**: 🟢 Low
|
||||
|
||||
### Time Required
|
||||
- **Local Test**: 30 minutes
|
||||
- **Development**: 4-6 hours
|
||||
- **Testing**: 2-3 hours
|
||||
- **Deployment**: 2-4 hours
|
||||
- **Total**: 🕐 1-2 days
|
||||
|
||||
### Resources
|
||||
- **New Technologies**: None! Use existing stack
|
||||
- **Server Changes**: Nginx config update
|
||||
- **DNS Required**: Yes, per custom domain
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
- ✅ **DNS Verification**: Vendors must prove domain ownership
|
||||
- ✅ **TXT Record Check**: Token-based verification
|
||||
- ✅ **Vendor Isolation**: Each vendor sees only their data
|
||||
- ✅ **Active Status**: Domains can be deactivated
|
||||
- ✅ **Audit Trail**: Track domain changes
|
||||
|
||||
---
|
||||
|
||||
## 🌐 DNS Configuration (Vendor Side)
|
||||
|
||||
When a vendor wants to use `customdomain1.com`:
|
||||
|
||||
```
|
||||
# At their domain registrar:
|
||||
|
||||
1. A Record:
|
||||
Name: @
|
||||
Value: 123.45.67.89 (your server IP)
|
||||
|
||||
2. Verification TXT Record:
|
||||
Name: _letzshop-verify
|
||||
Value: [token from your platform]
|
||||
|
||||
3. Wait 5-15 minutes for DNS propagation
|
||||
|
||||
4. Admin verifies domain
|
||||
|
||||
5. Domain goes live! 🎉
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment Options
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Test locally with /etc/hosts
|
||||
127.0.0.1 testdomain.local
|
||||
```
|
||||
|
||||
### Staging
|
||||
```bash
|
||||
# Use subdomain for testing
|
||||
staging-vendor.platform.com
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# Full custom domain support
|
||||
customdomain1.com → Vendor 1
|
||||
customdomain2.com → Vendor 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Why This Works
|
||||
|
||||
Your architecture is **already multi-tenant**:
|
||||
- ✅ Vendor isolation exists
|
||||
- ✅ Middleware detects context
|
||||
- ✅ All queries scoped to vendor
|
||||
- ✅ Request state management works
|
||||
|
||||
You just need to add **one more detection method**:
|
||||
1. ~~Check subdomain~~ (existing)
|
||||
2. ~~Check path~~ (existing)
|
||||
3. **Check custom domain** (new!)
|
||||
|
||||
---
|
||||
|
||||
## 📦 What's Included
|
||||
|
||||
### Complete Documentation
|
||||
- 5 detailed guides (70+ pages)
|
||||
- Architecture diagrams
|
||||
- Implementation checklist
|
||||
- Quick start guide
|
||||
- Troubleshooting help
|
||||
|
||||
### Production-Ready Code
|
||||
- Database models
|
||||
- API endpoints
|
||||
- Migrations
|
||||
- Configuration
|
||||
- Middleware updates
|
||||
|
||||
### Best Practices
|
||||
- Security guidelines
|
||||
- Testing strategies
|
||||
- Deployment procedures
|
||||
- Monitoring setup
|
||||
- Rollback plans
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Path
|
||||
|
||||
### New to Concept? (30 minutes)
|
||||
1. Read: EXECUTIVE_SUMMARY.md
|
||||
2. View: ARCHITECTURE_DIAGRAMS.md
|
||||
3. Try: QUICK_START.md
|
||||
|
||||
### Ready to Build? (2-3 hours)
|
||||
1. Follow: QUICK_START.md
|
||||
2. Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
|
||||
3. Use: Code files provided
|
||||
|
||||
### Deploying to Production? (1-2 days)
|
||||
1. Complete: QUICK_START.md test
|
||||
2. Follow: IMPLEMENTATION_CHECKLIST.md
|
||||
3. Reference: All documentation
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
After implementation:
|
||||
- [ ] Custom domains route to correct vendors
|
||||
- [ ] Subdomain routing still works
|
||||
- [ ] Path-based routing still works
|
||||
- [ ] DNS verification functional
|
||||
- [ ] SSL certificates work
|
||||
- [ ] Admin can manage domains
|
||||
- [ ] Monitoring in place
|
||||
- [ ] Documentation complete
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Benefits
|
||||
|
||||
### For You (Platform Owner)
|
||||
- 💰 Premium feature for vendors
|
||||
- 🏆 Competitive advantage
|
||||
- 📈 Higher vendor retention
|
||||
- 🔧 Single codebase maintained
|
||||
|
||||
### For Vendors
|
||||
- 🎨 Own brand domain
|
||||
- 🔍 Better SEO
|
||||
- 💼 Professional appearance
|
||||
- 📊 Marketing benefits
|
||||
|
||||
### For Customers
|
||||
- ✨ Seamless experience
|
||||
- 🔒 Trust familiar domain
|
||||
- 🎯 Better UX
|
||||
|
||||
---
|
||||
|
||||
## 📞 Next Steps
|
||||
|
||||
### To Understand
|
||||
→ Read [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
|
||||
|
||||
### To Test Locally
|
||||
→ Follow [QUICK_START.md](QUICK_START.md)
|
||||
|
||||
### To Implement
|
||||
→ Use [IMPLEMENTATION_CHECKLIST.md](IMPLEMENTATION_CHECKLIST.md)
|
||||
|
||||
### To Deploy
|
||||
→ Reference [CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md](CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md)
|
||||
|
||||
### To Navigate Everything
|
||||
→ Check [INDEX.md](INDEX.md)
|
||||
|
||||
---
|
||||
|
||||
## ❓ Common Questions
|
||||
|
||||
**Q: Will this break my existing setup?**
|
||||
A: No! Completely backward compatible.
|
||||
|
||||
**Q: Do I need new servers?**
|
||||
A: No! Single server handles all domains.
|
||||
|
||||
**Q: What about SSL certificates?**
|
||||
A: Use Cloudflare (easiest) or Let's Encrypt.
|
||||
|
||||
**Q: How long to implement?**
|
||||
A: 30 minutes for demo, 1-2 days for production.
|
||||
|
||||
**Q: Is it secure?**
|
||||
A: Yes! DNS verification prevents hijacking.
|
||||
|
||||
**Q: Can one vendor have multiple domains?**
|
||||
A: Yes! Fully supported.
|
||||
|
||||
More Q&A in [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
|
||||
|
||||
---
|
||||
|
||||
## 🏁 Final Thoughts
|
||||
|
||||
**Your architecture is perfect for this!**
|
||||
|
||||
You have:
|
||||
- ✅ Multi-tenant design
|
||||
- ✅ Vendor isolation
|
||||
- ✅ Context middleware
|
||||
- ✅ FastAPI + PostgreSQL
|
||||
|
||||
You just need:
|
||||
- ➕ Domain mapping table
|
||||
- ➕ Enhanced middleware
|
||||
- ➕ DNS verification
|
||||
|
||||
**It's simpler than you think!**
|
||||
|
||||
---
|
||||
|
||||
## 📋 File Locations
|
||||
|
||||
### Documentation (read online)
|
||||
- `/mnt/user-data/outputs/INDEX.md`
|
||||
- `/mnt/user-data/outputs/EXECUTIVE_SUMMARY.md`
|
||||
- `/mnt/user-data/outputs/QUICK_START.md`
|
||||
- `/mnt/user-data/outputs/CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md`
|
||||
- `/mnt/user-data/outputs/ARCHITECTURE_DIAGRAMS.md`
|
||||
- `/mnt/user-data/outputs/IMPLEMENTATION_CHECKLIST.md`
|
||||
|
||||
### Code Files (copy to your project)
|
||||
- `/home/claude/vendor_domain_model.py`
|
||||
- `/home/claude/updated_vendor_context.py`
|
||||
- `/home/claude/vendor_domains_api.py`
|
||||
- `/home/claude/migration_vendor_domains.py`
|
||||
- `/home/claude/config_updates.py`
|
||||
- `/home/claude/vendor_model_update.py`
|
||||
|
||||
---
|
||||
|
||||
**Ready to start?**
|
||||
|
||||
Begin with [QUICK_START.md](QUICK_START.md) for a 30-minute working demo!
|
||||
|
||||
Or read [INDEX.md](INDEX.md) for complete navigation guide.
|
||||
|
||||
---
|
||||
|
||||
**Good luck! 🚀**
|
||||
@@ -0,0 +1,698 @@
|
||||
# Multi-Theme Shop System - Complete Implementation Guide
|
||||
|
||||
## 🎨 Overview
|
||||
|
||||
This guide explains how to implement vendor-specific themes in your FastAPI multi-tenant e-commerce platform, allowing each vendor to have their own unique shop design, colors, branding, and layout.
|
||||
|
||||
## What You're Building
|
||||
|
||||
**Before:**
|
||||
- All vendor shops look the same
|
||||
- Same colors, fonts, layouts
|
||||
- Only vendor name changes
|
||||
|
||||
**After:**
|
||||
- Each vendor has unique theme
|
||||
- Custom colors, fonts, logos
|
||||
- Different layouts per vendor
|
||||
- Vendor-specific branding
|
||||
- CSS customization support
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Request → Vendor Middleware → Theme Middleware → Template Rendering
|
||||
↓ ↓ ↓
|
||||
Sets vendor Loads theme Applies styles
|
||||
in request config for and branding
|
||||
state vendor
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
1. Customer visits: customdomain1.com
|
||||
2. Vendor middleware: Identifies Vendor 1
|
||||
3. Theme middleware: Loads Vendor 1's theme
|
||||
4. Template receives:
|
||||
- vendor: Vendor 1 object
|
||||
- theme: Vendor 1 theme config
|
||||
5. Template renders with:
|
||||
- Vendor 1 colors
|
||||
- Vendor 1 logo
|
||||
- Vendor 1 layout preferences
|
||||
- Vendor 1 custom CSS
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Add Theme Database Table
|
||||
|
||||
Create the `vendor_themes` table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE vendor_themes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vendor_id INTEGER UNIQUE NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||||
theme_name VARCHAR(100) DEFAULT 'default',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
-- Colors (JSON)
|
||||
colors JSONB DEFAULT '{
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899",
|
||||
"background": "#ffffff",
|
||||
"text": "#1f2937",
|
||||
"border": "#e5e7eb"
|
||||
}'::jsonb,
|
||||
|
||||
-- Typography
|
||||
font_family_heading VARCHAR(100) DEFAULT 'Inter, sans-serif',
|
||||
font_family_body VARCHAR(100) DEFAULT 'Inter, sans-serif',
|
||||
|
||||
-- Branding
|
||||
logo_url VARCHAR(500),
|
||||
logo_dark_url VARCHAR(500),
|
||||
favicon_url VARCHAR(500),
|
||||
banner_url VARCHAR(500),
|
||||
|
||||
-- Layout
|
||||
layout_style VARCHAR(50) DEFAULT 'grid',
|
||||
header_style VARCHAR(50) DEFAULT 'fixed',
|
||||
product_card_style VARCHAR(50) DEFAULT 'modern',
|
||||
|
||||
-- Customization
|
||||
custom_css TEXT,
|
||||
social_links JSONB DEFAULT '{}'::jsonb,
|
||||
|
||||
-- Meta
|
||||
meta_title_template VARCHAR(200),
|
||||
meta_description TEXT,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vendor_themes_vendor_id ON vendor_themes(vendor_id);
|
||||
CREATE INDEX idx_vendor_themes_active ON vendor_themes(vendor_id, is_active);
|
||||
```
|
||||
|
||||
### Step 2: Create VendorTheme Model
|
||||
|
||||
File: `models/database/vendor_theme.py`
|
||||
|
||||
See the complete model in `/home/claude/vendor_theme_model.py`
|
||||
|
||||
**Key features:**
|
||||
- JSON fields for flexible color schemes
|
||||
- Brand asset URLs (logo, favicon, banner)
|
||||
- Layout preferences
|
||||
- Custom CSS support
|
||||
- CSS variables generator
|
||||
- to_dict() for template rendering
|
||||
|
||||
### Step 3: Update Vendor Model
|
||||
|
||||
Add theme relationship to `models/database/vendor.py`:
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
class Vendor(Base):
|
||||
# ... existing fields ...
|
||||
|
||||
# Add theme relationship
|
||||
theme = relationship(
|
||||
"VendorTheme",
|
||||
back_populates="vendor",
|
||||
uselist=False, # One-to-one relationship
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
@property
|
||||
def active_theme(self):
|
||||
"""Get vendor's active theme or return None"""
|
||||
if self.theme and self.theme.is_active:
|
||||
return self.theme
|
||||
return None
|
||||
```
|
||||
|
||||
### Step 4: Create Theme Context Middleware
|
||||
|
||||
File: `middleware/theme_context.py`
|
||||
|
||||
See complete middleware in `/home/claude/theme_context_middleware.py`
|
||||
|
||||
**What it does:**
|
||||
1. Runs AFTER vendor_context_middleware
|
||||
2. Loads theme for detected vendor
|
||||
3. Injects theme into request.state
|
||||
4. Falls back to default theme if needed
|
||||
|
||||
**Add to main.py:**
|
||||
```python
|
||||
from middleware.theme_context import theme_context_middleware
|
||||
|
||||
# AFTER vendor_context_middleware
|
||||
app.middleware("http")(theme_context_middleware)
|
||||
```
|
||||
|
||||
### Step 5: Create Shop Base Template
|
||||
|
||||
File: `app/templates/shop/base.html`
|
||||
|
||||
See complete template in `/home/claude/shop_base_template.html`
|
||||
|
||||
**Key features:**
|
||||
- Injects CSS variables from theme
|
||||
- Vendor-specific logo (light/dark mode)
|
||||
- Theme-aware header/footer
|
||||
- Social links from theme config
|
||||
- Custom CSS injection
|
||||
- Dynamic favicon
|
||||
- SEO meta tags
|
||||
|
||||
**Template receives:**
|
||||
```python
|
||||
{
|
||||
"vendor": vendor_object, # From vendor middleware
|
||||
"theme": theme_dict, # From theme middleware
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Create Shop Layout JavaScript
|
||||
|
||||
File: `static/shop/js/shop-layout.js`
|
||||
|
||||
See complete code in `/home/claude/shop_layout.js`
|
||||
|
||||
**Provides:**
|
||||
- Theme toggling (light/dark)
|
||||
- Cart management
|
||||
- Mobile menu
|
||||
- Search overlay
|
||||
- Toast notifications
|
||||
- Price formatting
|
||||
- Date formatting
|
||||
|
||||
### Step 7: Update Route Handlers
|
||||
|
||||
Ensure theme is passed to templates:
|
||||
|
||||
```python
|
||||
from middleware.theme_context import get_current_theme
|
||||
|
||||
@router.get("/")
|
||||
async def shop_home(request: Request, db: Session = Depends(get_db)):
|
||||
vendor = request.state.vendor
|
||||
theme = get_current_theme(request) # or request.state.theme
|
||||
|
||||
# Get products for vendor
|
||||
products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor.id,
|
||||
Product.is_active == True
|
||||
).all()
|
||||
|
||||
return templates.TemplateResponse("shop/home.html", {
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"theme": theme,
|
||||
"products": products
|
||||
})
|
||||
```
|
||||
|
||||
**Note:** If middleware is set up correctly, theme is already in `request.state.theme`, so you may not need to explicitly pass it!
|
||||
|
||||
## How Themes Work
|
||||
|
||||
### CSS Variables System
|
||||
|
||||
Each theme generates CSS custom properties:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-primary: #6366f1;
|
||||
--color-secondary: #8b5cf6;
|
||||
--color-accent: #ec4899;
|
||||
--color-background: #ffffff;
|
||||
--color-text: #1f2937;
|
||||
--color-border: #e5e7eb;
|
||||
--font-heading: Inter, sans-serif;
|
||||
--font-body: Inter, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in HTML/CSS:**
|
||||
```html
|
||||
<!-- In templates -->
|
||||
<button style="background-color: var(--color-primary)">
|
||||
Click Me
|
||||
</button>
|
||||
|
||||
<h1 style="font-family: var(--font-heading)">
|
||||
Welcome
|
||||
</h1>
|
||||
```
|
||||
|
||||
```css
|
||||
/* In stylesheets */
|
||||
.btn-primary {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-family: var(--font-heading);
|
||||
color: var(--color-text);
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Configuration Example
|
||||
|
||||
```python
|
||||
# Example theme for "Modern Electronics Store"
|
||||
theme = {
|
||||
"theme_name": "tech-modern",
|
||||
"colors": {
|
||||
"primary": "#2563eb", # Blue
|
||||
"secondary": "#0ea5e9", # Sky Blue
|
||||
"accent": "#f59e0b", # Amber
|
||||
"background": "#ffffff",
|
||||
"text": "#111827",
|
||||
"border": "#e5e7eb"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Roboto, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"branding": {
|
||||
"logo": "/media/vendors/tech-store/logo.png",
|
||||
"logo_dark": "/media/vendors/tech-store/logo-dark.png",
|
||||
"favicon": "/media/vendors/tech-store/favicon.ico",
|
||||
"banner": "/media/vendors/tech-store/banner.jpg"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
},
|
||||
"social_links": {
|
||||
"facebook": "https://facebook.com/techstore",
|
||||
"instagram": "https://instagram.com/techstore",
|
||||
"twitter": "https://twitter.com/techstore"
|
||||
},
|
||||
"custom_css": """
|
||||
.product-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
"""
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Theme Presets
|
||||
|
||||
You can create predefined theme templates:
|
||||
|
||||
```python
|
||||
# app/core/theme_presets.py
|
||||
|
||||
THEME_PRESETS = {
|
||||
"modern": {
|
||||
"colors": {
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Inter, sans-serif",
|
||||
"body": "Inter, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed"
|
||||
}
|
||||
},
|
||||
|
||||
"classic": {
|
||||
"colors": {
|
||||
"primary": "#1e40af",
|
||||
"secondary": "#7c3aed",
|
||||
"accent": "#dc2626",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Georgia, serif",
|
||||
"body": "Arial, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "list",
|
||||
"header": "static"
|
||||
}
|
||||
},
|
||||
|
||||
"minimal": {
|
||||
"colors": {
|
||||
"primary": "#000000",
|
||||
"secondary": "#404040",
|
||||
"accent": "#666666",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Helvetica, sans-serif",
|
||||
"body": "Helvetica, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "transparent"
|
||||
}
|
||||
},
|
||||
|
||||
"vibrant": {
|
||||
"colors": {
|
||||
"primary": "#f59e0b",
|
||||
"secondary": "#ef4444",
|
||||
"accent": "#8b5cf6",
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Poppins, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "masonry",
|
||||
"header": "fixed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def apply_preset(theme: VendorTheme, preset_name: str):
|
||||
"""Apply a preset to a vendor theme"""
|
||||
if preset_name not in THEME_PRESETS:
|
||||
raise ValueError(f"Unknown preset: {preset_name}")
|
||||
|
||||
preset = THEME_PRESETS[preset_name]
|
||||
|
||||
theme.theme_name = preset_name
|
||||
theme.colors = preset["colors"]
|
||||
theme.font_family_heading = preset["fonts"]["heading"]
|
||||
theme.font_family_body = preset["fonts"]["body"]
|
||||
theme.layout_style = preset["layout"]["style"]
|
||||
theme.header_style = preset["layout"]["header"]
|
||||
|
||||
return theme
|
||||
```
|
||||
|
||||
## Admin Interface for Theme Management
|
||||
|
||||
Create admin endpoints for managing themes:
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/vendor_themes.py
|
||||
|
||||
@router.get("/vendors/{vendor_id}/theme")
|
||||
def get_vendor_theme(vendor_id: int, db: Session = Depends(get_db)):
|
||||
"""Get theme configuration for vendor"""
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not theme:
|
||||
# Return default theme
|
||||
return get_default_theme()
|
||||
|
||||
return theme.to_dict()
|
||||
|
||||
|
||||
@router.put("/vendors/{vendor_id}/theme")
|
||||
def update_vendor_theme(
|
||||
vendor_id: int,
|
||||
theme_data: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update or create theme for vendor"""
|
||||
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not theme:
|
||||
theme = VendorTheme(vendor_id=vendor_id)
|
||||
db.add(theme)
|
||||
|
||||
# Update fields
|
||||
if "colors" in theme_data:
|
||||
theme.colors = theme_data["colors"]
|
||||
|
||||
if "fonts" in theme_data:
|
||||
theme.font_family_heading = theme_data["fonts"].get("heading")
|
||||
theme.font_family_body = theme_data["fonts"].get("body")
|
||||
|
||||
if "branding" in theme_data:
|
||||
theme.logo_url = theme_data["branding"].get("logo")
|
||||
theme.logo_dark_url = theme_data["branding"].get("logo_dark")
|
||||
theme.favicon_url = theme_data["branding"].get("favicon")
|
||||
|
||||
if "layout" in theme_data:
|
||||
theme.layout_style = theme_data["layout"].get("style")
|
||||
theme.header_style = theme_data["layout"].get("header")
|
||||
|
||||
if "custom_css" in theme_data:
|
||||
theme.custom_css = theme_data["custom_css"]
|
||||
|
||||
db.commit()
|
||||
db.refresh(theme)
|
||||
|
||||
return theme.to_dict()
|
||||
|
||||
|
||||
@router.post("/vendors/{vendor_id}/theme/preset/{preset_name}")
|
||||
def apply_theme_preset(
|
||||
vendor_id: int,
|
||||
preset_name: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Apply a preset theme to vendor"""
|
||||
from app.core.theme_presets import apply_preset, THEME_PRESETS
|
||||
|
||||
if preset_name not in THEME_PRESETS:
|
||||
raise HTTPException(400, f"Unknown preset: {preset_name}")
|
||||
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id
|
||||
).first()
|
||||
|
||||
if not theme:
|
||||
theme = VendorTheme(vendor_id=vendor_id)
|
||||
db.add(theme)
|
||||
|
||||
apply_preset(theme, preset_name)
|
||||
db.commit()
|
||||
db.refresh(theme)
|
||||
|
||||
return {
|
||||
"message": f"Applied {preset_name} preset",
|
||||
"theme": theme.to_dict()
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Different Themes for Different Vendors
|
||||
|
||||
### Vendor 1: Tech Electronics Store
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#2563eb", # Blue
|
||||
"secondary": "#0ea5e9",
|
||||
"accent": "#f59e0b"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Roboto, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vendor 2: Fashion Boutique
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#ec4899", # Pink
|
||||
"secondary": "#f472b6",
|
||||
"accent": "#fbbf24"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Playfair Display, serif",
|
||||
"body": "Lato, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "masonry",
|
||||
"header": "transparent"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vendor 3: Organic Food Store
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#10b981", # Green
|
||||
"secondary": "#059669",
|
||||
"accent": "#f59e0b"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Merriweather, serif",
|
||||
"body": "Source Sans Pro, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "list",
|
||||
"header": "static"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Themes
|
||||
|
||||
### Test 1: View Different Vendor Themes
|
||||
|
||||
```bash
|
||||
# Visit Vendor 1 (Tech store with blue theme)
|
||||
curl http://vendor1.localhost:8000/
|
||||
|
||||
# Visit Vendor 2 (Fashion with pink theme)
|
||||
curl http://vendor2.localhost:8000/
|
||||
|
||||
# Each should have different:
|
||||
# - Colors in CSS variables
|
||||
# - Logo
|
||||
# - Fonts
|
||||
# - Layout
|
||||
```
|
||||
|
||||
### Test 2: Theme API
|
||||
|
||||
```bash
|
||||
# Get vendor theme
|
||||
curl http://localhost:8000/api/v1/admin/vendors/1/theme
|
||||
|
||||
# Update colors
|
||||
curl -X PUT http://localhost:8000/api/v1/admin/vendors/1/theme \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"colors": {
|
||||
"primary": "#ff0000",
|
||||
"secondary": "#00ff00"
|
||||
}
|
||||
}'
|
||||
|
||||
# Apply preset
|
||||
curl -X POST http://localhost:8000/api/v1/admin/vendors/1/theme/preset/modern
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Platform Owner
|
||||
- ✅ Premium feature for enterprise vendors
|
||||
- ✅ Differentiate vendor packages (basic vs premium themes)
|
||||
- ✅ Additional revenue stream
|
||||
- ✅ Competitive advantage
|
||||
|
||||
### For Vendors
|
||||
- ✅ Unique brand identity
|
||||
- ✅ Professional appearance
|
||||
- ✅ Better customer recognition
|
||||
- ✅ Customizable to match brand
|
||||
|
||||
### For Customers
|
||||
- ✅ Distinct shopping experiences
|
||||
- ✅ Better brand recognition
|
||||
- ✅ More engaging designs
|
||||
- ✅ Professional appearance
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### 1. Theme Preview
|
||||
Allow vendors to preview themes before applying:
|
||||
|
||||
```python
|
||||
@router.get("/vendors/{vendor_id}/theme/preview/{preset_name}")
|
||||
def preview_theme(vendor_id: int, preset_name: str):
|
||||
"""Generate preview URL for theme"""
|
||||
# Return preview HTML with preset applied
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. Theme Marketplace
|
||||
Create a marketplace of premium themes:
|
||||
|
||||
```python
|
||||
class PremiumTheme(Base):
|
||||
__tablename__ = "premium_themes"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100))
|
||||
description = Column(Text)
|
||||
price = Column(Numeric(10, 2))
|
||||
preview_image = Column(String(500))
|
||||
config = Column(JSON)
|
||||
```
|
||||
|
||||
### 3. Dark Mode Auto-Detection
|
||||
Respect user's system preferences:
|
||||
|
||||
```javascript
|
||||
// Detect system dark mode preference
|
||||
if (window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
this.dark = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Theme Analytics
|
||||
Track which themes perform best:
|
||||
|
||||
```python
|
||||
class ThemeAnalytics(Base):
|
||||
__tablename__ = "theme_analytics"
|
||||
|
||||
theme_id = Column(Integer, ForeignKey("vendor_themes.id"))
|
||||
conversion_rate = Column(Numeric(5, 2))
|
||||
avg_session_duration = Column(Integer)
|
||||
bounce_rate = Column(Numeric(5, 2))
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**What you've built:**
|
||||
- ✅ Vendor-specific theme system
|
||||
- ✅ CSS variables for dynamic styling
|
||||
- ✅ Custom branding (logos, colors, fonts)
|
||||
- ✅ Layout customization
|
||||
- ✅ Custom CSS support
|
||||
- ✅ Theme presets
|
||||
- ✅ Admin theme management
|
||||
|
||||
**Each vendor now has:**
|
||||
- Unique colors and fonts
|
||||
- Custom logo and branding
|
||||
- Layout preferences
|
||||
- Social media links
|
||||
- Custom CSS overrides
|
||||
|
||||
**All controlled by:**
|
||||
- Database configuration
|
||||
- No code changes needed per vendor
|
||||
- Admin panel management
|
||||
- Preview and testing
|
||||
|
||||
**Your architecture supports this perfectly!** The vendor context + theme middleware pattern works seamlessly with your existing Alpine.js frontend.
|
||||
|
||||
Start with the default theme, then let vendors customize their shops! 🎨
|
||||
@@ -0,0 +1,509 @@
|
||||
# Theme System Integration Guide for Your Existing Architecture
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This guide shows how to integrate the multi-theme system into your **existing** FastAPI + Alpine.js + Tailwind CSS architecture.
|
||||
|
||||
**Key Point:** You already have 80% of what you need! Your `theme_config` JSON field in the Vendor model is the foundation.
|
||||
|
||||
## ✅ What You Already Have
|
||||
|
||||
1. **Vendor model** with `theme_config` JSON field ✅
|
||||
2. **Alpine.js** frontend pattern established ✅
|
||||
3. **Tailwind CSS** for styling ✅
|
||||
4. **Admin pages** with Jinja2 templates ✅
|
||||
5. **Vendor context middleware** ✅
|
||||
|
||||
## 🚀 Integration Steps
|
||||
|
||||
### Step 1: Update Vendor Model (5 minutes)
|
||||
|
||||
Your current model already has `theme_config`, so just add helper methods:
|
||||
|
||||
```python
|
||||
# models/database/vendor.py
|
||||
|
||||
class Vendor(Base, TimestampMixin):
|
||||
# ... existing fields ...
|
||||
theme_config = Column(JSON, default=dict) # ✅ You already have this!
|
||||
|
||||
# ADD THIS PROPERTY:
|
||||
@property
|
||||
def theme(self):
|
||||
"""
|
||||
Get theme configuration for this vendor.
|
||||
Returns dict with theme configuration.
|
||||
"""
|
||||
if self.theme_config:
|
||||
return self._normalize_theme_config(self.theme_config)
|
||||
return self._get_default_theme()
|
||||
|
||||
def _normalize_theme_config(self, config: dict) -> dict:
|
||||
"""Ensure theme_config has all required fields"""
|
||||
return {
|
||||
"colors": config.get("colors", {
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899"
|
||||
}),
|
||||
"fonts": config.get("fonts", {
|
||||
"heading": "Inter, sans-serif",
|
||||
"body": "Inter, sans-serif"
|
||||
}),
|
||||
"layout": config.get("layout", {
|
||||
"style": "grid",
|
||||
"header": "fixed"
|
||||
}),
|
||||
"branding": config.get("branding", {
|
||||
"logo": None,
|
||||
"favicon": None
|
||||
}),
|
||||
"custom_css": config.get("custom_css", None),
|
||||
"css_variables": self._generate_css_variables(config)
|
||||
}
|
||||
|
||||
def _generate_css_variables(self, config: dict) -> dict:
|
||||
"""Generate CSS custom properties from theme"""
|
||||
colors = config.get("colors", {})
|
||||
fonts = config.get("fonts", {})
|
||||
|
||||
return {
|
||||
"--color-primary": colors.get("primary", "#6366f1"),
|
||||
"--color-secondary": colors.get("secondary", "#8b5cf6"),
|
||||
"--color-accent": colors.get("accent", "#ec4899"),
|
||||
"--font-heading": fonts.get("heading", "Inter, sans-serif"),
|
||||
"--font-body": fonts.get("body", "Inter, sans-serif"),
|
||||
}
|
||||
|
||||
def _get_default_theme(self) -> dict:
|
||||
"""Default theme if none configured"""
|
||||
return {
|
||||
"colors": {"primary": "#6366f1", "secondary": "#8b5cf6", "accent": "#ec4899"},
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed"},
|
||||
"branding": {"logo": None, "favicon": None},
|
||||
"custom_css": None,
|
||||
"css_variables": {
|
||||
"--color-primary": "#6366f1",
|
||||
"--color-secondary": "#8b5cf6",
|
||||
"--color-accent": "#ec4899",
|
||||
"--font-heading": "Inter, sans-serif",
|
||||
"--font-body": "Inter, sans-serif",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**That's it for the model!** No new tables needed if you use the existing `theme_config` JSON field.
|
||||
|
||||
### Step 2: Add Theme Route to Admin Pages (2 minutes)
|
||||
|
||||
Add to `app/api/v1/admin/pages.py`:
|
||||
|
||||
```python
|
||||
@router.get("/vendors/{vendor_code}/theme", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def admin_vendor_theme_page(
|
||||
request: Request,
|
||||
vendor_code: str = Path(..., description="Vendor code"),
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Render vendor theme customization page.
|
||||
"""
|
||||
return templates.TemplateResponse(
|
||||
"admin/vendor-theme.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": current_user,
|
||||
"vendor_code": vendor_code,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Create Theme Management Page (5 minutes)
|
||||
|
||||
1. Copy the HTML template from `/mnt/user-data/outputs/vendor-theme-page.html`
|
||||
2. Save as `app/templates/admin/vendor-theme.html`
|
||||
3. Copy the JS component from `/mnt/user-data/outputs/vendor-theme.js`
|
||||
4. Save as `static/admin/js/vendor-theme.js`
|
||||
|
||||
### Step 4: Update Vendor Detail Page to Link to Theme (2 minutes)
|
||||
|
||||
In your `app/templates/admin/vendor-detail.html`, add a "Customize Theme" button:
|
||||
|
||||
```html
|
||||
<!-- In your vendor details page -->
|
||||
<div class="flex space-x-2">
|
||||
<a :href="`/admin/vendors/${vendor?.vendor_code}/edit`"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
Edit Vendor
|
||||
</a>
|
||||
|
||||
<!-- ADD THIS BUTTON -->
|
||||
<a :href="`/admin/vendors/${vendor?.vendor_code}/theme`"
|
||||
class="px-4 py-2 text-sm font-medium text-purple-700 bg-white border border-purple-600 rounded-lg hover:bg-purple-50">
|
||||
Customize Theme
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Step 5: Update Shop Template to Use Theme (10 minutes)
|
||||
|
||||
Create `app/templates/shop/base.html` (if you don't have it yet):
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" x-data="shopLayoutData()" x-bind:class="{ 'dark': dark }">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ vendor.name }}{% endblock %}</title>
|
||||
|
||||
<!-- ✅ CRITICAL: Inject theme CSS variables -->
|
||||
<style id="vendor-theme-variables">
|
||||
:root {
|
||||
{% for key, value in theme.css_variables.items() %}
|
||||
{{ key }}: {{ value }};
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
/* Custom CSS from vendor */
|
||||
{% if theme.custom_css %}
|
||||
{{ theme.custom_css | safe }}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center">
|
||||
{% if theme.branding.logo %}
|
||||
<img src="{{ theme.branding.logo }}"
|
||||
alt="{{ vendor.name }}"
|
||||
class="h-8">
|
||||
{% else %}
|
||||
<h1 class="text-xl font-bold"
|
||||
style="color: var(--color-primary)">
|
||||
{{ vendor.name }}
|
||||
</h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex space-x-6">
|
||||
<a href="/" class="hover:text-primary">Home</a>
|
||||
<a href="/products" class="hover:text-primary">Products</a>
|
||||
<a href="/about" class="hover:text-primary">About</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-100 mt-12 py-8">
|
||||
<div class="max-w-7xl mx-auto px-4 text-center text-gray-600">
|
||||
<p>© {{ now().year }} {{ vendor.name }}</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Step 6: Update Shop Routes to Pass Theme (5 minutes)
|
||||
|
||||
In your shop route handlers (e.g., `app/api/v1/public/vendors/pages.py`):
|
||||
|
||||
```python
|
||||
@router.get("/")
|
||||
async def shop_home(request: Request, db: Session = Depends(get_db)):
|
||||
vendor = request.state.vendor # From vendor context middleware
|
||||
|
||||
# Get theme from vendor
|
||||
theme = vendor.theme # Uses the property we added
|
||||
|
||||
return templates.TemplateResponse("shop/home.html", {
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"theme": theme, # ✅ Pass theme to template
|
||||
})
|
||||
```
|
||||
|
||||
## 📊 Using Tailwind with Theme Variables
|
||||
|
||||
The magic is in CSS variables! Tailwind can use your theme colors:
|
||||
|
||||
```html
|
||||
<!-- In your shop templates -->
|
||||
|
||||
<!-- Primary color button -->
|
||||
<button class="px-4 py-2 rounded-lg"
|
||||
style="background-color: var(--color-primary); color: white;">
|
||||
Shop Now
|
||||
</button>
|
||||
|
||||
<!-- Or use Tailwind utilities with inline styles -->
|
||||
<div class="text-2xl font-bold"
|
||||
style="color: var(--color-primary); font-family: var(--font-heading)">
|
||||
Welcome to {{ vendor.name }}
|
||||
</div>
|
||||
|
||||
<!-- Product card with theme colors -->
|
||||
<div class="p-4 bg-white rounded-lg border"
|
||||
style="border-color: var(--color-primary)">
|
||||
<h3 class="text-lg font-semibold"
|
||||
style="color: var(--color-primary)">
|
||||
Product Name
|
||||
</h3>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 🎨 How It All Works Together
|
||||
|
||||
### 1. Admin Customizes Theme
|
||||
|
||||
```
|
||||
Admin → /admin/vendors/TECHSTORE/theme
|
||||
↓
|
||||
Sees theme editor (colors, fonts, layout)
|
||||
↓
|
||||
Clicks "Save Theme"
|
||||
↓
|
||||
PUT /api/v1/admin/vendors/TECHSTORE
|
||||
↓
|
||||
Updates vendor.theme_config JSON:
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#2563eb", // Blue
|
||||
"secondary": "#0ea5e9"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Roboto, sans-serif"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Customer Visits Shop
|
||||
|
||||
```
|
||||
Customer → techstore.platform.com
|
||||
↓
|
||||
Vendor middleware identifies Vendor = TECHSTORE
|
||||
↓
|
||||
Shop route loads:
|
||||
- vendor object
|
||||
- theme = vendor.theme (property we added)
|
||||
↓
|
||||
Template renders with:
|
||||
:root {
|
||||
--color-primary: #2563eb;
|
||||
--color-secondary: #0ea5e9;
|
||||
--font-heading: Roboto, sans-serif;
|
||||
}
|
||||
↓
|
||||
Customer sees blue-themed shop with Roboto headings!
|
||||
```
|
||||
|
||||
## 🔧 API Endpoints Needed
|
||||
|
||||
Your existing vendor update endpoint already works! Just update `theme_config`:
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/vendors.py
|
||||
|
||||
@router.put("/vendors/{vendor_code}")
|
||||
async def update_vendor(
|
||||
vendor_code: str,
|
||||
vendor_data: VendorUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.vendor_code == vendor_code
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(404, "Vendor not found")
|
||||
|
||||
# Update fields
|
||||
if vendor_data.theme_config is not None:
|
||||
vendor.theme_config = vendor_data.theme_config # ✅ This already works!
|
||||
|
||||
# ... other updates ...
|
||||
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
return vendor
|
||||
```
|
||||
|
||||
**That's it!** Your existing update endpoint handles theme updates.
|
||||
|
||||
## 🎯 Quick Test
|
||||
|
||||
### 1. Test Theme Editor
|
||||
|
||||
```bash
|
||||
# Start your app
|
||||
uvicorn main:app --reload
|
||||
|
||||
# Visit admin
|
||||
http://localhost:8000/admin/vendors/YOUR_VENDOR_CODE/theme
|
||||
|
||||
# Change colors and save
|
||||
# Check database:
|
||||
SELECT theme_config FROM vendors WHERE vendor_code = 'YOUR_VENDOR_CODE';
|
||||
|
||||
# Should see:
|
||||
# {"colors": {"primary": "#your-color", ...}}
|
||||
```
|
||||
|
||||
### 2. Test Shop Rendering
|
||||
|
||||
```bash
|
||||
# Visit vendor shop
|
||||
http://vendor1.localhost:8000/
|
||||
|
||||
# Inspect page source
|
||||
# Should see:
|
||||
# <style id="vendor-theme-variables">
|
||||
# :root {
|
||||
# --color-primary: #your-color;
|
||||
# }
|
||||
# </style>
|
||||
```
|
||||
|
||||
## 📦 Files to Create/Update
|
||||
|
||||
### Create New Files:
|
||||
1. `app/templates/admin/vendor-theme.html` ← From `/mnt/user-data/outputs/vendor-theme-page.html`
|
||||
2. `static/admin/js/vendor-theme.js` ← From `/mnt/user-data/outputs/vendor-theme.js`
|
||||
3. `app/templates/shop/base.html` ← Base shop template with theme support
|
||||
|
||||
### Update Existing Files:
|
||||
1. `models/database/vendor.py` ← Add `theme` property and helper methods
|
||||
2. `app/api/v1/admin/pages.py` ← Add theme route
|
||||
3. `models/schema/vendor.py` ← Already has `theme_config` in VendorUpdate ✅
|
||||
|
||||
## 🎨 Example: Different Themes
|
||||
|
||||
### Vendor 1: Tech Store (Blue Theme)
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#2563eb",
|
||||
"secondary": "#0ea5e9",
|
||||
"accent": "#f59e0b"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Roboto, sans-serif",
|
||||
"body": "Open Sans, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vendor 2: Fashion Boutique (Pink Theme)
|
||||
```python
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#ec4899",
|
||||
"secondary": "#f472b6",
|
||||
"accent": "#fbbf24"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Playfair Display, serif",
|
||||
"body": "Lato, sans-serif"
|
||||
},
|
||||
"layout": {
|
||||
"style": "masonry"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] Add `theme` property to Vendor model
|
||||
- [ ] Add theme route to `pages.py`
|
||||
- [ ] Create `vendor-theme.html` template
|
||||
- [ ] Create `vendor-theme.js` Alpine component
|
||||
- [ ] Create shop `base.html` with theme injection
|
||||
- [ ] Update shop routes to pass `theme`
|
||||
- [ ] Add "Customize Theme" button to vendor detail page
|
||||
- [ ] Test theme editor in admin
|
||||
- [ ] Test theme rendering on shop
|
||||
- [ ] Verify CSS variables work with Tailwind
|
||||
|
||||
## 🚀 Benefits
|
||||
|
||||
### For You:
|
||||
- ✅ Uses existing `theme_config` field (no migration!)
|
||||
- ✅ Works with current Alpine.js pattern
|
||||
- ✅ Compatible with Tailwind CSS
|
||||
- ✅ Follows your established conventions
|
||||
|
||||
### For Vendors:
|
||||
- ✅ Easy theme customization
|
||||
- ✅ Live preview
|
||||
- ✅ Preset templates
|
||||
- ✅ Custom branding
|
||||
|
||||
## 💡 Advanced: Tailwind Custom Configuration
|
||||
|
||||
If you want Tailwind to use theme variables natively:
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'var(--color-primary)',
|
||||
secondary: 'var(--color-secondary)',
|
||||
accent: 'var(--color-accent)',
|
||||
},
|
||||
fontFamily: {
|
||||
heading: 'var(--font-heading)',
|
||||
body: 'var(--font-body)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then you can use:
|
||||
```html
|
||||
<button class="bg-primary text-white">Shop Now</button>
|
||||
<h1 class="font-heading text-primary">Welcome</h1>
|
||||
```
|
||||
|
||||
## 🎉 Summary
|
||||
|
||||
**Your architecture is perfect for this!**
|
||||
|
||||
1. You already have `theme_config` JSON field ✅
|
||||
2. Just add a `theme` property to access it ✅
|
||||
3. Create admin page to edit it ✅
|
||||
4. Inject CSS variables in shop templates ✅
|
||||
5. Use variables with Tailwind/inline styles ✅
|
||||
|
||||
**Total implementation time: ~30 minutes**
|
||||
|
||||
Each vendor gets unique theming without any database migrations! 🚀
|
||||
@@ -0,0 +1,246 @@
|
||||
{# app/templates/shop/base.html #}
|
||||
{# Base template for vendor shop frontend with theme support #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" x-data="shopLayoutData()" x-bind:class="{ 'dark': dark }">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
{# Dynamic title with vendor branding #}
|
||||
<title>
|
||||
{% block title %}{{ vendor.name }}{% endblock %}
|
||||
{% if vendor.tagline %} - {{ vendor.tagline }}{% endif %}
|
||||
</title>
|
||||
|
||||
{# SEO Meta Tags #}
|
||||
<meta name="description" content="{% block meta_description %}{{ vendor.description or 'Shop at ' + vendor.name }}{% endblock %}">
|
||||
<meta name="keywords" content="{% block meta_keywords %}{{ vendor.name }}, online shop{% endblock %}">
|
||||
|
||||
{# Favicon - vendor-specific or default #}
|
||||
{% if theme.branding.favicon %}
|
||||
<link rel="icon" type="image/x-icon" href="{{ theme.branding.favicon }}">
|
||||
{% else %}
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='favicon.ico') }}">
|
||||
{% endif %}
|
||||
|
||||
{# CRITICAL: Inject theme CSS variables #}
|
||||
<style id="vendor-theme-variables">
|
||||
:root {
|
||||
{% for key, value in theme.css_variables.items() %}
|
||||
{{ key }}: {{ value }};
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
{# Custom CSS from vendor theme #}
|
||||
{% if theme.custom_css %}
|
||||
{{ theme.custom_css | safe }}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
{# Tailwind CSS - uses CSS variables #}
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
|
||||
{# Base Shop Styles #}
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/shop.css') }}">
|
||||
|
||||
{# Optional: Theme-specific stylesheet #}
|
||||
{% if theme.theme_name != 'default' %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='shop/themes/' + theme.theme_name + '.css') }}">
|
||||
{% endif %}
|
||||
|
||||
{# Alpine.js for interactivity #}
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
|
||||
|
||||
{# Header - Theme-aware #}
|
||||
<header class="{% if theme.layout.header == 'fixed' %}sticky top-0 z-50{% endif %}
|
||||
bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
|
||||
{# Vendor Logo #}
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="flex items-center space-x-3">
|
||||
{% if theme.branding.logo %}
|
||||
{# Show light logo in light mode, dark logo in dark mode #}
|
||||
<img x-show="!dark"
|
||||
src="{{ theme.branding.logo }}"
|
||||
alt="{{ vendor.name }}"
|
||||
class="h-8 w-auto">
|
||||
{% if theme.branding.logo_dark %}
|
||||
<img x-show="dark"
|
||||
src="{{ theme.branding.logo_dark }}"
|
||||
alt="{{ vendor.name }}"
|
||||
class="h-8 w-auto">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-xl font-bold" style="color: var(--color-primary)">
|
||||
{{ vendor.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Navigation #}
|
||||
<nav class="hidden md:flex space-x-8">
|
||||
<a href="/" class="text-gray-700 dark:text-gray-300 hover:text-primary">
|
||||
Home
|
||||
</a>
|
||||
<a href="/products" class="text-gray-700 dark:text-gray-300 hover:text-primary">
|
||||
Products
|
||||
</a>
|
||||
<a href="/about" class="text-gray-700 dark:text-gray-300 hover:text-primary">
|
||||
About
|
||||
</a>
|
||||
<a href="/contact" class="text-gray-700 dark:text-gray-300 hover:text-primary">
|
||||
Contact
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{# Right side actions #}
|
||||
<div class="flex items-center space-x-4">
|
||||
|
||||
{# Search #}
|
||||
<button @click="openSearch()" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{# Cart #}
|
||||
<a href="/cart" class="relative p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
<span x-show="cartCount > 0"
|
||||
x-text="cartCount"
|
||||
class="absolute -top-1 -right-1 bg-accent text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"
|
||||
style="background-color: var(--color-accent)">
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{# Theme toggle #}
|
||||
<button @click="toggleTheme()"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<svg x-show="!dark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
|
||||
</svg>
|
||||
<svg x-show="dark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{# Account #}
|
||||
<a href="/account" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{# Mobile menu toggle #}
|
||||
<button @click="toggleMobileMenu()" class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{# Main Content Area #}
|
||||
<main class="min-h-screen">
|
||||
{% block content %}
|
||||
{# Page-specific content goes here #}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{# Footer with vendor info and social links #}
|
||||
<footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
|
||||
{# Vendor Info #}
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--color-primary)">
|
||||
{{ vendor.name }}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{{ vendor.description }}
|
||||
</p>
|
||||
|
||||
{# Social Links from theme #}
|
||||
{% if theme.social_links %}
|
||||
<div class="flex space-x-4">
|
||||
{% if theme.social_links.facebook %}
|
||||
<a href="{{ theme.social_links.facebook }}" target="_blank"
|
||||
class="text-gray-600 hover:text-primary dark:text-gray-400">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if theme.social_links.instagram %}
|
||||
<a href="{{ theme.social_links.instagram }}" target="_blank"
|
||||
class="text-gray-600 hover:text-primary dark:text-gray-400">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
{# Add more social networks as needed #}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Quick Links #}
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Quick Links</h4>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/products" class="text-gray-600 hover:text-primary dark:text-gray-400">Products</a></li>
|
||||
<li><a href="/about" class="text-gray-600 hover:text-primary dark:text-gray-400">About Us</a></li>
|
||||
<li><a href="/contact" class="text-gray-600 hover:text-primary dark:text-gray-400">Contact</a></li>
|
||||
<li><a href="/terms" class="text-gray-600 hover:text-primary dark:text-gray-400">Terms</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# Customer Service #}
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Customer Service</h4>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/help" class="text-gray-600 hover:text-primary dark:text-gray-400">Help Center</a></li>
|
||||
<li><a href="/shipping" class="text-gray-600 hover:text-primary dark:text-gray-400">Shipping Info</a></li>
|
||||
<li><a href="/returns" class="text-gray-600 hover:text-primary dark:text-gray-400">Returns</a></li>
|
||||
<li><a href="/faq" class="text-gray-600 hover:text-primary dark:text-gray-400">FAQ</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Copyright #}
|
||||
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700 text-center text-gray-600 dark:text-gray-400">
|
||||
<p>© {{ now().year }} {{ vendor.name }}. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{# Base Shop JavaScript #}
|
||||
<script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script>
|
||||
|
||||
{# Page-specific JavaScript #}
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
|
||||
{# Toast notification container #}
|
||||
<div id="toast-container" class="fixed bottom-4 right-4 z-50"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,228 @@
|
||||
// static/shop/js/shop-layout.js
|
||||
/**
|
||||
* Shop Layout Component
|
||||
* Provides base functionality for vendor shop pages
|
||||
* Works with vendor-specific themes
|
||||
*/
|
||||
|
||||
const shopLog = {
|
||||
info: (...args) => console.info('🛒 [SHOP]', ...args),
|
||||
warn: (...args) => console.warn('⚠️ [SHOP]', ...args),
|
||||
error: (...args) => console.error('❌ [SHOP]', ...args),
|
||||
debug: (...args) => console.log('🔍 [SHOP]', ...args)
|
||||
};
|
||||
|
||||
/**
|
||||
* Shop Layout Data
|
||||
* Base Alpine.js component for shop pages
|
||||
*/
|
||||
function shopLayoutData() {
|
||||
return {
|
||||
// Theme state
|
||||
dark: localStorage.getItem('shop-theme') === 'dark',
|
||||
|
||||
// UI state
|
||||
mobileMenuOpen: false,
|
||||
searchOpen: false,
|
||||
cartCount: 0,
|
||||
|
||||
// Cart state
|
||||
cart: [],
|
||||
|
||||
// Initialize
|
||||
init() {
|
||||
shopLog.info('Shop layout initializing...');
|
||||
|
||||
// Load cart from localStorage
|
||||
this.loadCart();
|
||||
|
||||
// Listen for cart updates
|
||||
window.addEventListener('cart-updated', () => {
|
||||
this.loadCart();
|
||||
});
|
||||
|
||||
shopLog.info('Shop layout initialized');
|
||||
},
|
||||
|
||||
// Theme management
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('shop-theme', this.dark ? 'dark' : 'light');
|
||||
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
// Mobile menu
|
||||
toggleMobileMenu() {
|
||||
this.mobileMenuOpen = !this.mobileMenuOpen;
|
||||
if (this.mobileMenuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
},
|
||||
|
||||
closeMobileMenu() {
|
||||
this.mobileMenuOpen = false;
|
||||
document.body.style.overflow = '';
|
||||
},
|
||||
|
||||
// Search
|
||||
openSearch() {
|
||||
this.searchOpen = true;
|
||||
shopLog.debug('Search opened');
|
||||
// Focus search input after a short delay
|
||||
setTimeout(() => {
|
||||
const input = document.querySelector('#search-input');
|
||||
if (input) input.focus();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
closeSearch() {
|
||||
this.searchOpen = false;
|
||||
},
|
||||
|
||||
// Cart management
|
||||
loadCart() {
|
||||
try {
|
||||
const cartData = localStorage.getItem('shop-cart');
|
||||
if (cartData) {
|
||||
this.cart = JSON.parse(cartData);
|
||||
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
shopLog.error('Failed to load cart:', error);
|
||||
this.cart = [];
|
||||
this.cartCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
addToCart(product, quantity = 1) {
|
||||
shopLog.info('Adding to cart:', product.name, 'x', quantity);
|
||||
|
||||
// Find existing item
|
||||
const existingIndex = this.cart.findIndex(item => item.id === product.id);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update quantity
|
||||
this.cart[existingIndex].quantity += quantity;
|
||||
} else {
|
||||
// Add new item
|
||||
this.cart.push({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
quantity: quantity
|
||||
});
|
||||
}
|
||||
|
||||
// Save and update
|
||||
this.saveCart();
|
||||
this.showToast(`${product.name} added to cart`, 'success');
|
||||
},
|
||||
|
||||
updateCartItem(productId, quantity) {
|
||||
const index = this.cart.findIndex(item => item.id === productId);
|
||||
if (index !== -1) {
|
||||
if (quantity <= 0) {
|
||||
this.cart.splice(index, 1);
|
||||
} else {
|
||||
this.cart[index].quantity = quantity;
|
||||
}
|
||||
this.saveCart();
|
||||
}
|
||||
},
|
||||
|
||||
removeFromCart(productId) {
|
||||
this.cart = this.cart.filter(item => item.id !== productId);
|
||||
this.saveCart();
|
||||
this.showToast('Item removed from cart', 'info');
|
||||
},
|
||||
|
||||
clearCart() {
|
||||
this.cart = [];
|
||||
this.saveCart();
|
||||
this.showToast('Cart cleared', 'info');
|
||||
},
|
||||
|
||||
saveCart() {
|
||||
try {
|
||||
localStorage.setItem('shop-cart', JSON.stringify(this.cart));
|
||||
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent('cart-updated'));
|
||||
|
||||
shopLog.debug('Cart saved:', this.cart.length, 'items');
|
||||
} catch (error) {
|
||||
shopLog.error('Failed to save cart:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Get cart total
|
||||
get cartTotal() {
|
||||
return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
},
|
||||
|
||||
// Toast notifications
|
||||
showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type} transform transition-all duration-300 mb-2`;
|
||||
|
||||
// Color based on type
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-3">
|
||||
<span>${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
class="ml-4 hover:opacity-75">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// Format currency
|
||||
formatPrice(price) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(price);
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.shopLayoutData = shopLayoutData;
|
||||
|
||||
shopLog.info('Shop layout module loaded');
|
||||
@@ -0,0 +1,124 @@
|
||||
# middleware/theme_context.py
|
||||
"""
|
||||
Theme Context Middleware
|
||||
Injects vendor-specific theme into request context
|
||||
"""
|
||||
import logging
|
||||
from fastapi import Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from models.database.vendor_theme import VendorTheme
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThemeContextManager:
|
||||
"""Manages theme context for vendor shops."""
|
||||
|
||||
@staticmethod
|
||||
def get_vendor_theme(db: Session, vendor_id: int) -> dict:
|
||||
"""
|
||||
Get theme configuration for vendor.
|
||||
Returns default theme if no custom theme is configured.
|
||||
"""
|
||||
theme = db.query(VendorTheme).filter(
|
||||
VendorTheme.vendor_id == vendor_id,
|
||||
VendorTheme.is_active == True
|
||||
).first()
|
||||
|
||||
if theme:
|
||||
return theme.to_dict()
|
||||
|
||||
# Return default theme
|
||||
return get_default_theme()
|
||||
|
||||
@staticmethod
|
||||
def get_default_theme() -> dict:
|
||||
"""Default theme configuration"""
|
||||
return {
|
||||
"theme_name": "default",
|
||||
"colors": {
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899",
|
||||
"background": "#ffffff",
|
||||
"text": "#1f2937",
|
||||
"border": "#e5e7eb"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Inter, sans-serif",
|
||||
"body": "Inter, sans-serif"
|
||||
},
|
||||
"branding": {
|
||||
"logo": None,
|
||||
"logo_dark": None,
|
||||
"favicon": None,
|
||||
"banner": None
|
||||
},
|
||||
"layout": {
|
||||
"style": "grid",
|
||||
"header": "fixed",
|
||||
"product_card": "modern"
|
||||
},
|
||||
"social_links": {},
|
||||
"custom_css": None,
|
||||
"css_variables": {
|
||||
"--color-primary": "#6366f1",
|
||||
"--color-secondary": "#8b5cf6",
|
||||
"--color-accent": "#ec4899",
|
||||
"--color-background": "#ffffff",
|
||||
"--color-text": "#1f2937",
|
||||
"--color-border": "#e5e7eb",
|
||||
"--font-heading": "Inter, sans-serif",
|
||||
"--font-body": "Inter, sans-serif",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def theme_context_middleware(request: Request, call_next):
|
||||
"""
|
||||
Middleware to inject theme context into request state.
|
||||
|
||||
This runs AFTER vendor_context_middleware has set request.state.vendor
|
||||
"""
|
||||
# Only inject theme for shop pages (not admin or API)
|
||||
if hasattr(request.state, 'vendor') and request.state.vendor:
|
||||
vendor = request.state.vendor
|
||||
|
||||
# Get database session
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
# Get vendor theme
|
||||
theme = ThemeContextManager.get_vendor_theme(db, vendor.id)
|
||||
request.state.theme = theme
|
||||
|
||||
logger.debug(
|
||||
f"Theme loaded for vendor {vendor.name}: {theme['theme_name']}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load theme for vendor {vendor.id}: {e}")
|
||||
# Fallback to default theme
|
||||
request.state.theme = ThemeContextManager.get_default_theme()
|
||||
finally:
|
||||
db.close()
|
||||
else:
|
||||
# No vendor context, use default theme
|
||||
request.state.theme = ThemeContextManager.get_default_theme()
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
def get_current_theme(request: Request) -> dict:
|
||||
"""Helper function to get current theme from request state."""
|
||||
return getattr(request.state, "theme", ThemeContextManager.get_default_theme())
|
||||
|
||||
|
||||
# Add to main.py after vendor_context_middleware:
|
||||
"""
|
||||
# Add theme context middleware (must be after vendor context)
|
||||
app.middleware("http")(theme_context_middleware)
|
||||
"""
|
||||
@@ -0,0 +1,143 @@
|
||||
# models/database/vendor_theme.py
|
||||
"""
|
||||
Vendor Theme Configuration Model
|
||||
Allows each vendor to customize their shop's appearance
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Text, JSON, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class VendorTheme(Base):
|
||||
"""
|
||||
Stores theme configuration for each vendor's shop.
|
||||
|
||||
Each vendor can have:
|
||||
- Custom colors (primary, secondary, accent)
|
||||
- Custom fonts
|
||||
- Custom logo and favicon
|
||||
- Custom CSS overrides
|
||||
- Layout preferences
|
||||
"""
|
||||
__tablename__ = "vendor_themes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=False, unique=True)
|
||||
|
||||
# Basic Theme Settings
|
||||
theme_name = Column(String(100), default="default") # e.g., "modern", "classic", "minimal"
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Color Scheme (JSON for flexibility)
|
||||
colors = Column(JSON, default={
|
||||
"primary": "#6366f1", # Indigo
|
||||
"secondary": "#8b5cf6", # Purple
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb" # Gray-200
|
||||
})
|
||||
|
||||
# Typography
|
||||
font_family_heading = Column(String(100), default="Inter, sans-serif")
|
||||
font_family_body = Column(String(100), default="Inter, sans-serif")
|
||||
|
||||
# Branding Assets
|
||||
logo_url = Column(String(500), nullable=True) # Path to vendor logo
|
||||
logo_dark_url = Column(String(500), nullable=True) # Dark mode logo
|
||||
favicon_url = Column(String(500), nullable=True) # Favicon
|
||||
banner_url = Column(String(500), nullable=True) # Homepage banner
|
||||
|
||||
# Layout Preferences
|
||||
layout_style = Column(String(50), default="grid") # grid, list, masonry
|
||||
header_style = Column(String(50), default="fixed") # fixed, static, transparent
|
||||
product_card_style = Column(String(50), default="modern") # modern, classic, minimal
|
||||
|
||||
# Custom CSS (for advanced customization)
|
||||
custom_css = Column(Text, nullable=True)
|
||||
|
||||
# Social Media Links
|
||||
social_links = Column(JSON, default={}) # {facebook: "url", instagram: "url", etc.}
|
||||
|
||||
# SEO & Meta
|
||||
meta_title_template = Column(String(200), nullable=True) # e.g., "{product_name} - {shop_name}"
|
||||
meta_description = Column(Text, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="theme")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VendorTheme(vendor_id={self.vendor_id}, theme_name='{self.theme_name}')>"
|
||||
|
||||
@property
|
||||
def primary_color(self):
|
||||
"""Get primary color from JSON"""
|
||||
return self.colors.get("primary", "#6366f1")
|
||||
|
||||
@property
|
||||
def css_variables(self):
|
||||
"""Generate CSS custom properties from theme config"""
|
||||
return {
|
||||
"--color-primary": self.colors.get("primary", "#6366f1"),
|
||||
"--color-secondary": self.colors.get("secondary", "#8b5cf6"),
|
||||
"--color-accent": self.colors.get("accent", "#ec4899"),
|
||||
"--color-background": self.colors.get("background", "#ffffff"),
|
||||
"--color-text": self.colors.get("text", "#1f2937"),
|
||||
"--color-border": self.colors.get("border", "#e5e7eb"),
|
||||
"--font-heading": self.font_family_heading,
|
||||
"--font-body": self.font_family_body,
|
||||
}
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert theme to dictionary for template rendering"""
|
||||
return {
|
||||
"theme_name": self.theme_name,
|
||||
"colors": self.colors,
|
||||
"fonts": {
|
||||
"heading": self.font_family_heading,
|
||||
"body": self.font_family_body,
|
||||
},
|
||||
"branding": {
|
||||
"logo": self.logo_url,
|
||||
"logo_dark": self.logo_dark_url,
|
||||
"favicon": self.favicon_url,
|
||||
"banner": self.banner_url,
|
||||
},
|
||||
"layout": {
|
||||
"style": self.layout_style,
|
||||
"header": self.header_style,
|
||||
"product_card": self.product_card_style,
|
||||
},
|
||||
"social_links": self.social_links,
|
||||
"custom_css": self.custom_css,
|
||||
"css_variables": self.css_variables,
|
||||
}
|
||||
|
||||
|
||||
# Update Vendor model to include theme relationship
|
||||
"""
|
||||
Add to models/database/vendor.py:
|
||||
|
||||
theme = relationship(
|
||||
"VendorTheme",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
@property
|
||||
def active_theme(self):
|
||||
'''Get vendor's active theme or return default'''
|
||||
if self.theme and self.theme.is_active:
|
||||
return self.theme
|
||||
return None
|
||||
"""
|
||||
@@ -0,0 +1,478 @@
|
||||
# Vendor Domains - Architecture Diagram
|
||||
|
||||
## System Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CLIENT REQUEST │
|
||||
│ POST /vendors/1/domains │
|
||||
│ {"domain": "myshop.com"} │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ENDPOINT LAYER │
|
||||
│ app/api/v1/admin/vendor_domains.py │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ @router.post("/{vendor_id}/domains") │
|
||||
│ def add_vendor_domain( │
|
||||
│ vendor_id: int, │
|
||||
│ domain_data: VendorDomainCreate, ◄───┐ │
|
||||
│ db: Session, │ │
|
||||
│ current_admin: User │ │
|
||||
│ ): │ │
|
||||
│ domain = vendor_domain_service │ │
|
||||
│ .add_domain(...) │ │
|
||||
│ return VendorDomainResponse(...) │ │
|
||||
│ │ │
|
||||
└─────────────────────┬───────────────────────┼───────────────────┘
|
||||
│ │
|
||||
│ │
|
||||
┌────────────▼──────────┐ ┌────────▼─────────┐
|
||||
│ Pydantic Validation │ │ Authentication │
|
||||
│ (Auto by FastAPI) │ │ Dependency │
|
||||
└────────────┬──────────┘ └──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SERVICE LAYER │
|
||||
│ app/services/vendor_domain_service.py │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ class VendorDomainService: │
|
||||
│ │
|
||||
│ def add_domain(db, vendor_id, domain_data): │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 1. Verify vendor exists │ │
|
||||
│ │ 2. Check domain limit │ │
|
||||
│ │ 3. Validate domain format │ │
|
||||
│ │ 4. Check uniqueness │ │
|
||||
│ │ 5. Handle primary domain logic │ │
|
||||
│ │ 6. Create database record │ │
|
||||
│ │ 7. Generate verification token │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ Raises Custom Exceptions │ │
|
||||
│ │ - VendorNotFoundException │ │
|
||||
│ │ - DomainAlreadyExistsException │ │
|
||||
│ │ - MaxDomainsReachedException │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DATABASE LAYER │
|
||||
│ models/database/vendor_domain.py │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ class VendorDomain(Base): │
|
||||
│ id: int │
|
||||
│ vendor_id: int (FK) │
|
||||
│ domain: str (unique) │
|
||||
│ is_primary: bool │
|
||||
│ is_active: bool │
|
||||
│ is_verified: bool │
|
||||
│ verification_token: str │
|
||||
│ ssl_status: str │
|
||||
│ ... │
|
||||
│ │
|
||||
└─────────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DATABASE │
|
||||
│ PostgreSQL / MySQL │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Request Flow Diagram
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Client │
|
||||
└────┬─────┘
|
||||
│ POST /vendors/1/domains
|
||||
│ {"domain": "myshop.com", "is_primary": true}
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ FastAPI Router │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 1. URL Routing │ │
|
||||
│ │ 2. Pydantic Validation │ │
|
||||
│ │ 3. Dependency Injection │ │
|
||||
│ │ - get_db() │ │
|
||||
│ │ - get_current_admin_user() │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Endpoint Function │
|
||||
│ add_vendor_domain() │
|
||||
│ │
|
||||
│ ✓ Receives validated data │
|
||||
│ ✓ Has DB session │
|
||||
│ ✓ Has authenticated admin user │
|
||||
│ ✓ Calls service layer │
|
||||
│ ✓ Returns response model │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Service Layer │
|
||||
│ vendor_domain_service.add_domain() │
|
||||
│ │
|
||||
│ Business Logic: │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Vendor Validation │ │
|
||||
│ │ ├─ Check vendor exists │ │
|
||||
│ │ └─ Get vendor object │ │
|
||||
│ │ │ │
|
||||
│ │ Limit Checking │ │
|
||||
│ │ ├─ Count existing domains │ │
|
||||
│ │ └─ Enforce max limit │ │
|
||||
│ │ │ │
|
||||
│ │ Domain Validation │ │
|
||||
│ │ ├─ Normalize format │ │
|
||||
│ │ ├─ Check reserved subdomains │ │
|
||||
│ │ └─ Validate regex pattern │ │
|
||||
│ │ │ │
|
||||
│ │ Uniqueness Check │ │
|
||||
│ │ └─ Query existing domains │ │
|
||||
│ │ │ │
|
||||
│ │ Primary Domain Logic │ │
|
||||
│ │ └─ Unset other primary domains │ │
|
||||
│ │ │ │
|
||||
│ │ Create Record │ │
|
||||
│ │ ├─ Generate verification token │ │
|
||||
│ │ ├─ Set initial status │ │
|
||||
│ │ └─ Create VendorDomain object │ │
|
||||
│ │ │ │
|
||||
│ │ Database Transaction │ │
|
||||
│ │ ├─ db.add() │ │
|
||||
│ │ ├─ db.commit() │ │
|
||||
│ │ └─ db.refresh() │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Database │
|
||||
│ INSERT INTO vendor_domains ... │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Return to Endpoint │
|
||||
│ ← VendorDomain object │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Endpoint Response │
|
||||
│ VendorDomainResponse( │
|
||||
│ id=1, │
|
||||
│ domain="myshop.com", │
|
||||
│ is_verified=False, │
|
||||
│ verification_token="abc123...", │
|
||||
│ ... │
|
||||
│ ) │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ FastAPI Serialization │
|
||||
│ Convert to JSON │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ HTTP Response (201 Created) │
|
||||
│ { │
|
||||
│ "id": 1, │
|
||||
│ "domain": "myshop.com", │
|
||||
│ "is_verified": false, │
|
||||
│ "verification_token": "abc123...", │
|
||||
│ ... │
|
||||
│ } │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Client │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
## Error Handling Flow
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Client │
|
||||
└────┬─────┘
|
||||
│ POST /vendors/1/domains
|
||||
│ {"domain": "existing.com"}
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Service Layer │
|
||||
│ │
|
||||
│ def add_domain(...): │
|
||||
│ if self._domain_exists(db, domain): │
|
||||
│ raise VendorDomainAlready │
|
||||
│ ExistsException( │
|
||||
│ domain="existing.com", │
|
||||
│ existing_vendor_id=2 │
|
||||
│ ) │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
│ Exception raised
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Exception Handler │
|
||||
│ app/exceptions/handler.py │
|
||||
│ │
|
||||
│ @app.exception_handler(LetzShopException) │
|
||||
│ async def custom_exception_handler(...): │
|
||||
│ return JSONResponse( │
|
||||
│ status_code=exc.status_code, │
|
||||
│ content=exc.to_dict() │
|
||||
│ ) │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ HTTP Response (409 Conflict) │
|
||||
│ { │
|
||||
│ "error_code": "VENDOR_DOMAIN_ │
|
||||
│ ALREADY_EXISTS", │
|
||||
│ "message": "Domain 'existing.com' │
|
||||
│ is already registered", │
|
||||
│ "status_code": 409, │
|
||||
│ "details": { │
|
||||
│ "domain": "existing.com", │
|
||||
│ "existing_vendor_id": 2 │
|
||||
│ } │
|
||||
│ } │
|
||||
└────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Client │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
## Component Interaction Diagram
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Endpoints │ ◄─── HTTP Requests from client
|
||||
│ (HTTP Layer) │ ───► HTTP Responses to client
|
||||
└────────┬────────┘
|
||||
│ Calls
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Service │ ◄─── Business logic
|
||||
│ Layer │ ───► Returns domain objects
|
||||
└────────┬────────┘ or raises exceptions
|
||||
│ Uses
|
||||
│
|
||||
├──────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Database │ │ Exceptions │
|
||||
│ Models │ │ (Custom) │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ SQLAlchemy │ │ Exception │
|
||||
│ ORM │ │ Handler │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Database │ │ JSON Error │
|
||||
│ (PostgreSQL) │ │ Response │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## Data Flow for Domain Verification
|
||||
|
||||
```
|
||||
Step 1: Add Domain
|
||||
┌──────────┐
|
||||
│ Admin │ POST /vendors/1/domains
|
||||
└────┬─────┘ {"domain": "myshop.com"}
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ System creates domain record │
|
||||
│ - domain: "myshop.com" │
|
||||
│ - is_verified: false │
|
||||
│ - verification_token: "abc123..." │
|
||||
└────────────────────────────────────┘
|
||||
|
||||
Step 2: Get Instructions
|
||||
┌──────────┐
|
||||
│ Admin │ GET /domains/1/verification-instructions
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ System returns instructions: │
|
||||
│ "Add TXT record: │
|
||||
│ _letzshop-verify.myshop.com │
|
||||
│ Value: abc123..." │
|
||||
└────────────────────────────────────┘
|
||||
|
||||
Step 3: Vendor Adds DNS Record
|
||||
┌──────────┐
|
||||
│ Vendor │ Adds TXT record at DNS provider
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ DNS Provider (GoDaddy/etc) │
|
||||
│ _letzshop-verify.myshop.com TXT │
|
||||
│ "abc123..." │
|
||||
└────────────────────────────────────┘
|
||||
|
||||
Step 4: Verify Domain
|
||||
┌──────────┐
|
||||
│ Admin │ POST /domains/1/verify
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ System: │
|
||||
│ 1. Queries DNS for TXT record │
|
||||
│ 2. Checks token matches │
|
||||
│ 3. Updates domain: │
|
||||
│ - is_verified: true │
|
||||
│ - verified_at: now() │
|
||||
└────────────────────────────────────┘
|
||||
|
||||
Step 5: Activate Domain
|
||||
┌──────────┐
|
||||
│ Admin │ PUT /domains/1 {"is_active": true}
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ System activates domain: │
|
||||
│ - is_active: true │
|
||||
│ - Domain now routes to vendor │
|
||||
└────────────────────────────────────┘
|
||||
|
||||
Result: Domain Active!
|
||||
┌──────────────┐
|
||||
│ Customer │ Visits https://myshop.com
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Middleware detects custom domain │
|
||||
│ Routes to Vendor 1 │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## File Structure Visual
|
||||
|
||||
```
|
||||
project/
|
||||
│
|
||||
├── app/
|
||||
│ ├── api/
|
||||
│ │ └── v1/
|
||||
│ │ └── admin/
|
||||
│ │ ├── vendors.py ✓ Existing (reference)
|
||||
│ │ └── vendor_domains.py ★ NEW (endpoints)
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── vendor_service.py ✓ Existing (reference)
|
||||
│ │ └── vendor_domain_service.py ★ NEW (business logic)
|
||||
│ │
|
||||
│ └── exceptions/
|
||||
│ ├── __init__.py ✓ UPDATE (add exports)
|
||||
│ ├── base.py ✓ Existing
|
||||
│ ├── auth.py ✓ Existing
|
||||
│ ├── admin.py ✓ Existing
|
||||
│ └── vendor_domain.py ★ NEW (custom exceptions)
|
||||
│
|
||||
└── models/
|
||||
├── schema/
|
||||
│ ├── vendor.py ✓ Existing
|
||||
│ └── vendor_domain.py ★ NEW (pydantic schemas)
|
||||
│
|
||||
└── database/
|
||||
├── vendor.py ✓ UPDATE (add domains relationship)
|
||||
└── vendor_domain.py ✓ Existing (database model)
|
||||
|
||||
Legend:
|
||||
★ NEW - Files to create
|
||||
✓ Existing - Files already exist
|
||||
✓ UPDATE - Files to modify
|
||||
```
|
||||
|
||||
## Separation of Concerns Visual
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ENDPOINT LAYER │
|
||||
│ - HTTP request/response │
|
||||
│ - FastAPI decorators │
|
||||
│ - Dependency injection │
|
||||
│ - Response models │
|
||||
│ - Documentation │
|
||||
│ │
|
||||
│ ✓ No business logic │
|
||||
│ ✓ No database operations │
|
||||
│ ✓ No validation (handled by Pydantic) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
│ Calls
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────────────────┐
|
||||
│ SERVICE LAYER │
|
||||
│ - Business logic │
|
||||
│ - Database operations │
|
||||
│ - Transaction management │
|
||||
│ - Error handling │
|
||||
│ - Validation logic │
|
||||
│ - Logging │
|
||||
│ │
|
||||
│ ✓ Reusable methods │
|
||||
│ ✓ Unit testable │
|
||||
│ ✓ No HTTP concerns │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
│ Uses
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────────────────┐
|
||||
│ DATABASE LAYER │
|
||||
│ - SQLAlchemy models │
|
||||
│ - Table definitions │
|
||||
│ - Relationships │
|
||||
│ - Database constraints │
|
||||
│ │
|
||||
│ ✓ Pure data models │
|
||||
│ ✓ No business logic │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
This architecture ensures:
|
||||
- ✅ Clean separation of concerns
|
||||
- ✅ Easy to test each layer
|
||||
- ✅ Reusable business logic
|
||||
- ✅ Maintainable codebase
|
||||
- ✅ Follows SOLID principles
|
||||
@@ -0,0 +1,567 @@
|
||||
# Vendor Domains - Refactored Architecture Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains the refactored vendor domains implementation that follows your application's architecture patterns with proper separation of concerns.
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
The implementation follows these key principles:
|
||||
|
||||
1. **Separation of Concerns**: Endpoints, service layer, schemas, and database models are separated
|
||||
2. **Exception-Based Error Handling**: Custom exceptions with proper HTTP status codes
|
||||
3. **Service Layer Pattern**: Business logic isolated in service classes
|
||||
4. **Pydantic Validation**: Input validation using Pydantic schemas
|
||||
5. **Consistent Response Format**: Standardized response models
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/
|
||||
│ └── v1/
|
||||
│ └── admin/
|
||||
│ └── vendor_domains.py # HTTP endpoints (NEW)
|
||||
├── services/
|
||||
│ └── vendor_domain_service.py # Business logic (NEW)
|
||||
├── exceptions/
|
||||
│ ├── __init__.py # Updated exports
|
||||
│ └── vendor_domain.py # Domain exceptions (NEW)
|
||||
models/
|
||||
├── schema/
|
||||
│ └── vendor_domain.py # Pydantic schemas (NEW)
|
||||
└── database/
|
||||
├── vendor.py # Updated Vendor model
|
||||
└── vendor_domain.py # VendorDomain model
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Exceptions (`app/exceptions/vendor_domain.py`)
|
||||
|
||||
Custom exceptions for domain operations:
|
||||
|
||||
```python
|
||||
# Resource Not Found (404)
|
||||
- VendorDomainNotFoundException
|
||||
|
||||
# Conflicts (409)
|
||||
- VendorDomainAlreadyExistsException
|
||||
|
||||
# Validation (422)
|
||||
- InvalidDomainFormatException
|
||||
- ReservedDomainException
|
||||
|
||||
# Business Logic (400)
|
||||
- DomainNotVerifiedException
|
||||
- DomainVerificationFailedException
|
||||
- DomainAlreadyVerifiedException
|
||||
- MultiplePrimaryDomainsException
|
||||
- MaxDomainsReachedException
|
||||
- UnauthorizedDomainAccessException
|
||||
|
||||
# External Service (502)
|
||||
- DNSVerificationException
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Inherit from appropriate base exceptions
|
||||
- Include relevant context in `details` dict
|
||||
- Proper HTTP status codes
|
||||
- Clear, actionable error messages
|
||||
|
||||
### 2. Pydantic Schemas (`models/schema/vendor_domain.py`)
|
||||
|
||||
Input validation and response models:
|
||||
|
||||
```python
|
||||
# Request Schemas
|
||||
- VendorDomainCreate # For adding domains
|
||||
- VendorDomainUpdate # For updating settings
|
||||
|
||||
# Response Schemas
|
||||
- VendorDomainResponse # Single domain
|
||||
- VendorDomainListResponse # List of domains
|
||||
- DomainVerificationInstructions # DNS instructions
|
||||
- DomainVerificationResponse # Verification result
|
||||
- DomainDeletionResponse # Deletion confirmation
|
||||
```
|
||||
|
||||
**Validation Features:**
|
||||
- Domain normalization (lowercase, remove protocol)
|
||||
- Reserved subdomain checking
|
||||
- Format validation using regex
|
||||
- Field validators with custom error messages
|
||||
|
||||
### 3. Service Layer (`app/services/vendor_domain_service.py`)
|
||||
|
||||
Business logic and database operations:
|
||||
|
||||
```python
|
||||
class VendorDomainService:
|
||||
# Core Operations
|
||||
add_domain() # Add custom domain
|
||||
get_vendor_domains() # List vendor's domains
|
||||
get_domain_by_id() # Get single domain
|
||||
update_domain() # Update settings
|
||||
delete_domain() # Remove domain
|
||||
|
||||
# Verification
|
||||
verify_domain() # DNS verification
|
||||
get_verification_instructions() # Get DNS instructions
|
||||
|
||||
# Private Helpers
|
||||
_get_vendor_by_id_or_raise() # Vendor lookup
|
||||
_check_domain_limit() # Enforce max domains
|
||||
_domain_exists() # Check uniqueness
|
||||
_validate_domain_format() # Format validation
|
||||
_unset_primary_domains() # Primary domain logic
|
||||
```
|
||||
|
||||
**Service Pattern:**
|
||||
- All database operations in service layer
|
||||
- Raises custom exceptions (not HTTPException)
|
||||
- Transaction management (commit/rollback)
|
||||
- Comprehensive logging
|
||||
- Helper methods with `_` prefix for internal use
|
||||
|
||||
### 4. API Endpoints (`app/api/v1/admin/vendor_domains.py`)
|
||||
|
||||
HTTP layer for domain management:
|
||||
|
||||
```python
|
||||
# Endpoints
|
||||
POST /vendors/{vendor_id}/domains # Add domain
|
||||
GET /vendors/{vendor_id}/domains # List domains
|
||||
GET /vendors/domains/{domain_id} # Get domain
|
||||
PUT /vendors/domains/{domain_id} # Update domain
|
||||
DELETE /vendors/domains/{domain_id} # Delete domain
|
||||
POST /vendors/domains/{domain_id}/verify # Verify ownership
|
||||
GET /vendors/domains/{domain_id}/verification-instructions # Get instructions
|
||||
```
|
||||
|
||||
**Endpoint Pattern:**
|
||||
- Only handle HTTP concerns (request/response)
|
||||
- Delegate business logic to service layer
|
||||
- Use Pydantic schemas for validation
|
||||
- Proper dependency injection
|
||||
- Comprehensive docstrings
|
||||
- No direct database access
|
||||
|
||||
## Comparison: Old vs New
|
||||
|
||||
### Old Implementation Issues
|
||||
|
||||
```python
|
||||
# ❌ Mixed concerns
|
||||
@router.post("/{vendor_id}/domains")
|
||||
def add_vendor_domain(...):
|
||||
# Validation in endpoint
|
||||
domain = VendorDomain.normalize_domain(domain_data.domain)
|
||||
|
||||
# Business logic in endpoint
|
||||
if not domain or '/' in domain:
|
||||
raise HTTPException(400, "Invalid domain")
|
||||
|
||||
# Database operations in endpoint
|
||||
existing = db.query(VendorDomain).filter(...).first()
|
||||
if existing:
|
||||
raise HTTPException(409, "Domain exists")
|
||||
|
||||
# Direct database access
|
||||
domain = VendorDomain(...)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Business logic mixed with HTTP layer
|
||||
- HTTPException instead of custom exceptions
|
||||
- No service layer separation
|
||||
- Direct database access in endpoints
|
||||
- Validation scattered across endpoint
|
||||
- Hard to test business logic
|
||||
|
||||
### New Implementation
|
||||
|
||||
```python
|
||||
# ✅ Proper separation
|
||||
|
||||
# Endpoint (HTTP layer only)
|
||||
@router.post("/{vendor_id}/domains", response_model=VendorDomainResponse)
|
||||
def add_vendor_domain(
|
||||
vendor_id: int = Path(..., gt=0),
|
||||
domain_data: VendorDomainCreate = Body(...), # Pydantic validation
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Add domain - delegates to service layer"""
|
||||
domain = vendor_domain_service.add_domain(db, vendor_id, domain_data)
|
||||
return VendorDomainResponse(...mapping...)
|
||||
|
||||
# Service (Business logic)
|
||||
class VendorDomainService:
|
||||
def add_domain(self, db, vendor_id, domain_data):
|
||||
"""Business logic and database operations"""
|
||||
# Verify vendor
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
|
||||
# Check limits
|
||||
self._check_domain_limit(db, vendor_id)
|
||||
|
||||
# Validate format
|
||||
self._validate_domain_format(normalized_domain)
|
||||
|
||||
# Check uniqueness
|
||||
if self._domain_exists(db, normalized_domain):
|
||||
raise VendorDomainAlreadyExistsException(...) # Custom exception
|
||||
|
||||
# Business logic
|
||||
if domain_data.is_primary:
|
||||
self._unset_primary_domains(db, vendor_id)
|
||||
|
||||
# Database operations
|
||||
new_domain = VendorDomain(...)
|
||||
db.add(new_domain)
|
||||
db.commit()
|
||||
return new_domain
|
||||
|
||||
# Schema (Validation)
|
||||
class VendorDomainCreate(BaseModel):
|
||||
domain: str
|
||||
is_primary: bool = False
|
||||
|
||||
@field_validator('domain')
|
||||
def validate_domain(cls, v: str) -> str:
|
||||
"""Normalize and validate domain"""
|
||||
# Validation logic here
|
||||
return normalized_domain
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Clean separation of concerns
|
||||
- Custom exceptions with proper status codes
|
||||
- Testable business logic
|
||||
- Reusable service methods
|
||||
- Centralized validation
|
||||
- Easy to maintain
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Add Exception File
|
||||
|
||||
```bash
|
||||
# Create new exception file
|
||||
app/exceptions/vendor_domain.py
|
||||
```
|
||||
|
||||
Copy content from `vendor_domain_exceptions.py`
|
||||
|
||||
### 2. Update Exception Exports
|
||||
|
||||
```python
|
||||
# app/exceptions/__init__.py
|
||||
from .vendor_domain import (
|
||||
VendorDomainNotFoundException,
|
||||
VendorDomainAlreadyExistsException,
|
||||
# ... other exceptions
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# ... existing exports
|
||||
"VendorDomainNotFoundException",
|
||||
"VendorDomainAlreadyExistsException",
|
||||
# ... other exports
|
||||
]
|
||||
```
|
||||
|
||||
### 3. Add Pydantic Schemas
|
||||
|
||||
```bash
|
||||
# Create schema file
|
||||
models/schema/vendor_domain.py
|
||||
```
|
||||
|
||||
Copy content from `vendor_domain_schema.py`
|
||||
|
||||
### 4. Add Service Layer
|
||||
|
||||
```bash
|
||||
# Create service file
|
||||
app/services/vendor_domain_service.py
|
||||
```
|
||||
|
||||
Copy content from `vendor_domain_service.py`
|
||||
|
||||
### 5. Replace Endpoint File
|
||||
|
||||
```bash
|
||||
# Replace existing file
|
||||
app/api/v1/admin/vendor_domains.py
|
||||
```
|
||||
|
||||
Copy content from `vendor_domains.py`
|
||||
|
||||
### 6. Install DNS Library
|
||||
|
||||
```bash
|
||||
pip install dnspython
|
||||
```
|
||||
|
||||
Required for DNS verification functionality.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Adding a Domain
|
||||
|
||||
```python
|
||||
# Request
|
||||
POST /api/v1/admin/vendors/1/domains
|
||||
{
|
||||
"domain": "myshop.com",
|
||||
"is_primary": true
|
||||
}
|
||||
|
||||
# Response (201)
|
||||
{
|
||||
"id": 1,
|
||||
"vendor_id": 1,
|
||||
"domain": "myshop.com",
|
||||
"is_primary": true,
|
||||
"is_active": false,
|
||||
"is_verified": false,
|
||||
"ssl_status": "pending",
|
||||
"verification_token": "abc123...",
|
||||
"verified_at": null,
|
||||
"created_at": "2025-01-15T10:00:00Z",
|
||||
"updated_at": "2025-01-15T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Verification
|
||||
|
||||
```python
|
||||
# Step 1: Get verification instructions
|
||||
GET /api/v1/admin/vendors/domains/1/verification-instructions
|
||||
|
||||
# Response
|
||||
{
|
||||
"domain": "myshop.com",
|
||||
"verification_token": "abc123xyz...",
|
||||
"instructions": {
|
||||
"step1": "Go to your domain's DNS settings",
|
||||
"step2": "Add a new TXT record with the following values:",
|
||||
"step3": "Wait for DNS propagation (5-15 minutes)",
|
||||
"step4": "Click 'Verify Domain' button"
|
||||
},
|
||||
"txt_record": {
|
||||
"type": "TXT",
|
||||
"name": "_letzshop-verify",
|
||||
"value": "abc123xyz...",
|
||||
"ttl": 3600
|
||||
}
|
||||
}
|
||||
|
||||
# Step 2: Vendor adds DNS record
|
||||
# _letzshop-verify.myshop.com TXT "abc123xyz..."
|
||||
|
||||
# Step 3: Verify domain
|
||||
POST /api/v1/admin/vendors/domains/1/verify
|
||||
|
||||
# Response (200)
|
||||
{
|
||||
"message": "Domain myshop.com verified successfully",
|
||||
"domain": "myshop.com",
|
||||
"verified_at": "2025-01-15T10:15:00Z",
|
||||
"is_verified": true
|
||||
}
|
||||
```
|
||||
|
||||
### Activating Domain
|
||||
|
||||
```python
|
||||
# After verification, activate domain
|
||||
PUT /api/v1/admin/vendors/domains/1
|
||||
{
|
||||
"is_active": true
|
||||
}
|
||||
|
||||
# Response (200)
|
||||
{
|
||||
"id": 1,
|
||||
"vendor_id": 1,
|
||||
"domain": "myshop.com",
|
||||
"is_primary": true,
|
||||
"is_active": true,
|
||||
"is_verified": true,
|
||||
"ssl_status": "pending",
|
||||
"verified_at": "2025-01-15T10:15:00Z",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Examples
|
||||
|
||||
### Domain Already Exists
|
||||
|
||||
```python
|
||||
POST /api/v1/admin/vendors/1/domains
|
||||
{
|
||||
"domain": "existing.com"
|
||||
}
|
||||
|
||||
# Response (409 Conflict)
|
||||
{
|
||||
"error_code": "VENDOR_DOMAIN_ALREADY_EXISTS",
|
||||
"message": "Domain 'existing.com' is already registered",
|
||||
"status_code": 409,
|
||||
"details": {
|
||||
"domain": "existing.com",
|
||||
"existing_vendor_id": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Invalid Domain Format
|
||||
|
||||
```python
|
||||
POST /api/v1/admin/vendors/1/domains
|
||||
{
|
||||
"domain": "admin.example.com" # Reserved subdomain
|
||||
}
|
||||
|
||||
# Response (422 Validation Error)
|
||||
{
|
||||
"error_code": "RESERVED_DOMAIN",
|
||||
"message": "Domain cannot use reserved subdomain: admin",
|
||||
"status_code": 422,
|
||||
"details": {
|
||||
"domain": "admin.example.com",
|
||||
"reserved_part": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Verification Failed
|
||||
|
||||
```python
|
||||
POST /api/v1/admin/vendors/domains/1/verify
|
||||
|
||||
# Response (400 Bad Request)
|
||||
{
|
||||
"error_code": "DOMAIN_VERIFICATION_FAILED",
|
||||
"message": "Domain verification failed for 'myshop.com': Verification token not found in DNS records",
|
||||
"status_code": 400,
|
||||
"details": {
|
||||
"domain": "myshop.com",
|
||||
"reason": "Verification token not found in DNS records"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```python
|
||||
# tests/unit/services/test_vendor_domain_service.py
|
||||
def test_add_domain_success(db_session):
|
||||
"""Test successful domain addition"""
|
||||
service = VendorDomainService()
|
||||
domain_data = VendorDomainCreate(
|
||||
domain="test.com",
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
domain = service.add_domain(db_session, vendor_id=1, domain_data=domain_data)
|
||||
|
||||
assert domain.domain == "test.com"
|
||||
assert domain.is_primary is True
|
||||
assert domain.is_verified is False
|
||||
|
||||
|
||||
def test_add_domain_already_exists(db_session):
|
||||
"""Test adding duplicate domain raises exception"""
|
||||
service = VendorDomainService()
|
||||
|
||||
with pytest.raises(VendorDomainAlreadyExistsException):
|
||||
service.add_domain(db_session, vendor_id=1, domain_data=...)
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```python
|
||||
# tests/integration/api/test_vendor_domains.py
|
||||
def test_add_domain_endpoint(client, admin_headers):
|
||||
"""Test domain addition endpoint"""
|
||||
response = client.post(
|
||||
"/api/v1/admin/vendors/1/domains",
|
||||
json={"domain": "newshop.com", "is_primary": False},
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["domain"] == "newshop.com"
|
||||
assert data["is_verified"] is False
|
||||
|
||||
|
||||
def test_verify_domain_not_found(client, admin_headers):
|
||||
"""Test verification with non-existent domain"""
|
||||
response = client.post(
|
||||
"/api/v1/admin/vendors/domains/99999/verify",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["error_code"] == "VENDOR_DOMAIN_NOT_FOUND"
|
||||
```
|
||||
|
||||
## Benefits of This Architecture
|
||||
|
||||
### 1. Maintainability
|
||||
- Clear separation makes code easy to understand
|
||||
- Changes isolated to appropriate layers
|
||||
- Easy to locate and fix bugs
|
||||
|
||||
### 2. Testability
|
||||
- Service layer can be unit tested independently
|
||||
- Mock dependencies easily
|
||||
- Integration tests for endpoints
|
||||
|
||||
### 3. Reusability
|
||||
- Service methods can be called from anywhere
|
||||
- Schemas reused across endpoints
|
||||
- Exceptions standardized
|
||||
|
||||
### 4. Scalability
|
||||
- Add new endpoints without duplicating logic
|
||||
- Extend service layer for new features
|
||||
- Easy to add caching, queuing, etc.
|
||||
|
||||
### 5. Error Handling
|
||||
- Consistent error responses
|
||||
- Proper HTTP status codes
|
||||
- Detailed error information for debugging
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Database Migration**: Create migration for vendor_domains table if not exists
|
||||
2. **Middleware Update**: Update vendor detection middleware to check custom domains
|
||||
3. **Frontend Integration**: Build UI for domain management
|
||||
4. **SSL Automation**: Add automatic SSL certificate provisioning
|
||||
5. **Monitoring**: Add logging and monitoring for domain operations
|
||||
6. **Rate Limiting**: Implement rate limits for domain additions
|
||||
7. **Webhooks**: Add webhooks for domain status changes
|
||||
|
||||
## Conclusion
|
||||
|
||||
This refactored implementation follows your application's architecture patterns:
|
||||
- ✅ Proper separation of concerns
|
||||
- ✅ Exception-based error handling
|
||||
- ✅ Service layer for business logic
|
||||
- ✅ Pydantic schemas for validation
|
||||
- ✅ Clean, maintainable code
|
||||
- ✅ Consistent with existing patterns (vendors.py example)
|
||||
|
||||
The code is now production-ready, maintainable, and follows best practices!
|
||||
916
docs/__temp/__PROJECT_ROADMAP/19_migration_plan_FINAL.md
Normal file
916
docs/__temp/__PROJECT_ROADMAP/19_migration_plan_FINAL.md
Normal file
@@ -0,0 +1,916 @@
|
||||
# Vendor & Users Pages Migration Plan - FINAL
|
||||
## Based on Actual Legacy Files
|
||||
|
||||
**Date:** October 23, 2025
|
||||
**Status:** Icons fixed ✅ | Logout working ✅ | Dashboard migrated ✅
|
||||
|
||||
---
|
||||
|
||||
## 📁 Legacy Files Analysis
|
||||
|
||||
### Existing Files:
|
||||
1. **vendors.html** - Basic placeholder (needs vendor LIST implementation)
|
||||
2. **vendor-edit.html** - Detailed edit form (OLD CSS, needs redesign)
|
||||
3. **vendors.js** - Only has `vendorCreation()` function (NOT vendor list)
|
||||
4. **vendor-edit.js** - Complete edit functionality (uses OLD auth/api pattern)
|
||||
5. **init-alpine.js** - Base Alpine data (theme, menus)
|
||||
|
||||
### Key Findings:
|
||||
- ✅ `vendors.js` currently only has CREATE vendor function
|
||||
- ❌ NO vendor LIST function exists yet
|
||||
- ✅ `vendor-edit.js` exists but uses OLD patterns:
|
||||
- Uses `apiClient` (should use `ApiClient`)
|
||||
- Uses `Auth.isAuthenticated()` pattern
|
||||
- Uses `Utils.confirm()` and custom modals
|
||||
- ✅ Dashboard pattern: Uses `ApiClient`, `Logger`, `Utils.showToast`
|
||||
|
||||
### Pattern Differences:
|
||||
|
||||
**OLD Pattern (vendor-edit.js):**
|
||||
```javascript
|
||||
// OLD API client (lowercase)
|
||||
const response = await apiClient.get('/admin/vendors/1');
|
||||
|
||||
// OLD Auth pattern
|
||||
if (!Auth.isAuthenticated()) { ... }
|
||||
const user = Auth.getCurrentUser();
|
||||
```
|
||||
|
||||
**NEW Pattern (dashboard.js):**
|
||||
```javascript
|
||||
// NEW API client (uppercase)
|
||||
const response = await ApiClient.get('/admin/vendors');
|
||||
|
||||
// NEW Auth - handled by cookie, no client-side check needed
|
||||
// Just call API and let middleware handle it
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Migration Strategy
|
||||
|
||||
### Task 1: Create Vendor List Function (HIGH PRIORITY) 🏪
|
||||
|
||||
**Current State:** vendors.js only has `vendorCreation()` function
|
||||
**Goal:** Add `adminVendors()` function for the vendor LIST page
|
||||
|
||||
#### 1.1 Update vendors.js - Add Vendor List Function
|
||||
|
||||
**File:** `static/admin/js/vendors.js`
|
||||
|
||||
**Action:** ADD new function (keep existing `vendorCreation()` function):
|
||||
|
||||
```javascript
|
||||
// static/admin/js/vendors.js
|
||||
|
||||
// ============================================
|
||||
// VENDOR LIST FUNCTION (NEW - Add this)
|
||||
// ============================================
|
||||
function adminVendors() {
|
||||
return {
|
||||
// State
|
||||
vendors: [],
|
||||
stats: {
|
||||
total: 0,
|
||||
verified: 0,
|
||||
pending: 0,
|
||||
inactive: 0
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
Logger.info('Vendors page initialized', 'VENDORS');
|
||||
await this.loadVendors();
|
||||
await this.loadStats();
|
||||
},
|
||||
|
||||
// Load vendors list
|
||||
async loadVendors() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const response = await ApiClient.get('/admin/vendors');
|
||||
// Handle different response structures
|
||||
this.vendors = response.vendors || response.items || response || [];
|
||||
Logger.info('Vendors loaded', 'VENDORS', { count: this.vendors.length });
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load vendors', 'VENDORS', error);
|
||||
this.error = error.message || 'Failed to load vendors';
|
||||
Utils.showToast('Failed to load vendors', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Load statistics
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await ApiClient.get('/admin/vendors/stats');
|
||||
this.stats = response;
|
||||
Logger.info('Stats loaded', 'VENDORS', this.stats);
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load stats', 'VENDORS', error);
|
||||
// Don't show error toast for stats, just log it
|
||||
}
|
||||
},
|
||||
|
||||
// Format date (matches dashboard pattern)
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
// View vendor details
|
||||
viewVendor(vendorCode) {
|
||||
Logger.info('View vendor', 'VENDORS', { vendorCode });
|
||||
// Navigate to details page or open modal
|
||||
window.location.href = `/admin/vendors/${vendorCode}`;
|
||||
},
|
||||
|
||||
// Edit vendor
|
||||
editVendor(vendorCode) {
|
||||
Logger.info('Edit vendor', 'VENDORS', { vendorCode });
|
||||
window.location.href = `/admin/vendors/${vendorCode}/edit`;
|
||||
},
|
||||
|
||||
// Delete vendor
|
||||
async deleteVendor(vendor) {
|
||||
if (!confirm(`Are you sure you want to delete vendor "${vendor.name}"?\n\nThis action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ApiClient.delete(`/admin/vendors/${vendor.vendor_code}`);
|
||||
Utils.showToast('Vendor deleted successfully', 'success');
|
||||
await this.loadVendors();
|
||||
await this.loadStats();
|
||||
} catch (error) {
|
||||
Logger.error('Failed to delete vendor', 'VENDORS', error);
|
||||
Utils.showToast(error.message || 'Failed to delete vendor', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// Open create modal/page
|
||||
openCreateModal() {
|
||||
Logger.info('Open create vendor', 'VENDORS');
|
||||
// Navigate to create page (or open modal)
|
||||
window.location.href = '/admin/vendors/create';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VENDOR CREATION FUNCTION (EXISTING - Keep this)
|
||||
// ============================================
|
||||
function vendorCreation() {
|
||||
// ... keep your existing vendorCreation function as is ...
|
||||
// (the code you already have)
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Create Vendors List Template
|
||||
|
||||
**File:** `app/templates/admin/vendors.html`
|
||||
|
||||
```jinja2
|
||||
{# app/templates/admin/vendors.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Vendors{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminVendors(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Vendor Management
|
||||
</h2>
|
||||
<a
|
||||
href="/admin/vendors/create"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
|
||||
>
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Vendor
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading vendors...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<p class="font-semibold">Error loading vendors</p>
|
||||
<p class="text-sm" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards - EXACTLY like dashboard -->
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Card: Total Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
||||
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Vendors
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Verified Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('badge-check', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Verified Vendors
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.verified || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Pending Verification -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-yellow-500 bg-yellow-100 rounded-full dark:text-yellow-100 dark:bg-yellow-500">
|
||||
<span x-html="$icon('clock', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Pending
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.pending || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Inactive Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-red-500 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-500">
|
||||
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Inactive
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.inactive || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vendors Table - EXACTLY like dashboard table -->
|
||||
<div x-show="!loading" class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full whitespace-no-wrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Vendor</th>
|
||||
<th class="px-4 py-3">Subdomain</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Created</th>
|
||||
<th class="px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<!-- Empty State -->
|
||||
<template x-if="vendors.length === 0">
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('user-group', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No vendors found</p>
|
||||
<p class="text-xs mt-1">Create your first vendor to get started</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Vendor Rows -->
|
||||
<template x-for="vendor in vendors" :key="vendor.id || vendor.vendor_code">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<!-- Vendor Info with Avatar -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div class="absolute inset-0 rounded-full bg-purple-100 dark:bg-purple-600 flex items-center justify-center">
|
||||
<span class="text-xs font-semibold text-purple-600 dark:text-purple-100"
|
||||
x-text="vendor.name?.charAt(0).toUpperCase() || '?'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="vendor.name"></p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="vendor.vendor_code"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Subdomain -->
|
||||
<td class="px-4 py-3 text-sm" x-text="vendor.subdomain"></td>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="inline-flex items-center px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="vendor.is_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'">
|
||||
<span x-show="vendor.is_verified" x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
|
||||
<span x-text="vendor.is_verified ? 'Verified' : 'Pending'"></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Created Date -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(vendor.created_at)"></td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<!-- View Button -->
|
||||
<button
|
||||
@click="viewVendor(vendor.vendor_code)"
|
||||
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="View details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<button
|
||||
@click="editVendor(vendor.vendor_code)"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Edit vendor"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<button
|
||||
@click="deleteVendor(vendor)"
|
||||
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Delete vendor"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/vendors.js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
**Checklist Task 1:**
|
||||
- [ ] Add `adminVendors()` function to `static/admin/js/vendors.js` (keep existing `vendorCreation()`)
|
||||
- [ ] Create `app/templates/admin/vendors.html` template
|
||||
- [ ] Add backend route for `/admin/vendors`
|
||||
- [ ] Test vendor list page loads
|
||||
- [ ] Test stats cards display
|
||||
- [ ] Test vendor table displays data
|
||||
- [ ] Test action buttons work
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Migrate Vendor Edit Page (HIGH PRIORITY) ✏️
|
||||
|
||||
**Current State:** vendor-edit.js exists but uses OLD patterns
|
||||
**Goal:** Update to use NEW patterns (ApiClient, Logger, no Auth checks)
|
||||
|
||||
#### 2.1 Update vendor-edit.js to NEW Pattern
|
||||
|
||||
**File:** `static/admin/js/vendor-edit.js`
|
||||
|
||||
**Changes Needed:**
|
||||
1. Replace `apiClient` → `ApiClient`
|
||||
2. Remove `Auth.isAuthenticated()` checks (handled by backend)
|
||||
3. Replace `Utils.confirm()` → Use `confirm()`
|
||||
4. Remove custom modals, use simple confirms
|
||||
5. Add Logger calls
|
||||
6. Update route patterns
|
||||
|
||||
```javascript
|
||||
// static/admin/js/vendor-edit.js
|
||||
|
||||
function adminVendorEdit() {
|
||||
return {
|
||||
// State
|
||||
vendor: null,
|
||||
formData: {},
|
||||
errors: {},
|
||||
loadingVendor: false,
|
||||
saving: false,
|
||||
vendorCode: null,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
Logger.info('Vendor edit page initialized', 'VENDOR_EDIT');
|
||||
|
||||
// Get vendor code from URL
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/\/admin\/vendors\/([^\/]+)\/edit/);
|
||||
|
||||
if (match) {
|
||||
this.vendorCode = match[1];
|
||||
Logger.info('Editing vendor', 'VENDOR_EDIT', { vendorCode: this.vendorCode });
|
||||
await this.loadVendor();
|
||||
} else {
|
||||
Logger.error('No vendor code in URL', 'VENDOR_EDIT');
|
||||
Utils.showToast('Invalid vendor URL', 'error');
|
||||
setTimeout(() => window.location.href = '/admin/vendors', 2000);
|
||||
}
|
||||
},
|
||||
|
||||
// Load vendor data
|
||||
async loadVendor() {
|
||||
this.loadingVendor = true;
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.get(`/admin/vendors/${this.vendorCode}`);
|
||||
this.vendor = response;
|
||||
|
||||
// Initialize form data
|
||||
this.formData = {
|
||||
name: response.name || '',
|
||||
subdomain: response.subdomain || '',
|
||||
description: response.description || '',
|
||||
contact_email: response.contact_email || '',
|
||||
contact_phone: response.contact_phone || '',
|
||||
website: response.website || '',
|
||||
business_address: response.business_address || '',
|
||||
tax_number: response.tax_number || ''
|
||||
};
|
||||
|
||||
Logger.info('Vendor loaded', 'VENDOR_EDIT', {
|
||||
vendor_code: this.vendor.vendor_code,
|
||||
name: this.vendor.name
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load vendor', 'VENDOR_EDIT', error);
|
||||
Utils.showToast('Failed to load vendor', 'error');
|
||||
setTimeout(() => window.location.href = '/admin/vendors', 2000);
|
||||
} finally {
|
||||
this.loadingVendor = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Format subdomain
|
||||
formatSubdomain() {
|
||||
this.formData.subdomain = this.formData.subdomain
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
},
|
||||
|
||||
// Submit form
|
||||
async handleSubmit() {
|
||||
Logger.info('Submitting vendor update', 'VENDOR_EDIT');
|
||||
this.errors = {};
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}`,
|
||||
this.formData
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast('Vendor updated successfully', 'success');
|
||||
Logger.info('Vendor updated', 'VENDOR_EDIT', response);
|
||||
|
||||
// Optionally redirect back to list
|
||||
// setTimeout(() => window.location.href = '/admin/vendors', 1500);
|
||||
} catch (error) {
|
||||
Logger.error('Failed to update vendor', 'VENDOR_EDIT', error);
|
||||
|
||||
// Handle validation errors
|
||||
if (error.details && error.details.validation_errors) {
|
||||
error.details.validation_errors.forEach(err => {
|
||||
const field = err.loc?.[1] || err.loc?.[0];
|
||||
if (field) {
|
||||
this.errors[field] = err.msg;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Utils.showToast(error.message || 'Failed to update vendor', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle verification
|
||||
async toggleVerification() {
|
||||
const action = this.vendor.is_verified ? 'unverify' : 'verify';
|
||||
|
||||
// CHANGED: Simple confirm instead of custom modal
|
||||
if (!confirm(`Are you sure you want to ${action} this vendor?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}/verification`,
|
||||
{ is_verified: !this.vendor.is_verified }
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast(`Vendor ${action}ed successfully`, 'success');
|
||||
Logger.info(`Vendor ${action}ed`, 'VENDOR_EDIT');
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to ${action} vendor`, 'VENDOR_EDIT', error);
|
||||
Utils.showToast(`Failed to ${action} vendor`, 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle active status
|
||||
async toggleActive() {
|
||||
const action = this.vendor.is_active ? 'deactivate' : 'activate';
|
||||
|
||||
// CHANGED: Simple confirm instead of custom modal
|
||||
if (!confirm(`Are you sure you want to ${action} this vendor?\n\nThis will affect their operations.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
// CHANGED: apiClient → ApiClient
|
||||
const response = await ApiClient.put(
|
||||
`/admin/vendors/${this.vendorCode}/status`,
|
||||
{ is_active: !this.vendor.is_active }
|
||||
);
|
||||
|
||||
this.vendor = response;
|
||||
Utils.showToast(`Vendor ${action}d successfully`, 'success');
|
||||
Logger.info(`Vendor ${action}d`, 'VENDOR_EDIT');
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to ${action} vendor`, 'VENDOR_EDIT', error);
|
||||
Utils.showToast(`Failed to ${action} vendor`, 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Create Vendor Edit Template
|
||||
|
||||
**File:** `app/templates/admin/vendor-edit.html`
|
||||
|
||||
```jinja2
|
||||
{# app/templates/admin/vendor-edit.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Edit Vendor{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminVendorEdit(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Edit Vendor
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-show="vendor">
|
||||
<span x-text="vendor?.name"></span>
|
||||
<span class="text-gray-400">•</span>
|
||||
<span x-text="vendor?.vendor_code"></span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="/admin/vendors"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
Back to Vendors
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loadingVendor" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading vendor...</p>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div x-show="!loadingVendor && vendor" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex items-center gap-3 mb-6 pb-6 border-b dark:border-gray-700">
|
||||
<button
|
||||
@click="toggleVerification()"
|
||||
:disabled="saving"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
:class="vendor?.is_verified ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'">
|
||||
<span x-html="$icon(vendor?.is_verified ? 'x-circle' : 'badge-check', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="vendor?.is_verified ? 'Unverify Vendor' : 'Verify Vendor'"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="toggleActive()"
|
||||
:disabled="saving"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
:class="vendor?.is_active ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'">
|
||||
<span x-html="$icon(vendor?.is_active ? 'lock-closed' : 'lock-open', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="vendor?.is_active ? 'Deactivate' : 'Activate'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
<!-- Left Column: Basic Info -->
|
||||
<div>
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Basic Information
|
||||
</h3>
|
||||
|
||||
<!-- Vendor Code (readonly) -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Vendor Code
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.vendor_code"
|
||||
disabled
|
||||
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
|
||||
>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
Cannot be changed after creation
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Name -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Vendor Name <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.name"
|
||||
required
|
||||
maxlength="255"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.name }"
|
||||
>
|
||||
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
|
||||
</label>
|
||||
|
||||
<!-- Subdomain -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Subdomain <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.subdomain"
|
||||
@input="formatSubdomain()"
|
||||
required
|
||||
maxlength="100"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.subdomain }"
|
||||
>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
Lowercase letters, numbers, and hyphens only
|
||||
</span>
|
||||
<span x-show="errors.subdomain" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.subdomain"></span>
|
||||
</label>
|
||||
|
||||
<!-- Description -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Description
|
||||
</span>
|
||||
<textarea
|
||||
x-model="formData.description"
|
||||
rows="3"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Contact Info -->
|
||||
<div>
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Contact Information
|
||||
</h3>
|
||||
|
||||
<!-- Owner Email (readonly) -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Owner Email
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
x-model="vendor.owner_email"
|
||||
disabled
|
||||
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
|
||||
>
|
||||
</label>
|
||||
|
||||
<!-- Contact Email -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Contact Email <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
x-model="formData.contact_email"
|
||||
required
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.contact_email }"
|
||||
>
|
||||
<span x-show="errors.contact_email" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.contact_email"></span>
|
||||
</label>
|
||||
|
||||
<!-- Phone -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Phone
|
||||
</span>
|
||||
<input
|
||||
type="tel"
|
||||
x-model="formData.contact_phone"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
>
|
||||
</label>
|
||||
|
||||
<!-- Website -->
|
||||
<label class="block mb-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Website
|
||||
</span>
|
||||
<input
|
||||
type="url"
|
||||
x-model="formData.website"
|
||||
:disabled="saving"
|
||||
placeholder="https://example.com"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Details -->
|
||||
<div class="mb-8">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Business Details
|
||||
</h3>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<!-- Business Address -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Business Address
|
||||
</span>
|
||||
<textarea
|
||||
x-model="formData.business_address"
|
||||
rows="3"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<!-- Tax Number -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Tax Number
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="formData.tax_number"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<a
|
||||
href="/admin/vendors"
|
||||
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
|
||||
Cancel
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="saving"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!saving">Save Changes</span>
|
||||
<span x-show="saving" class="flex items-center">
|
||||
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
Saving...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/vendor-edit.js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
**Checklist Task 2:**
|
||||
- [ ] Update `static/admin/js/vendor-edit.js` with NEW patterns
|
||||
- [ ] Create `app/templates/admin/vendor-edit.html` template
|
||||
- [ ] Add backend route for `/admin/vendors/{vendor_code}/edit`
|
||||
- [ ] Test vendor edit page loads
|
||||
- [ ] Test form submission works
|
||||
- [ ] Test quick action buttons (verify/activate)
|
||||
- [ ] Test validation error display
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create Users Page (MEDIUM PRIORITY) 👥
|
||||
|
||||
**File:** `app/templates/admin/users.html`
|
||||
**File:** `static/admin/js/users.js`
|
||||
|
||||
Create from scratch - same pattern as vendors list page (see previous plan sections for full code)
|
||||
|
||||
---
|
||||
|
||||
## ⏰ Updated Time Estimates
|
||||
|
||||
| Task | Estimated Time | Priority |
|
||||
|------|---------------|----------|
|
||||
| Task 1: Add Vendor List Function | 45-60 min | HIGH |
|
||||
| Task 2: Update Vendor Edit to NEW pattern | 60-75 min | HIGH |
|
||||
| Task 3: Create Users Page | 45-60 min | MEDIUM |
|
||||
| Testing & Verification | 30-45 min | HIGH |
|
||||
| Cleanup | 15 min | LOW |
|
||||
|
||||
**Total: 3-4 hours**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
By end of session:
|
||||
- [ ] Vendor LIST page working with stats and table
|
||||
- [ ] Vendor EDIT page using NEW patterns (ApiClient, Logger)
|
||||
- [ ] Users page created and working
|
||||
- [ ] All pages match dashboard styling
|
||||
- [ ] No console errors
|
||||
- [ ] Old patterns removed from vendor-edit.js
|
||||
|
||||
---
|
||||
|
||||
## 📝 Key Pattern Changes
|
||||
|
||||
### OLD Pattern (vendor-edit.js):
|
||||
```javascript
|
||||
// OLD
|
||||
const vendor = await apiClient.get('/admin/vendors/1');
|
||||
if (!Auth.isAuthenticated()) { ... }
|
||||
Utils.confirm('Message', 'Title');
|
||||
```
|
||||
|
||||
### NEW Pattern (dashboard.js):
|
||||
```javascript
|
||||
// NEW
|
||||
const vendor = await ApiClient.get('/admin/vendors/1');
|
||||
// No Auth checks - backend handles it
|
||||
confirm('Message with details');
|
||||
Logger.info('Action', 'COMPONENT', data);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Let's migrate! 🚀**
|
||||
752
docs/__temp/__PROJECT_ROADMAP/FRONTEND_DOCUMENTATION_PLAN.md
Normal file
752
docs/__temp/__PROJECT_ROADMAP/FRONTEND_DOCUMENTATION_PLAN.md
Normal file
@@ -0,0 +1,752 @@
|
||||
# Frontend Documentation Plan - LetzShop Platform
|
||||
|
||||
## 📚 Documentation Structure Overview
|
||||
|
||||
This documentation plan focuses on **practical guides** for implementing new features in the three main sections of the platform: **Admin**, **Vendor**, and **Shop** (customer-facing).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Documentation Goals
|
||||
|
||||
1. **Enable rapid development** - Team members can implement new pages in 15-30 minutes
|
||||
2. **Ensure consistency** - All pages follow the same Alpine.js architecture patterns
|
||||
3. **Reduce errors** - Common pitfalls are documented and preventable
|
||||
4. **Facilitate onboarding** - New developers can contribute within days, not weeks
|
||||
5. **Maintain quality** - Code quality standards are embedded in the guides
|
||||
|
||||
---
|
||||
|
||||
## 📖 Proposed Documentation Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── frontend/
|
||||
│ ├── index.md # Frontend overview & navigation
|
||||
│ │
|
||||
│ ├── 01-getting-started/
|
||||
│ │ ├── overview.md # Tech stack & architecture summary
|
||||
│ │ ├── setup.md # Local development setup
|
||||
│ │ ├── project-structure.md # File organization
|
||||
│ │ └── naming-conventions.md # Naming standards
|
||||
│ │
|
||||
│ ├── 02-core-concepts/
|
||||
│ │ ├── alpine-architecture.md # Alpine.js patterns we use
|
||||
│ │ ├── template-system.md # Jinja2 templates & inheritance
|
||||
│ │ ├── state-management.md # Reactive state with Alpine.js
|
||||
│ │ ├── api-integration.md # Using apiClient
|
||||
│ │ └── common-pitfalls.md # Variable conflicts & other issues
|
||||
│ │
|
||||
│ ├── 03-implementation-guides/
|
||||
│ │ ├── admin-section/
|
||||
│ │ │ ├── creating-admin-page.md # Step-by-step admin page guide
|
||||
│ │ │ ├── admin-page-template.md # Copy-paste template
|
||||
│ │ │ ├── sidebar-integration.md # Adding menu items
|
||||
│ │ │ └── admin-examples.md # Real examples (dashboard, vendors)
|
||||
│ │ │
|
||||
│ │ ├── vendor-section/
|
||||
│ │ │ ├── creating-vendor-page.md # Step-by-step vendor page guide
|
||||
│ │ │ ├── vendor-page-template.md # Copy-paste template
|
||||
│ │ │ └── vendor-examples.md # Real examples
|
||||
│ │ │
|
||||
│ │ └── shop-section/
|
||||
│ │ ├── creating-shop-page.md # Step-by-step shop page guide
|
||||
│ │ ├── shop-page-template.md # Copy-paste template
|
||||
│ │ └── shop-examples.md # Real examples
|
||||
│ │
|
||||
│ ├── 04-ui-components/
|
||||
│ │ ├── component-library.md # Overview & reference
|
||||
│ │ ├── forms.md # All form components
|
||||
│ │ ├── buttons.md # Button styles
|
||||
│ │ ├── cards.md # Card components
|
||||
│ │ ├── tables.md # Table patterns
|
||||
│ │ ├── modals.md # Modal dialogs
|
||||
│ │ ├── badges.md # Status badges
|
||||
│ │ ├── alerts-toasts.md # Notifications
|
||||
│ │ └── icons.md # Icon usage
|
||||
│ │
|
||||
│ ├── 05-common-patterns/
|
||||
│ │ ├── data-loading.md # Loading states & error handling
|
||||
│ │ ├── pagination.md # Client-side pagination
|
||||
│ │ ├── filtering-sorting.md # Table operations
|
||||
│ │ ├── form-validation.md # Validation patterns
|
||||
│ │ ├── crud-operations.md # Create, Read, Update, Delete
|
||||
│ │ └── real-time-updates.md # WebSocket/polling patterns
|
||||
│ │
|
||||
│ ├── 06-advanced-topics/
|
||||
│ │ ├── performance.md # Optimization techniques
|
||||
│ │ ├── dark-mode.md # Theme implementation
|
||||
│ │ ├── accessibility.md # A11y guidelines
|
||||
│ │ ├── responsive-design.md # Mobile-first approach
|
||||
│ │ └── debugging.md # Debugging techniques
|
||||
│ │
|
||||
│ ├── 07-testing/
|
||||
│ │ ├── testing-overview.md # Testing strategy
|
||||
│ │ ├── testing-hub-guide.md # Using the testing hub
|
||||
│ │ └── manual-testing-checklist.md # QA checklist
|
||||
│ │
|
||||
│ └── 08-reference/
|
||||
│ ├── alpine-js-reference.md # Alpine.js quick reference
|
||||
│ ├── api-client-reference.md # apiClient methods
|
||||
│ ├── utils-reference.md # Utility functions
|
||||
│ ├── css-classes.md # Tailwind classes we use
|
||||
│ └── troubleshooting.md # Common issues & solutions
|
||||
│
|
||||
└── backend/ # Backend docs (later)
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Priority Documentation Order
|
||||
|
||||
### Phase 1: Core Essentials (Week 1)
|
||||
**Goal:** Team can create basic pages immediately
|
||||
|
||||
1. **`01-getting-started/overview.md`**
|
||||
- Quick tech stack summary
|
||||
- Architecture diagram
|
||||
- 5-minute quick start
|
||||
|
||||
2. **`02-core-concepts/alpine-architecture.md`** ⭐ CRITICAL
|
||||
- The `...data()` pattern
|
||||
- `currentPage` identifier
|
||||
- Initialization guards
|
||||
- Variable naming (avoid conflicts!)
|
||||
- Based on FRONTEND_ARCHITECTURE_OVERVIEW.txt
|
||||
|
||||
3. **`03-implementation-guides/admin-section/creating-admin-page.md`** ⭐ CRITICAL
|
||||
- Step-by-step guide
|
||||
- 10-minute implementation time
|
||||
- Includes sidebar integration
|
||||
- Based on ALPINE_PAGE_TEMPLATE.md + COMPLETE_IMPLEMENTATION_GUIDE.md
|
||||
|
||||
4. **`03-implementation-guides/admin-section/admin-page-template.md`** ⭐ CRITICAL
|
||||
- Copy-paste HTML template
|
||||
- Copy-paste JavaScript template
|
||||
- Includes all essential patterns
|
||||
|
||||
### Phase 2: Component Library (Week 2)
|
||||
**Goal:** Team knows all available UI components
|
||||
|
||||
5. **`04-ui-components/component-library.md`**
|
||||
- Live examples
|
||||
- Based on Components page we created
|
||||
|
||||
6. **`04-ui-components/forms.md`**
|
||||
- All form inputs
|
||||
- Validation patterns
|
||||
- Based on UI_COMPONENTS.md
|
||||
|
||||
7. **`04-ui-components/icons.md`**
|
||||
- How to use icons
|
||||
- Category reference
|
||||
- Based on Icons Browser we created
|
||||
|
||||
### Phase 3: Common Patterns (Week 3)
|
||||
**Goal:** Team can implement common features
|
||||
|
||||
8. **`05-common-patterns/pagination.md`**
|
||||
- Based on PAGINATION_DOCUMENTATION.md
|
||||
- Avoid `currentPage` conflict!
|
||||
|
||||
9. **`05-common-patterns/crud-operations.md`**
|
||||
- List, create, edit, delete patterns
|
||||
- Based on vendors.js patterns
|
||||
|
||||
10. **`05-common-patterns/data-loading.md`**
|
||||
- Loading states
|
||||
- Error handling
|
||||
- Based on dashboard.js patterns
|
||||
|
||||
### Phase 4: Other Sections (Week 4)
|
||||
**Goal:** Vendor and Shop sections documented
|
||||
|
||||
11. **`03-implementation-guides/vendor-section/creating-vendor-page.md`**
|
||||
12. **`03-implementation-guides/shop-section/creating-shop-page.md`**
|
||||
|
||||
### Phase 5: Reference & Polish (Week 5)
|
||||
**Goal:** Complete reference documentation
|
||||
|
||||
13. **`02-core-concepts/common-pitfalls.md`** ⭐ IMPORTANT
|
||||
- Variable name conflicts (currentPage!)
|
||||
- Based on VENDORS_SIDEBAR_FIX.md
|
||||
|
||||
14. **`08-reference/troubleshooting.md`**
|
||||
15. All remaining reference docs
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Core Documentation Files (Detailed Specs)
|
||||
|
||||
### 1. `02-core-concepts/alpine-architecture.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# Alpine.js Architecture Pattern
|
||||
|
||||
## Overview
|
||||
Our frontend uses Alpine.js with a specific inheritance pattern...
|
||||
|
||||
## The Base Pattern
|
||||
[Include FRONTEND_ARCHITECTURE_OVERVIEW.txt content]
|
||||
|
||||
## File Structure
|
||||
[Show the correct file locations]
|
||||
|
||||
## Critical Rules
|
||||
1. ALWAYS use `...data()` first
|
||||
2. ALWAYS set `currentPage` identifier
|
||||
3. ALWAYS use lowercase `apiClient`
|
||||
4. ALWAYS include initialization guard
|
||||
5. ALWAYS use unique variable names
|
||||
|
||||
## Common Mistakes
|
||||
[Include vendor currentPage conflict example]
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- FRONTEND_ARCHITECTURE_OVERVIEW.txt
|
||||
- ALPINE_PAGE_TEMPLATE.md
|
||||
- VENDORS_SIDEBAR_FIX.md
|
||||
|
||||
---
|
||||
|
||||
### 2. `03-implementation-guides/admin-section/creating-admin-page.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# Creating a New Admin Page (10-Minute Guide)
|
||||
|
||||
## Prerequisites
|
||||
- [ ] Backend API endpoint exists
|
||||
- [ ] Route added to pages.py
|
||||
- [ ] Sidebar menu item planned
|
||||
|
||||
## Step 1: Create HTML Template (3 minutes)
|
||||
|
||||
Copy this template to `app/templates/admin/your-page.html`:
|
||||
|
||||
[Include complete template from ALPINE_PAGE_TEMPLATE.md]
|
||||
|
||||
## Step 2: Create JavaScript Component (5 minutes)
|
||||
|
||||
Copy this template to `static/admin/js/your-page.js`:
|
||||
|
||||
[Include complete JS template]
|
||||
|
||||
## Step 3: Add Sidebar Menu Item (1 minute)
|
||||
|
||||
[Show exact HTML to add to sidebar]
|
||||
|
||||
## Step 4: Add Route (1 minute)
|
||||
|
||||
[Show exact Python code for pages.py]
|
||||
|
||||
## Step 5: Test (2 minutes)
|
||||
|
||||
Checklist:
|
||||
- [ ] Page loads
|
||||
- [ ] Sidebar shows purple bar
|
||||
- [ ] Data loads from API
|
||||
- [ ] Dark mode works
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue 1: Sidebar not showing purple bar
|
||||
**Cause:** Variable name conflict
|
||||
**Solution:** [Link to common-pitfalls.md]
|
||||
|
||||
### Issue 2: Data not loading
|
||||
**Cause:** API endpoint mismatch
|
||||
**Solution:** Check apiClient.get() URL
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- ALPINE_PAGE_TEMPLATE.md
|
||||
- COMPLETE_IMPLEMENTATION_GUIDE.md
|
||||
- All the architecture documents
|
||||
|
||||
---
|
||||
|
||||
### 3. `05-common-patterns/pagination.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# Client-Side Pagination Pattern
|
||||
|
||||
## Overview
|
||||
Pagination splits data into pages...
|
||||
|
||||
## Quick Start
|
||||
|
||||
[Include PAGINATION_QUICK_START.txt content]
|
||||
|
||||
## Full Implementation
|
||||
|
||||
[Include PAGINATION_DOCUMENTATION.md content]
|
||||
|
||||
## ⚠️ CRITICAL: Avoid Variable Conflicts
|
||||
|
||||
When implementing pagination, DO NOT name your pagination variable `currentPage`
|
||||
if your page uses the sidebar!
|
||||
|
||||
**Wrong:**
|
||||
```javascript
|
||||
currentPage: 'vendors', // For sidebar
|
||||
currentPage: 1, // For pagination - OVERWRITES ABOVE!
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
```javascript
|
||||
currentPage: 'vendors', // For sidebar
|
||||
page: 1, // For pagination - different name!
|
||||
```
|
||||
|
||||
[Link to VENDORS_SIDEBAR_FIX.md for full explanation]
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- PAGINATION_DOCUMENTATION.md
|
||||
- PAGINATION_QUICK_START.txt
|
||||
- VENDORS_SIDEBAR_FIX.md
|
||||
|
||||
---
|
||||
|
||||
### 4. `04-ui-components/component-library.md`
|
||||
|
||||
**Content:**
|
||||
```markdown
|
||||
# UI Component Library
|
||||
|
||||
## Live Reference
|
||||
Visit `/admin/components` to see all components with live examples.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
[Include UI_COMPONENTS_QUICK_REFERENCE.md content]
|
||||
|
||||
## Detailed Components
|
||||
|
||||
[Include UI_COMPONENTS.md content]
|
||||
|
||||
## Copy-Paste Examples
|
||||
|
||||
Each component includes:
|
||||
- Visual example
|
||||
- HTML code
|
||||
- Alpine.js bindings
|
||||
- Dark mode support
|
||||
```
|
||||
|
||||
**Based on:**
|
||||
- UI_COMPONENTS.md
|
||||
- UI_COMPONENTS_QUICK_REFERENCE.md
|
||||
- The Components page we created
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Documentation Features
|
||||
|
||||
### 1. **Code Templates**
|
||||
Every guide includes ready-to-use code templates:
|
||||
- ✅ Complete, working code
|
||||
- ✅ Comments explaining each part
|
||||
- ✅ No placeholders that need changing (except obvious ones like "your-page")
|
||||
|
||||
### 2. **Visual Diagrams**
|
||||
Use ASCII diagrams and flowcharts:
|
||||
```
|
||||
User Request → FastAPI Route → Jinja2 Template → HTML + Alpine.js → Browser
|
||||
```
|
||||
|
||||
### 3. **Before/After Examples**
|
||||
Show incorrect vs correct implementations:
|
||||
|
||||
**❌ Wrong:**
|
||||
```javascript
|
||||
currentPage: 'vendors',
|
||||
currentPage: 1 // Conflict!
|
||||
```
|
||||
|
||||
**✅ Correct:**
|
||||
```javascript
|
||||
currentPage: 'vendors',
|
||||
page: 1 // Different name
|
||||
```
|
||||
|
||||
### 4. **Checklists**
|
||||
Every guide ends with a testing checklist:
|
||||
- [ ] Page loads
|
||||
- [ ] Sidebar active indicator works
|
||||
- [ ] API data loads
|
||||
- [ ] Pagination works
|
||||
- [ ] Dark mode works
|
||||
|
||||
### 5. **Troubleshooting Sections**
|
||||
Common issues with solutions:
|
||||
```
|
||||
Problem: Sidebar indicator not showing
|
||||
Solution: Check for variable name conflicts
|
||||
Reference: docs/frontend/08-reference/troubleshooting.md#sidebar-issues
|
||||
```
|
||||
|
||||
### 6. **Time Estimates**
|
||||
Each task shows expected completion time:
|
||||
- Creating admin page: **10 minutes**
|
||||
- Adding pagination: **5 minutes**
|
||||
- Adding form validation: **15 minutes**
|
||||
|
||||
### 7. **Cross-References**
|
||||
Heavy linking between related topics:
|
||||
```
|
||||
See also:
|
||||
- [Alpine.js Architecture](../02-core-concepts/alpine-architecture.md)
|
||||
- [Common Pitfalls](../02-core-concepts/common-pitfalls.md)
|
||||
- [Pagination Pattern](../05-common-patterns/pagination.md)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Standards
|
||||
|
||||
### Writing Style
|
||||
- **Practical first** - Show code, then explain
|
||||
- **Concise** - Get to the point quickly
|
||||
- **Examples everywhere** - Real code from actual pages
|
||||
- **Searchable** - Good headings, keywords, tags
|
||||
|
||||
### Code Examples
|
||||
```javascript
|
||||
// ✅ Good example - complete and working
|
||||
function adminDashboard() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'dashboard',
|
||||
stats: {},
|
||||
|
||||
async init() {
|
||||
if (window._dashboardInitialized) return;
|
||||
window._dashboardInitialized = true;
|
||||
await this.loadStats();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ❌ Bad example - incomplete, needs work
|
||||
function myPage() {
|
||||
return {
|
||||
// TODO: Add your code here
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Warning Boxes
|
||||
Use admonitions for critical information:
|
||||
|
||||
```markdown
|
||||
!!! warning "Critical: Variable Name Conflicts"
|
||||
Never use `currentPage` for both sidebar identification and pagination!
|
||||
This will cause the sidebar active indicator to break.
|
||||
|
||||
**Solution:** Use `page` or `pageNumber` for pagination.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Implementation
|
||||
|
||||
### MkDocs Configuration Update
|
||||
|
||||
Update `mkdocs.yml` to include frontend section:
|
||||
|
||||
```yaml
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started:
|
||||
- Installation: getting-started/installation.md
|
||||
- Quick Start: getting-started/quickstart.md
|
||||
- Database Setup: getting-started/database-setup.md
|
||||
- Configuration: getting-started/configuration.md
|
||||
|
||||
# NEW: Frontend Documentation
|
||||
- Frontend:
|
||||
- Overview: frontend/index.md
|
||||
- Getting Started:
|
||||
- Overview: frontend/01-getting-started/overview.md
|
||||
- Setup: frontend/01-getting-started/setup.md
|
||||
- Project Structure: frontend/01-getting-started/project-structure.md
|
||||
- Naming Conventions: frontend/01-getting-started/naming-conventions.md
|
||||
- Core Concepts:
|
||||
- Alpine.js Architecture: frontend/02-core-concepts/alpine-architecture.md
|
||||
- Template System: frontend/02-core-concepts/template-system.md
|
||||
- State Management: frontend/02-core-concepts/state-management.md
|
||||
- API Integration: frontend/02-core-concepts/api-integration.md
|
||||
- Common Pitfalls: frontend/02-core-concepts/common-pitfalls.md
|
||||
- Implementation Guides:
|
||||
- Admin Section:
|
||||
- Creating Admin Page: frontend/03-implementation-guides/admin-section/creating-admin-page.md
|
||||
- Admin Page Template: frontend/03-implementation-guides/admin-section/admin-page-template.md
|
||||
- Sidebar Integration: frontend/03-implementation-guides/admin-section/sidebar-integration.md
|
||||
- Examples: frontend/03-implementation-guides/admin-section/admin-examples.md
|
||||
- Vendor Section:
|
||||
- Creating Vendor Page: frontend/03-implementation-guides/vendor-section/creating-vendor-page.md
|
||||
- Vendor Page Template: frontend/03-implementation-guides/vendor-section/vendor-page-template.md
|
||||
- Examples: frontend/03-implementation-guides/vendor-section/vendor-examples.md
|
||||
- Shop Section:
|
||||
- Creating Shop Page: frontend/03-implementation-guides/shop-section/creating-shop-page.md
|
||||
- Shop Page Template: frontend/03-implementation-guides/shop-section/shop-page-template.md
|
||||
- Examples: frontend/03-implementation-guides/shop-section/shop-examples.md
|
||||
- UI Components:
|
||||
- Component Library: frontend/04-ui-components/component-library.md
|
||||
- Forms: frontend/04-ui-components/forms.md
|
||||
- Buttons: frontend/04-ui-components/buttons.md
|
||||
- Cards: frontend/04-ui-components/cards.md
|
||||
- Tables: frontend/04-ui-components/tables.md
|
||||
- Modals: frontend/04-ui-components/modals.md
|
||||
- Badges: frontend/04-ui-components/badges.md
|
||||
- Alerts & Toasts: frontend/04-ui-components/alerts-toasts.md
|
||||
- Icons: frontend/04-ui-components/icons.md
|
||||
- Common Patterns:
|
||||
- Data Loading: frontend/05-common-patterns/data-loading.md
|
||||
- Pagination: frontend/05-common-patterns/pagination.md
|
||||
- Filtering & Sorting: frontend/05-common-patterns/filtering-sorting.md
|
||||
- Form Validation: frontend/05-common-patterns/form-validation.md
|
||||
- CRUD Operations: frontend/05-common-patterns/crud-operations.md
|
||||
- Real-time Updates: frontend/05-common-patterns/real-time-updates.md
|
||||
- Advanced Topics:
|
||||
- Performance: frontend/06-advanced-topics/performance.md
|
||||
- Dark Mode: frontend/06-advanced-topics/dark-mode.md
|
||||
- Accessibility: frontend/06-advanced-topics/accessibility.md
|
||||
- Responsive Design: frontend/06-advanced-topics/responsive-design.md
|
||||
- Debugging: frontend/06-advanced-topics/debugging.md
|
||||
- Testing:
|
||||
- Testing Overview: frontend/07-testing/testing-overview.md
|
||||
- Testing Hub Guide: frontend/07-testing/testing-hub-guide.md
|
||||
- Manual Testing: frontend/07-testing/manual-testing-checklist.md
|
||||
- Reference:
|
||||
- Alpine.js Quick Reference: frontend/08-reference/alpine-js-reference.md
|
||||
- API Client Reference: frontend/08-reference/api-client-reference.md
|
||||
- Utils Reference: frontend/08-reference/utils-reference.md
|
||||
- CSS Classes: frontend/08-reference/css-classes.md
|
||||
- Troubleshooting: frontend/08-reference/troubleshooting.md
|
||||
|
||||
# Existing sections
|
||||
- API:
|
||||
- Overview: api/index.md
|
||||
# ... rest of API docs
|
||||
|
||||
- Testing:
|
||||
- Testing Guide: testing/testing-guide.md
|
||||
- Test Maintenance: testing/test-maintenance.md
|
||||
|
||||
# Backend docs come later
|
||||
# - Backend:
|
||||
# - Architecture: backend/architecture.md
|
||||
# - ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Metrics
|
||||
|
||||
### Team Adoption
|
||||
- [ ] 100% of team can create a basic admin page in <15 minutes
|
||||
- [ ] 90% of new pages follow architecture correctly on first try
|
||||
- [ ] <5% of PRs have sidebar indicator issues
|
||||
- [ ] <10% of PRs have variable naming conflicts
|
||||
|
||||
### Documentation Quality
|
||||
- [ ] Every guide has working code examples
|
||||
- [ ] Every guide has a testing checklist
|
||||
- [ ] Every guide links to related topics
|
||||
- [ ] Every guide has time estimates
|
||||
|
||||
### Onboarding Speed
|
||||
- [ ] New developers can create first page within 1 day
|
||||
- [ ] New developers can work independently within 3 days
|
||||
- [ ] Reduced onboarding questions by 80%
|
||||
|
||||
---
|
||||
|
||||
## 📅 Implementation Timeline
|
||||
|
||||
### Week 1: Core Essentials
|
||||
- Write Alpine.js Architecture guide
|
||||
- Write Creating Admin Page guide
|
||||
- Create Admin Page Template
|
||||
- Write Common Pitfalls guide
|
||||
|
||||
### Week 2: Components & Patterns
|
||||
- Document Component Library
|
||||
- Document Forms, Buttons, Cards
|
||||
- Document Pagination pattern
|
||||
- Document CRUD operations
|
||||
|
||||
### Week 3: Reference & Vendor
|
||||
- Complete Reference section
|
||||
- Write Vendor section guides
|
||||
- Create Vendor templates
|
||||
|
||||
### Week 4: Shop & Polish
|
||||
- Write Shop section guides
|
||||
- Create Shop templates
|
||||
- Review and polish all docs
|
||||
- Add missing cross-references
|
||||
|
||||
### Week 5: Testing & Launch
|
||||
- Internal review with team
|
||||
- Fix any issues found
|
||||
- Launch documentation
|
||||
- Gather feedback
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Maintenance Plan
|
||||
|
||||
### Regular Updates
|
||||
- **Weekly:** Check for new common issues
|
||||
- **Monthly:** Review and update examples
|
||||
- **Quarterly:** Major documentation review
|
||||
|
||||
### Version Control
|
||||
- All documentation in Git
|
||||
- Changes reviewed like code
|
||||
- Version numbers for major updates
|
||||
|
||||
### Feedback Loop
|
||||
- Add "Was this helpful?" to each page
|
||||
- Collect common questions
|
||||
- Update docs based on feedback
|
||||
|
||||
---
|
||||
|
||||
## 📊 Documentation Metrics
|
||||
|
||||
Track these metrics:
|
||||
1. **Page views** - Which docs are most used?
|
||||
2. **Search terms** - What are people looking for?
|
||||
3. **Time on page** - Are docs too long/short?
|
||||
4. **Bounce rate** - Are people finding what they need?
|
||||
5. **Questions in Slack** - Are docs answering questions?
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Path for New Developers
|
||||
|
||||
### Day 1: Foundations
|
||||
1. Read: Overview
|
||||
2. Read: Alpine.js Architecture
|
||||
3. Read: Creating Admin Page guide
|
||||
4. Exercise: Create a simple "Hello World" admin page
|
||||
|
||||
### Day 2: Practice
|
||||
1. Read: Component Library
|
||||
2. Read: Data Loading pattern
|
||||
3. Exercise: Create admin page that loads data from API
|
||||
|
||||
### Day 3: Patterns
|
||||
1. Read: Pagination pattern
|
||||
2. Read: CRUD operations
|
||||
3. Exercise: Create full CRUD page with pagination
|
||||
|
||||
### Day 4: Real Work
|
||||
1. Read: Common Pitfalls
|
||||
2. Read: Troubleshooting
|
||||
3. Exercise: Implement first real feature
|
||||
|
||||
### Day 5: Independence
|
||||
- Work on real tickets independently
|
||||
- Refer to docs as needed
|
||||
- Ask questions when stuck
|
||||
|
||||
---
|
||||
|
||||
## 📝 Documentation Templates
|
||||
|
||||
### Guide Template
|
||||
```markdown
|
||||
# [Feature/Pattern Name]
|
||||
|
||||
## Overview
|
||||
Brief 2-3 sentence description
|
||||
|
||||
## Prerequisites
|
||||
- [ ] Requirement 1
|
||||
- [ ] Requirement 2
|
||||
|
||||
## Quick Start (5 minutes)
|
||||
Fastest path to working code
|
||||
|
||||
## Step-by-Step Guide
|
||||
Detailed instructions
|
||||
|
||||
## Common Issues
|
||||
Problems and solutions
|
||||
|
||||
## Testing Checklist
|
||||
- [ ] Test 1
|
||||
- [ ] Test 2
|
||||
|
||||
## See Also
|
||||
- [Related doc 1](link)
|
||||
- [Related doc 2](link)
|
||||
```
|
||||
|
||||
### Reference Template
|
||||
```markdown
|
||||
# [API/Component Name] Reference
|
||||
|
||||
## Overview
|
||||
What it does
|
||||
|
||||
## Usage
|
||||
Basic usage example
|
||||
|
||||
## API
|
||||
Full API documentation
|
||||
|
||||
## Examples
|
||||
Multiple real-world examples
|
||||
|
||||
## See Also
|
||||
Related references
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Ready to Implement
|
||||
|
||||
This documentation plan provides:
|
||||
|
||||
1. **Clear structure** - Organized by role and task
|
||||
2. **Practical focus** - Implementation guides, not theory
|
||||
3. **Real examples** - Based on actual working code
|
||||
4. **Team-oriented** - Designed for collaboration
|
||||
5. **Maintainable** - Easy to update and extend
|
||||
|
||||
**Next Steps:**
|
||||
1. Review and approve this plan
|
||||
2. Start with Phase 1 (Core Essentials)
|
||||
3. Write docs one at a time
|
||||
4. Get team feedback early
|
||||
5. Iterate and improve
|
||||
|
||||
**Estimated effort:**
|
||||
- 5 weeks for initial documentation
|
||||
- 2-4 hours per week for maintenance
|
||||
- Massive time savings for team (100+ hours/year)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Benefits
|
||||
|
||||
Once complete, your team will have:
|
||||
|
||||
✅ **Faster development** - 10-15 minute page creation
|
||||
✅ **Fewer bugs** - Common mistakes documented
|
||||
✅ **Better code quality** - Patterns enforced through docs
|
||||
✅ **Easier onboarding** - New devs productive in days
|
||||
✅ **Reduced questions** - Self-service documentation
|
||||
✅ **Scalable knowledge** - Team expertise captured
|
||||
|
||||
**ROI:** Pays for itself after 2-3 features implemented! 📈
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
# Work Plan - October 22, 2025
|
||||
## Jinja2 Migration: Polish & Complete Admin Panel
|
||||
|
||||
**Current Status:** Core migration complete ✅ | Auth loop fixed ✅ | Minor issues remaining ⚠️
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Today's Goals
|
||||
|
||||
1. ✅ Fix icon system and utils.js conflicts
|
||||
2. ✅ Test and verify logout flow
|
||||
3. ✅ Test all admin pages (vendors, users)
|
||||
4. ✅ Create remaining templates
|
||||
5. ✅ Clean up and remove old code
|
||||
|
||||
**Estimated Time:** 3-4 hours
|
||||
|
||||
---
|
||||
|
||||
## 📋 Task List
|
||||
|
||||
### Priority 1: Fix Icon/Utils Conflicts (HIGH) ⚠️
|
||||
|
||||
**Issue Reported:**
|
||||
> "Share some outputs about $icons issues and utils already declared"
|
||||
|
||||
#### Task 1.1: Investigate Icon Issues
|
||||
- [ ] Check browser console for icon-related errors
|
||||
- [ ] Verify `icons.js` is loaded only once
|
||||
- [ ] Check for duplicate `window.icon` declarations
|
||||
- [ ] Test icon rendering in all templates
|
||||
|
||||
**Files to Check:**
|
||||
- `static/shared/js/icons.js`
|
||||
- `app/templates/admin/base.html` (script order)
|
||||
- `app/templates/admin/login.html` (script order)
|
||||
|
||||
**Expected Issues:**
|
||||
```javascript
|
||||
// Possible duplicate declaration
|
||||
Uncaught SyntaxError: Identifier 'icon' has already been declared
|
||||
// or
|
||||
Warning: window.icon is already defined
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
- Ensure `icons.js` loaded only once per page
|
||||
- Remove any duplicate `icon()` function declarations
|
||||
- Verify Alpine magic helper `$icon()` is registered correctly
|
||||
|
||||
#### Task 1.2: Investigate Utils Issues
|
||||
- [ ] Check for duplicate `Utils` object declarations
|
||||
- [ ] Verify `utils.js` loaded only once
|
||||
- [ ] Test all utility functions (formatDate, showToast, etc.)
|
||||
|
||||
**Files to Check:**
|
||||
- `static/shared/js/utils.js`
|
||||
- `static/shared/js/api-client.js` (Utils defined here too?)
|
||||
|
||||
**Potential Fix:**
|
||||
```javascript
|
||||
// Option 1: Use namespace to avoid conflicts
|
||||
if (typeof window.Utils === 'undefined') {
|
||||
window.Utils = { /* ... */ };
|
||||
}
|
||||
|
||||
// Option 2: Remove duplicate definitions
|
||||
// Keep Utils only in one place (either utils.js OR api-client.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Priority 2: Test Logout Flow (HIGH) 🔐
|
||||
|
||||
#### Task 2.1: Test Logout Button
|
||||
- [ ] Click logout in header
|
||||
- [ ] Verify cookie is deleted
|
||||
- [ ] Verify localStorage is cleared
|
||||
- [ ] Verify redirect to login page
|
||||
- [ ] Verify cannot access dashboard after logout
|
||||
|
||||
**Test Script:**
|
||||
```javascript
|
||||
// Before logout
|
||||
console.log('Cookie:', document.cookie);
|
||||
console.log('localStorage:', localStorage.getItem('admin_token'));
|
||||
|
||||
// Click logout
|
||||
|
||||
// After logout (should be empty)
|
||||
console.log('Cookie:', document.cookie); // Should not contain admin_token
|
||||
console.log('localStorage:', localStorage.getItem('admin_token')); // Should be null
|
||||
```
|
||||
|
||||
#### Task 2.2: Update Logout Endpoint (if needed)
|
||||
**File:** `app/api/v1/admin/auth.py`
|
||||
|
||||
Already implemented, just verify:
|
||||
```python
|
||||
@router.post("/logout")
|
||||
def admin_logout(response: Response):
|
||||
# Clears the cookie
|
||||
response.delete_cookie(key="admin_token", path="/")
|
||||
return {"message": "Logged out successfully"}
|
||||
```
|
||||
|
||||
#### Task 2.3: Update Header Logout Button
|
||||
**File:** `app/templates/partials/header.html`
|
||||
|
||||
Verify logout button calls the correct endpoint:
|
||||
```html
|
||||
<button @click="handleLogout()">
|
||||
Logout
|
||||
</button>
|
||||
|
||||
<script>
|
||||
function handleLogout() {
|
||||
// Call logout API
|
||||
fetch('/api/v1/admin/auth/logout', { method: 'POST' })
|
||||
.then(() => {
|
||||
// Clear localStorage
|
||||
localStorage.clear();
|
||||
// Redirect
|
||||
window.location.href = '/admin/login';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Priority 3: Test All Admin Pages (MEDIUM) 📄
|
||||
|
||||
#### Task 3.1: Test Vendors Page
|
||||
- [ ] Navigate to `/admin/vendors`
|
||||
- [ ] Verify page loads with authentication
|
||||
- [ ] Check if template exists or needs creation
|
||||
- [ ] Test vendor list display
|
||||
- [ ] Test vendor creation button
|
||||
|
||||
**If template missing:**
|
||||
Create `app/templates/admin/vendors.html`
|
||||
|
||||
#### Task 3.2: Test Users Page
|
||||
- [ ] Navigate to `/admin/users`
|
||||
- [ ] Verify page loads with authentication
|
||||
- [ ] Check if template exists or needs creation
|
||||
- [ ] Test user list display
|
||||
|
||||
**If template missing:**
|
||||
Create `app/templates/admin/users.html`
|
||||
|
||||
#### Task 3.3: Test Navigation
|
||||
- [ ] Click all sidebar links
|
||||
- [ ] Verify no 404 errors
|
||||
- [ ] Verify active state highlights correctly
|
||||
- [ ] Test breadcrumbs (if applicable)
|
||||
|
||||
---
|
||||
|
||||
### Priority 4: Create Missing Templates (MEDIUM) 📝
|
||||
|
||||
#### Task 4.1: Create Vendors Template
|
||||
**File:** `app/templates/admin/vendors.html`
|
||||
|
||||
```jinja2
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Vendors Management{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminVendors(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Vendors Management
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Vendor list content -->
|
||||
<div x-data="adminVendors()">
|
||||
<!-- Your existing vendors.html content here -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/vendors.js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
#### Task 4.2: Create Users Template
|
||||
**File:** `app/templates/admin/users.html`
|
||||
|
||||
Similar structure to vendors template.
|
||||
|
||||
#### Task 4.3: Verify Vendor Edit Page
|
||||
Check if vendor-edit needs a template or if it's a modal/overlay.
|
||||
|
||||
---
|
||||
|
||||
### Priority 5: Cleanup (LOW) 🧹
|
||||
|
||||
#### Task 5.1: Remove Old Static HTML Files
|
||||
- [ ] Delete `static/admin/dashboard.html` (if exists)
|
||||
- [ ] Delete `static/admin/vendors.html` (if exists)
|
||||
- [ ] Delete `static/admin/users.html` (if exists)
|
||||
- [ ] Delete `static/admin/partials/` directory
|
||||
|
||||
**Before deleting:** Backup files just in case!
|
||||
|
||||
#### Task 5.2: Remove Partial Loader
|
||||
- [ ] Delete `static/shared/js/partial-loader.js`
|
||||
- [ ] Remove any references to `partialLoader` in code
|
||||
- [ ] Search codebase: `grep -r "partial-loader" .`
|
||||
|
||||
#### Task 5.3: Clean Up frontend.py
|
||||
**File:** `app/routes/frontend.py`
|
||||
|
||||
- [ ] Remove commented-out admin routes
|
||||
- [ ] Or delete file entirely if only contained admin routes
|
||||
- [ ] Update imports if needed
|
||||
|
||||
#### Task 5.4: Production Mode Preparation
|
||||
- [ ] Set log levels to production (INFO or WARN)
|
||||
- [ ] Update cookie `secure=True` for production
|
||||
- [ ] Remove debug console.logs
|
||||
- [ ] Test with production settings
|
||||
|
||||
**Update log levels:**
|
||||
```javascript
|
||||
// static/admin/js/log-config.js
|
||||
GLOBAL_LEVEL: isDevelopment ? 4 : 2, // Debug in dev, Warnings in prod
|
||||
LOGIN: isDevelopment ? 4 : 1, // Full debug in dev, errors only in prod
|
||||
API_CLIENT: isDevelopment ? 3 : 1, // Info in dev, errors only in prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### Comprehensive Testing
|
||||
- [ ] Fresh login (clear all data first)
|
||||
- [ ] Dashboard loads correctly
|
||||
- [ ] Stats cards display data
|
||||
- [ ] Recent vendors table works
|
||||
- [ ] Sidebar navigation works
|
||||
- [ ] Dark mode toggle works
|
||||
- [ ] Logout clears auth and redirects
|
||||
- [ ] Cannot access dashboard after logout
|
||||
- [ ] Vendors page loads
|
||||
- [ ] Users page loads
|
||||
- [ ] No console errors
|
||||
- [ ] No 404 errors in Network tab
|
||||
- [ ] Icons display correctly
|
||||
- [ ] All Alpine.js components work
|
||||
|
||||
### Browser Testing
|
||||
- [ ] Chrome/Edge
|
||||
- [ ] Firefox
|
||||
- [ ] Safari (if available)
|
||||
- [ ] Mobile view (responsive)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Debugging Guide
|
||||
|
||||
### If Icons Don't Display:
|
||||
```javascript
|
||||
// Check in console:
|
||||
console.log('window.icon:', typeof window.icon);
|
||||
console.log('window.Icons:', typeof window.Icons);
|
||||
console.log('$icon available:', typeof Alpine !== 'undefined' && Alpine.magic('icon'));
|
||||
|
||||
// Test manually:
|
||||
document.body.innerHTML += window.icon('home', 'w-6 h-6');
|
||||
```
|
||||
|
||||
### If Utils Undefined:
|
||||
```javascript
|
||||
// Check in console:
|
||||
console.log('Utils:', typeof Utils);
|
||||
console.log('Utils methods:', Object.keys(Utils || {}));
|
||||
|
||||
// Test manually:
|
||||
Utils.showToast('Test message', 'info');
|
||||
```
|
||||
|
||||
### If Auth Fails:
|
||||
```javascript
|
||||
// Check storage:
|
||||
console.log('localStorage token:', localStorage.getItem('admin_token'));
|
||||
console.log('Cookie:', document.cookie);
|
||||
|
||||
// Test API manually:
|
||||
fetch('/api/v1/admin/auth/me', {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('admin_token')}` }
|
||||
}).then(r => r.json()).then(console.log);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Documentation Tasks
|
||||
|
||||
### Update Documentation
|
||||
- [ ] Update project README with new architecture
|
||||
- [ ] Document authentication flow (cookies + localStorage)
|
||||
- [ ] Document template structure
|
||||
- [ ] Add deployment notes (dev vs production)
|
||||
- [ ] Update API documentation if needed
|
||||
|
||||
### Code Comments
|
||||
- [ ] Add comments to complex authentication code
|
||||
- [ ] Document cookie settings and rationale
|
||||
- [ ] Explain dual token storage pattern
|
||||
- [ ] Add JSDoc comments to JavaScript functions
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Phase Preview (After Today)
|
||||
|
||||
### Vendor Portal Migration
|
||||
1. Apply same Jinja2 pattern to vendor routes
|
||||
2. Create vendor templates (login, dashboard, etc.)
|
||||
3. Implement vendor authentication (separate cookie: `vendor_token`)
|
||||
4. Test vendor flows
|
||||
|
||||
### Customer/Shop Migration
|
||||
1. Customer authentication system
|
||||
2. Shop templates
|
||||
3. Shopping cart (consider cookie vs localStorage)
|
||||
4. "Remember Me" implementation
|
||||
|
||||
### Advanced Features
|
||||
1. "Remember Me" checkbox (30-day cookies)
|
||||
2. Session management
|
||||
3. Multiple device logout
|
||||
4. Security enhancements (CSRF tokens)
|
||||
|
||||
---
|
||||
|
||||
## ⏰ Time Estimates
|
||||
|
||||
| Task | Estimated Time | Priority |
|
||||
|------|---------------|----------|
|
||||
| Fix icon/utils issues | 30-45 min | HIGH |
|
||||
| Test logout flow | 15-30 min | HIGH |
|
||||
| Test admin pages | 30 min | MEDIUM |
|
||||
| Create missing templates | 45-60 min | MEDIUM |
|
||||
| Cleanup old code | 30 min | LOW |
|
||||
| Testing & verification | 30-45 min | HIGH |
|
||||
| Documentation | 30 min | LOW |
|
||||
|
||||
**Total: 3-4 hours**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria for Today
|
||||
|
||||
By end of day, we should have:
|
||||
- [ ] All icons displaying correctly
|
||||
- [ ] No JavaScript errors in console
|
||||
- [ ] Logout flow working perfectly
|
||||
- [ ] All admin pages accessible and working
|
||||
- [ ] Templates for vendors and users pages
|
||||
- [ ] Old code cleaned up
|
||||
- [ ] Comprehensive testing completed
|
||||
- [ ] Documentation updated
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Stretch Goals (If Time Permits)
|
||||
|
||||
1. Add loading states to all buttons
|
||||
2. Improve error messages (user-friendly)
|
||||
3. Add success/error toasts to all operations
|
||||
4. Implement "Remember Me" checkbox
|
||||
5. Start vendor portal migration
|
||||
6. Add unit tests for authentication
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support Resources
|
||||
|
||||
### If Stuck:
|
||||
- Review yesterday's complete file implementations
|
||||
- Check browser console for detailed logs (log level 4)
|
||||
- Use test-auth-flow.html for systematic testing
|
||||
- Check Network tab for HTTP requests/responses
|
||||
|
||||
### Reference Files:
|
||||
- `static/admin/test-auth-flow.html` - Testing interface
|
||||
- `TESTING_CHECKLIST.md` - Systematic testing guide
|
||||
- Yesterday's complete file updates (in conversation)
|
||||
|
||||
---
|
||||
|
||||
**Good luck with today's tasks! 🚀**
|
||||
|
||||
Remember: Take breaks, test
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,520 @@
|
||||
# Jinja2 Migration Progress - Admin Panel
|
||||
|
||||
**Date:** October 20, 2025
|
||||
**Project:** Multi-Tenant E-commerce Platform
|
||||
**Goal:** Migrate from static HTML files to Jinja2 server-rendered templates
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Current Status: DEBUGGING AUTH LOOP
|
||||
|
||||
We successfully set up the Jinja2 infrastructure but are experiencing authentication redirect loops. We're in the process of simplifying the auth flow to resolve this.
|
||||
|
||||
---
|
||||
|
||||
## ✅ What's Been Completed
|
||||
|
||||
### 1. Infrastructure Setup ✅
|
||||
|
||||
- [x] Added Jinja2Templates to `main.py`
|
||||
- [x] Created `app/templates/` directory structure
|
||||
- [x] Created `app/api/v1/admin/pages.py` for HTML routes
|
||||
- [x] Integrated pages router into the main app
|
||||
|
||||
**Files Created:**
|
||||
```
|
||||
app/
|
||||
├── templates/
|
||||
│ ├── admin/
|
||||
│ │ ├── base.html ✅ Created
|
||||
│ │ ├── login.html ✅ Created
|
||||
│ │ └── dashboard.html ✅ Created
|
||||
│ └── partials/
|
||||
│ ├── header.html ✅ Moved from static
|
||||
│ └── sidebar.html ✅ Moved from static
|
||||
└── api/
|
||||
└── v1/
|
||||
└── admin/
|
||||
└── pages.py ✅ Created
|
||||
```
|
||||
|
||||
### 2. Route Configuration ✅
|
||||
|
||||
**New Jinja2 Routes (working):**
|
||||
- `/admin/` → redirects to `/admin/dashboard`
|
||||
- `/admin/login` → login page (no auth)
|
||||
- `/admin/dashboard` → dashboard page (requires auth)
|
||||
- `/admin/vendors` → vendors page (requires auth)
|
||||
- `/admin/users` → users page (requires auth)
|
||||
|
||||
**Old Static Routes (disabled):**
|
||||
- Commented out admin routes in `app/routes/frontend.py`
|
||||
- Old `/static/admin/*.html` routes no longer active
|
||||
|
||||
### 3. Exception Handler Updates ✅
|
||||
|
||||
- [x] Updated `app/exceptions/handler.py` to redirect HTML requests on 401
|
||||
- [x] Added `_is_html_page_request()` helper function
|
||||
- [x] Server-side redirects working for unauthenticated page access
|
||||
|
||||
### 4. JavaScript Updates ✅
|
||||
|
||||
Updated all JavaScript files to use new routes:
|
||||
|
||||
**Files Updated:**
|
||||
- `static/admin/js/dashboard.js` - viewVendor() uses `/admin/vendors`
|
||||
- `static/admin/js/login.js` - redirects to `/admin/dashboard`
|
||||
- `static/admin/js/vendors.js` - auth checks use `/admin/login`
|
||||
- `static/admin/js/vendor-edit.js` - all redirects updated
|
||||
- `static/shared/js/api-client.js` - handleUnauthorized() uses `/admin/login`
|
||||
|
||||
### 5. Template Structure ✅
|
||||
|
||||
**Base Template (`app/templates/admin/base.html`):**
|
||||
- Server-side includes for header and sidebar (no more AJAX loading!)
|
||||
- Proper script loading order
|
||||
- Alpine.js integration
|
||||
- No more `partial-loader.js`
|
||||
|
||||
**Dashboard Template (`app/templates/admin/dashboard.html`):**
|
||||
- Extends base template
|
||||
- Uses Alpine.js `adminDashboard()` component
|
||||
- Stats cards and recent vendors table
|
||||
|
||||
**Login Template (`app/templates/admin/login.html`):**
|
||||
- Standalone page (doesn't extend base)
|
||||
- Uses Alpine.js `adminLogin()` component
|
||||
|
||||
---
|
||||
|
||||
## ❌ Current Problem: Authentication Loop
|
||||
|
||||
### Issue Description
|
||||
|
||||
Getting infinite redirect loops in various scenarios:
|
||||
1. After login → redirects back to login
|
||||
2. On login page → continuous API calls to `/admin/auth/me`
|
||||
3. Dashboard → redirects to login → redirects to dashboard
|
||||
|
||||
### Root Causes Identified
|
||||
|
||||
1. **Multiple redirect handlers fighting:**
|
||||
- Server-side: `handler.py` redirects on 401 for HTML pages
|
||||
- Client-side: `api-client.js` also redirects on 401
|
||||
- Both triggering simultaneously
|
||||
|
||||
2. **Login page checking auth on init:**
|
||||
- Calls `/admin/auth/me` on page load
|
||||
- Gets 401 → triggers redirect
|
||||
- Creates loop
|
||||
|
||||
3. **Token not being sent properly:**
|
||||
- Token stored but API calls not including it
|
||||
- Gets 401 even with valid token
|
||||
|
||||
### Latest Approach (In Progress)
|
||||
|
||||
Simplifying to minimal working version:
|
||||
- Login page does NOTHING on init (no auth checking)
|
||||
- API client does NOT redirect (just throws errors)
|
||||
- Server ONLY redirects browser HTML requests (not API calls)
|
||||
- One source of truth for auth handling
|
||||
|
||||
---
|
||||
|
||||
## 📝 Files Modified (Complete List)
|
||||
|
||||
### Backend Files
|
||||
|
||||
1. **`main.py`**
|
||||
```python
|
||||
# Added:
|
||||
- Jinja2Templates import and configuration
|
||||
- admin_pages router include at /admin prefix
|
||||
```
|
||||
|
||||
2. **`app/api/main.py`** (unchanged - just includes v1 routes)
|
||||
|
||||
3. **`app/api/v1/admin/__init__.py`**
|
||||
```python
|
||||
# Added:
|
||||
- import pages
|
||||
- router.include_router(pages.router, tags=["admin-pages"])
|
||||
```
|
||||
|
||||
4. **`app/api/v1/admin/pages.py`** (NEW FILE)
|
||||
```python
|
||||
# Contains:
|
||||
- @router.get("/") - root redirect
|
||||
- @router.get("/login") - login page
|
||||
- @router.get("/dashboard") - dashboard page
|
||||
- @router.get("/vendors") - vendors page
|
||||
- @router.get("/users") - users page
|
||||
```
|
||||
|
||||
5. **`app/routes/frontend.py`**
|
||||
```python
|
||||
# Changed:
|
||||
- Commented out all /admin/ routes
|
||||
- Left vendor and shop routes active
|
||||
```
|
||||
|
||||
6. **`app/exceptions/handler.py`**
|
||||
```python
|
||||
# Added:
|
||||
- 401 redirect logic for HTML pages
|
||||
- _is_html_page_request() helper
|
||||
# Status: Needs simplification
|
||||
```
|
||||
|
||||
### Frontend Files
|
||||
|
||||
1. **`static/admin/js/login.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- Removed /static/admin/ paths
|
||||
- Updated to /admin/ paths
|
||||
- checkExistingAuth() logic
|
||||
# Status: Needs simplification
|
||||
```
|
||||
|
||||
2. **`static/admin/js/dashboard.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- viewVendor() uses /admin/vendors
|
||||
# Status: Working
|
||||
```
|
||||
|
||||
3. **`static/admin/js/vendors.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- checkAuth() redirects to /admin/login
|
||||
- handleLogout() redirects to /admin/login
|
||||
# Status: Not tested yet
|
||||
```
|
||||
|
||||
4. **`static/admin/js/vendor-edit.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- All /static/admin/ paths to /admin/
|
||||
# Status: Not tested yet
|
||||
```
|
||||
|
||||
5. **`static/shared/js/api-client.js`**
|
||||
```javascript
|
||||
// Changed:
|
||||
- handleUnauthorized() uses /admin/login
|
||||
# Status: Needs simplification - causing loops
|
||||
```
|
||||
|
||||
6. **`static/shared/js/utils.js`** (unchanged - working fine)
|
||||
|
||||
### Template Files (NEW)
|
||||
|
||||
1. **`app/templates/admin/base.html`** ✅
|
||||
- Master layout with sidebar and header
|
||||
- Script loading in correct order
|
||||
- No partial-loader.js
|
||||
|
||||
2. **`app/templates/admin/login.html`** ✅
|
||||
- Standalone login page
|
||||
- Alpine.js adminLogin() component
|
||||
|
||||
3. **`app/templates/admin/dashboard.html`** ✅
|
||||
- Extends base.html
|
||||
- Alpine.js adminDashboard() component
|
||||
|
||||
4. **`app/templates/partials/header.html`** ✅
|
||||
- Top navigation bar
|
||||
- Updated logout link to /admin/login
|
||||
|
||||
5. **`app/templates/partials/sidebar.html`** ✅
|
||||
- Side navigation menu
|
||||
- Updated all links to /admin/* paths
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Next Steps (Tomorrow)
|
||||
|
||||
### Immediate Priority: Fix Auth Loop
|
||||
|
||||
Apply the simplified approach from the last message:
|
||||
|
||||
1. **Simplify `login.js`:**
|
||||
```javascript
|
||||
// Remove all auth checking on init
|
||||
// Just show login form
|
||||
// Only redirect after successful login
|
||||
```
|
||||
|
||||
2. **Simplify `api-client.js`:**
|
||||
```javascript
|
||||
// Remove handleUnauthorized() redirect logic
|
||||
// Just throw errors, don't redirect
|
||||
// Let server handle redirects
|
||||
```
|
||||
|
||||
3. **Simplify `handler.py`:**
|
||||
```javascript
|
||||
// Only redirect browser HTML requests (text/html accept header)
|
||||
// Don't redirect API calls (application/json)
|
||||
// Don't redirect if already on login page
|
||||
```
|
||||
|
||||
**Test Flow:**
|
||||
1. Navigate to `/admin/login` → should show form (no loops)
|
||||
2. Login → should redirect to `/admin/dashboard`
|
||||
3. Dashboard → should load with sidebar/header
|
||||
4. No console errors, no 404s for partials
|
||||
|
||||
### After Auth Works
|
||||
|
||||
1. **Create remaining page templates:**
|
||||
- `app/templates/admin/vendors.html`
|
||||
- `app/templates/admin/users.html`
|
||||
- `app/templates/admin/vendor-edit.html`
|
||||
|
||||
2. **Test all admin flows:**
|
||||
- Login ✓
|
||||
- Dashboard ✓
|
||||
- Vendors list
|
||||
- Vendor create
|
||||
- Vendor edit
|
||||
- User management
|
||||
|
||||
3. **Cleanup:**
|
||||
- Remove old static HTML files
|
||||
- Remove `app/routes/frontend.py` admin routes completely
|
||||
- Remove `partial-loader.js`
|
||||
|
||||
4. **Migrate vendor portal:**
|
||||
- Same process for `/vendor/*` routes
|
||||
- Create vendor templates
|
||||
- Update vendor JavaScript files
|
||||
|
||||
---
|
||||
|
||||
## 📚 Key Learnings
|
||||
|
||||
### What Worked
|
||||
|
||||
1. ✅ **Server-side template rendering** - Clean, fast, no AJAX for partials
|
||||
2. ✅ **Jinja2 integration** - Easy to set up, works with FastAPI
|
||||
3. ✅ **Route separation** - HTML routes in `pages.py`, API routes separate
|
||||
4. ✅ **Template inheritance** - `base.html` + `{% extends %}` pattern
|
||||
|
||||
### What Caused Issues
|
||||
|
||||
1. ❌ **Multiple redirect handlers** - Client + server both handling 401
|
||||
2. ❌ **Auth checking on login page** - Created loops
|
||||
3. ❌ **Complex error handling** - Too many places making decisions
|
||||
4. ❌ **Path inconsistencies** - Old `/static/admin/` vs new `/admin/`
|
||||
|
||||
### Best Practices Identified
|
||||
|
||||
1. **Single source of truth for redirects** - Choose server OR client, not both
|
||||
2. **Login page should be dumb** - No auth checking, just show form
|
||||
3. **API client should be simple** - Fetch data, throw errors, don't redirect
|
||||
4. **Server handles page-level auth** - FastAPI dependencies + exception handler
|
||||
5. **Clear separation** - HTML pages vs API endpoints
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Project Structure (Current)
|
||||
|
||||
```
|
||||
project/
|
||||
├── main.py ✅ Updated
|
||||
├── app/
|
||||
│ ├── api/
|
||||
│ │ ├── main.py ✅ Unchanged
|
||||
│ │ └── v1/
|
||||
│ │ └── admin/
|
||||
│ │ ├── __init__.py ✅ Updated
|
||||
│ │ ├── pages.py ✅ NEW
|
||||
│ │ ├── auth.py ✅ Existing (API routes)
|
||||
│ │ ├── vendors.py ✅ Existing (API routes)
|
||||
│ │ └── dashboard.py ✅ Existing (API routes)
|
||||
│ ├── routes/
|
||||
│ │ └── frontend.py ⚠️ Partially disabled
|
||||
│ ├── exceptions/
|
||||
│ │ └── handler.py ⚠️ Needs simplification
|
||||
│ └── templates/ ✅ NEW
|
||||
│ ├── admin/
|
||||
│ │ ├── base.html
|
||||
│ │ ├── login.html
|
||||
│ │ └── dashboard.html
|
||||
│ └── partials/
|
||||
│ ├── header.html
|
||||
│ └── sidebar.html
|
||||
└── static/
|
||||
├── admin/
|
||||
│ ├── js/
|
||||
│ │ ├── login.js ⚠️ Needs simplification
|
||||
│ │ ├── dashboard.js ✅ Updated
|
||||
│ │ ├── vendors.js ✅ Updated
|
||||
│ │ └── vendor-edit.js ✅ Updated
|
||||
│ └── css/
|
||||
│ └── tailwind.output.css ✅ Unchanged
|
||||
└── shared/
|
||||
└── js/
|
||||
├── api-client.js ⚠️ Needs simplification
|
||||
├── utils.js ✅ Working
|
||||
└── icons.js ✅ Working
|
||||
```
|
||||
|
||||
**Legend:**
|
||||
- ✅ = Working correctly
|
||||
- ⚠️ = Needs attention/debugging
|
||||
- ❌ = Not working/causing issues
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Debug Commands
|
||||
|
||||
### Clear localStorage (Browser Console)
|
||||
```javascript
|
||||
localStorage.clear();
|
||||
```
|
||||
|
||||
### Check stored tokens
|
||||
```javascript
|
||||
console.log('admin_token:', localStorage.getItem('admin_token'));
|
||||
console.log('admin_user:', localStorage.getItem('admin_user'));
|
||||
```
|
||||
|
||||
### Test API call manually
|
||||
```javascript
|
||||
fetch('/api/v1/admin/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('admin_token')}`
|
||||
}
|
||||
}).then(r => r.json()).then(d => console.log(d));
|
||||
```
|
||||
|
||||
### Check current route
|
||||
```javascript
|
||||
console.log('Current path:', window.location.pathname);
|
||||
console.log('Full URL:', window.location.href);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Reference: Working Code Snippets
|
||||
|
||||
### Minimal Login.js (To Try Tomorrow)
|
||||
|
||||
```javascript
|
||||
function adminLogin() {
|
||||
return {
|
||||
dark: false,
|
||||
credentials: { username: '', password: '' },
|
||||
loading: false,
|
||||
error: null,
|
||||
success: null,
|
||||
errors: {},
|
||||
|
||||
init() {
|
||||
this.dark = localStorage.getItem('theme') === 'dark';
|
||||
// NO AUTH CHECKING - just show form
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
if (!this.validateForm()) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: this.credentials.username,
|
||||
password: this.credentials.password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.message);
|
||||
|
||||
localStorage.setItem('admin_token', data.access_token);
|
||||
localStorage.setItem('admin_user', JSON.stringify(data.user));
|
||||
|
||||
this.success = 'Login successful!';
|
||||
setTimeout(() => window.location.href = '/admin/dashboard', 500);
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Simplified API Client Request Method
|
||||
|
||||
```javascript
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const config = {
|
||||
...options,
|
||||
headers: this.getHeaders(options.headers)
|
||||
};
|
||||
|
||||
const response = await fetch(url, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Request failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
// NO REDIRECT LOGIC HERE!
|
||||
}
|
||||
```
|
||||
|
||||
### Simplified Exception Handler
|
||||
|
||||
```python
|
||||
if exc.status_code == 401:
|
||||
accept_header = request.headers.get("accept", "")
|
||||
is_browser = "text/html" in accept_header
|
||||
|
||||
if is_browser and not request.url.path.endswith("/login"):
|
||||
if request.url.path.startswith("/admin"):
|
||||
return RedirectResponse(url="/admin/login", status_code=302)
|
||||
|
||||
# Return JSON for API calls
|
||||
return JSONResponse(status_code=exc.status_code, content=exc.to_dict())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Questions to Answer Tomorrow
|
||||
|
||||
1. Does the simplified auth flow work without loops?
|
||||
2. Can we successfully login and access dashboard?
|
||||
3. Are tokens being sent correctly in API requests?
|
||||
4. Do we need the auth check on login page at all?
|
||||
5. Should we move ALL redirect logic to server-side?
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
The migration will be considered successful when:
|
||||
|
||||
- [ ] Login page loads without loops
|
||||
- [ ] Login succeeds and redirects to dashboard
|
||||
- [ ] Dashboard displays with sidebar and header
|
||||
- [ ] No 404 errors for partials
|
||||
- [ ] Icons display correctly
|
||||
- [ ] Stats cards load data from API
|
||||
- [ ] Navigation between admin pages works
|
||||
- [ ] Logout works correctly
|
||||
|
||||
---
|
||||
|
||||
**End of Session - October 20, 2025**
|
||||
|
||||
Good work today! We made significant progress on the infrastructure. Tomorrow we'll resolve the auth loop and complete the admin panel migration.
|
||||
284
docs/__temp/__PROJECT_ROADMAP/ROUTE_MIGRATION_SUMMARY.txt
Normal file
284
docs/__temp/__PROJECT_ROADMAP/ROUTE_MIGRATION_SUMMARY.txt
Normal file
@@ -0,0 +1,284 @@
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ ROUTE MIGRATION: Static → Jinja2 Templates ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📦 WHAT YOU GET
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
3 New Route Files:
|
||||
├─ admin_pages.py ......... Admin HTML routes (12 routes)
|
||||
├─ vendor_pages.py ........ Vendor HTML routes (13 routes)
|
||||
└─ shop_pages.py .......... Shop HTML routes (19 routes)
|
||||
|
||||
1 Migration Guide:
|
||||
└─ MIGRATION_GUIDE.md ..... Complete step-by-step guide
|
||||
|
||||
|
||||
🎯 KEY IMPROVEMENTS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Before (frontend.py):
|
||||
❌ Static FileResponse
|
||||
❌ No authentication
|
||||
❌ No dynamic content
|
||||
❌ Poor SEO
|
||||
|
||||
After (New files):
|
||||
✅ Jinja2 Templates
|
||||
✅ Server-side auth
|
||||
✅ Dynamic rendering
|
||||
✅ Better SEO
|
||||
|
||||
|
||||
📁 FILE STRUCTURE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
app/
|
||||
├── api/v1/
|
||||
│ ├── admin/
|
||||
│ │ └── pages.py ←─────────── Admin routes (NEW)
|
||||
│ ├── vendor/
|
||||
│ │ └── pages.py ←─────────── Vendor routes (NEW)
|
||||
│ └── shop/
|
||||
│ └── pages.py ←─────────── Shop routes (NEW)
|
||||
├── routes/
|
||||
│ └── frontend.py ←──────────── DELETE after migration
|
||||
└── templates/
|
||||
├── admin/ ←────────────── Admin HTML templates
|
||||
├── vendor/ ←────────────── Vendor HTML templates
|
||||
└── shop/ ←────────────── Shop HTML templates
|
||||
|
||||
|
||||
🚀 QUICK INSTALL (5 STEPS)
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Step 1: Create directories
|
||||
$ mkdir -p app/api/v1/admin
|
||||
$ mkdir -p app/api/v1/vendor
|
||||
$ mkdir -p app/api/v1/shop
|
||||
|
||||
Step 2: Copy new route files
|
||||
$ cp admin_pages.py app/api/v1/admin/pages.py
|
||||
$ cp vendor_pages.py app/api/v1/vendor/pages.py
|
||||
$ cp shop_pages.py app/api/v1/shop/pages.py
|
||||
|
||||
Step 3: Update main router (see MIGRATION_GUIDE.md)
|
||||
- Include new routers in app/api/v1/router.py
|
||||
|
||||
Step 4: Test routes
|
||||
$ uvicorn app.main:app --reload
|
||||
- Visit http://localhost:8000/admin/dashboard
|
||||
- Visit http://localhost:8000/vendor/ACME/dashboard
|
||||
- Visit http://localhost:8000/shop/
|
||||
|
||||
Step 5: Remove old frontend.py
|
||||
$ mv app/routes/frontend.py app/routes/frontend.py.backup
|
||||
|
||||
|
||||
📋 ROUTE BREAKDOWN
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Admin Routes (admin_pages.py):
|
||||
✅ /admin/ → Redirect to login
|
||||
✅ /admin/login → Login page
|
||||
✅ /admin/dashboard → Dashboard
|
||||
✅ /admin/vendors → Vendor list
|
||||
✅ /admin/vendors/create → Create vendor form
|
||||
✅ /admin/vendors/{code} → Vendor details
|
||||
✅ /admin/vendors/{code}/edit → Edit vendor form
|
||||
✅ /admin/users → User management
|
||||
✅ /admin/imports → Import history
|
||||
✅ /admin/settings → Platform settings
|
||||
|
||||
Vendor Routes (vendor_pages.py):
|
||||
✅ /vendor/{code}/ → Redirect to login
|
||||
✅ /vendor/{code}/login → Login page
|
||||
✅ /vendor/{code}/dashboard → Dashboard
|
||||
✅ /vendor/{code}/products → Product management
|
||||
✅ /vendor/{code}/orders → Order management
|
||||
✅ /vendor/{code}/customers → Customer management
|
||||
✅ /vendor/{code}/inventory → Inventory management
|
||||
✅ /vendor/{code}/marketplace → Marketplace imports
|
||||
✅ /vendor/{code}/team → Team management
|
||||
✅ /vendor/{code}/settings → Vendor settings
|
||||
✅ /vendor/login → Fallback login (query param)
|
||||
✅ /vendor/dashboard → Fallback dashboard
|
||||
|
||||
Shop Routes (shop_pages.py):
|
||||
Public Routes:
|
||||
✅ /shop/ → Homepage
|
||||
✅ /shop/products → Product catalog
|
||||
✅ /shop/products/{id} → Product detail
|
||||
✅ /shop/categories/{slug} → Category page
|
||||
✅ /shop/cart → Shopping cart
|
||||
✅ /shop/checkout → Checkout
|
||||
✅ /shop/search → Search results
|
||||
✅ /shop/account/register → Registration
|
||||
✅ /shop/account/login → Customer login
|
||||
|
||||
Authenticated Routes:
|
||||
✅ /shop/account/dashboard → Account dashboard
|
||||
✅ /shop/account/orders → Order history
|
||||
✅ /shop/account/orders/{id} → Order detail
|
||||
✅ /shop/account/profile → Profile settings
|
||||
✅ /shop/account/addresses → Address management
|
||||
✅ /shop/account/wishlist → Wishlist
|
||||
✅ /shop/account/settings → Account settings
|
||||
|
||||
Static Pages:
|
||||
✅ /shop/about → About us
|
||||
✅ /shop/contact → Contact us
|
||||
✅ /shop/faq → FAQ
|
||||
✅ /shop/privacy → Privacy policy
|
||||
✅ /shop/terms → Terms & conditions
|
||||
|
||||
|
||||
🔑 KEY FEATURES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
✅ Server-Side Authentication
|
||||
- Admin routes require admin role
|
||||
- Vendor routes require vendor role
|
||||
- Shop account routes require customer role
|
||||
- Public routes accessible to all
|
||||
|
||||
✅ Dynamic Content Rendering
|
||||
- User data passed to templates
|
||||
- Server-side rendering for SEO
|
||||
- Context variables for personalization
|
||||
|
||||
✅ Template Inheritance
|
||||
- Base templates for consistent layout
|
||||
- Block overrides for page-specific content
|
||||
- Shared components
|
||||
|
||||
✅ Proper URL Structure
|
||||
- RESTful URL patterns
|
||||
- Vendor code in URL path
|
||||
- Clear separation of concerns
|
||||
|
||||
|
||||
⚠️ IMPORTANT NOTES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Route Order Matters!
|
||||
❌ WRONG:
|
||||
@router.get("/vendors/{code}")
|
||||
@router.get("/vendors/create") ← Never matches!
|
||||
|
||||
✅ CORRECT:
|
||||
@router.get("/vendors/create") ← Specific first
|
||||
@router.get("/vendors/{code}") ← Parameterized last
|
||||
|
||||
Authentication Dependencies:
|
||||
- get_current_admin_user → Admin pages
|
||||
- get_current_vendor_user → Vendor pages
|
||||
- get_current_customer_user → Shop account pages
|
||||
|
||||
Template Paths:
|
||||
- Must match directory structure
|
||||
- Use forward slashes: "admin/dashboard.html"
|
||||
- Base path: "app/templates/"
|
||||
|
||||
|
||||
🧪 TESTING CHECKLIST
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Admin Routes:
|
||||
□ /admin/ redirects to /admin/login
|
||||
□ /admin/login shows login form
|
||||
□ /admin/dashboard requires auth
|
||||
□ /admin/vendors shows vendor list
|
||||
□ /admin/vendors/{code}/edit loads correctly
|
||||
|
||||
Vendor Routes:
|
||||
□ /vendor/ACME/ redirects to login
|
||||
□ /vendor/ACME/login shows login form
|
||||
□ /vendor/ACME/dashboard requires auth
|
||||
□ All /admin/* subroutes work
|
||||
|
||||
Shop Routes:
|
||||
□ /shop/ shows products
|
||||
□ /shop/products/{id} shows product
|
||||
□ /shop/cart works without auth
|
||||
□ /shop/account/dashboard requires auth
|
||||
□ /shop/account/orders shows orders
|
||||
|
||||
|
||||
🔧 COMMON ISSUES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Problem: "Template not found"
|
||||
→ Check templates directory path
|
||||
→ Verify template file exists
|
||||
→ Check forward slashes in path
|
||||
|
||||
Problem: "401 Unauthorized"
|
||||
→ Check auth dependency is defined
|
||||
→ Verify token is being sent
|
||||
→ Check user role matches requirement
|
||||
|
||||
Problem: "Route conflict"
|
||||
→ Reorder routes (specific before parameterized)
|
||||
→ Check for duplicate routes
|
||||
→ Review route registration order
|
||||
|
||||
Problem: "Module not found"
|
||||
→ Check __init__.py exists in directories
|
||||
→ Verify import paths
|
||||
→ Restart server after adding files
|
||||
|
||||
|
||||
📊 COMPARISON TABLE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Feature │ Old (frontend.py) │ New (pages.py)
|
||||
─────────────────┼───────────────────┼────────────────
|
||||
Authentication │ Client-side │ Server-side ✅
|
||||
Dynamic Content │ None │ Full Python ✅
|
||||
Template Reuse │ Copy-paste │ Inheritance ✅
|
||||
SEO │ Poor │ Good ✅
|
||||
Security │ Client only │ Server validates ✅
|
||||
Maintainability │ Medium │ High ✅
|
||||
Lines of Code │ ~200 │ ~600 (organized) ✅
|
||||
|
||||
|
||||
💡 PRO TIPS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Test incrementally
|
||||
- Migrate one section at a time
|
||||
- Keep old routes until new ones work
|
||||
|
||||
2. Use template inheritance
|
||||
- Create base.html for each section
|
||||
- Override blocks in child templates
|
||||
|
||||
3. Pass user data in context
|
||||
- Available in templates via {{ user.name }}
|
||||
- No extra API calls needed
|
||||
|
||||
4. Handle both auth and non-auth
|
||||
- Some routes public (login, register)
|
||||
- Some routes require auth (dashboard)
|
||||
|
||||
5. Follow RESTful patterns
|
||||
- /vendors/create not /create-vendor
|
||||
- /vendors/{code}/edit not /edit-vendor/{code}
|
||||
|
||||
|
||||
📖 NEXT STEPS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Read MIGRATION_GUIDE.md for detailed steps
|
||||
2. Install new route files
|
||||
3. Update main router
|
||||
4. Test each section
|
||||
5. Remove old frontend.py
|
||||
6. Update documentation
|
||||
|
||||
|
||||
══════════════════════════════════════════════════════════════════
|
||||
Migration made easy! 🚀
|
||||
Your app is now using modern Jinja2 templates!
|
||||
══════════════════════════════════════════════════════════════════
|
||||
512
docs/__temp/__PROJECT_ROADMAP/implementation_roadmap.md
Normal file
512
docs/__temp/__PROJECT_ROADMAP/implementation_roadmap.md
Normal file
@@ -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
|
||||
<div x-data="myComponent()" x-init="init()">
|
||||
<div x-show="loading">Loading...</div>
|
||||
<div x-show="error" x-text="error"></div>
|
||||
<div x-show="!loading && !error">
|
||||
<template x-for="item in data" :key="item.id">
|
||||
<div x-text="item.name"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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!** 🚀
|
||||
1070
docs/__temp/__PROJECT_ROADMAP/slice1_doc.md
Normal file
1070
docs/__temp/__PROJECT_ROADMAP/slice1_doc.md
Normal file
File diff suppressed because it is too large
Load Diff
808
docs/__temp/__PROJECT_ROADMAP/slice2_doc.md
Normal file
808
docs/__temp/__PROJECT_ROADMAP/slice2_doc.md
Normal file
@@ -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 %}
|
||||
<div x-data="marketplaceImport()" x-init="loadJobs()">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>Marketplace Import</h1>
|
||||
<button @click="showImportModal = true" class="btn btn-primary">
|
||||
New Import
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Import Configuration Card -->
|
||||
<div class="card mb-3">
|
||||
<h3>Letzshop CSV URLs</h3>
|
||||
<div class="config-grid">
|
||||
<div>
|
||||
<label>French (FR)</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.letzshop_csv_url_fr"
|
||||
class="form-control"
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label>English (EN)</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.letzshop_csv_url_en"
|
||||
class="form-control"
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label>German (DE)</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="vendor.letzshop_csv_url_de"
|
||||
class="form-control"
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted mt-2">
|
||||
Configure these URLs in vendor settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Import Jobs List -->
|
||||
<div class="card">
|
||||
<h3>Import History</h3>
|
||||
|
||||
<template x-if="jobs.length === 0 && !loading">
|
||||
<p class="text-muted">No imports yet. Start your first import!</p>
|
||||
</template>
|
||||
|
||||
<template x-if="jobs.length > 0">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Language</th>
|
||||
<th>Status</th>
|
||||
<th>Progress</th>
|
||||
<th>Results</th>
|
||||
<th>Started</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="job in jobs" :key="job.id">
|
||||
<tr>
|
||||
<td><strong x-text="`#${job.id}`"></strong></td>
|
||||
<td x-text="job.language.toUpperCase()"></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
:class="{
|
||||
'badge-warning': job.status === 'pending' || job.status === 'processing',
|
||||
'badge-success': job.status === 'completed',
|
||||
'badge-danger': job.status === 'failed'
|
||||
}"
|
||||
x-text="job.status"
|
||||
></span>
|
||||
</td>
|
||||
<td>
|
||||
<template x-if="job.status === 'processing'">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="`width: ${(job.processed_rows / job.total_rows * 100)}%`"
|
||||
></div>
|
||||
</div>
|
||||
<small x-text="`${job.processed_rows} / ${job.total_rows}`"></small>
|
||||
</template>
|
||||
<template x-if="job.status === 'completed'">
|
||||
<span x-text="`${job.total_rows} rows`"></span>
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<template x-if="job.status === 'completed'">
|
||||
<div class="text-sm">
|
||||
<div>✓ <span x-text="job.imported_count"></span> imported</div>
|
||||
<div>↻ <span x-text="job.updated_count"></span> updated</div>
|
||||
<template x-if="job.error_count > 0">
|
||||
<div class="text-danger">✗ <span x-text="job.error_count"></span> errors</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
<td x-text="formatDate(job.started_at)"></td>
|
||||
<td>
|
||||
<button
|
||||
@click="viewProducts(job.id)"
|
||||
class="btn btn-sm"
|
||||
:disabled="job.status !== 'completed'"
|
||||
>
|
||||
View Products
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- New Import Modal -->
|
||||
<div x-show="showImportModal" class="modal-overlay" @click.self="showImportModal = false">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>New Import</h3>
|
||||
<button @click="showImportModal = false" class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="triggerImport()">
|
||||
<div class="form-group">
|
||||
<label>Language</label>
|
||||
<select x-model="newImport.language" class="form-control" required>
|
||||
<option value="">Select language</option>
|
||||
<option value="fr">French (FR)</option>
|
||||
<option value="en">English (EN)</option>
|
||||
<option value="de">German (DE)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>CSV URL</label>
|
||||
<input
|
||||
type="url"
|
||||
x-model="newImport.csv_url"
|
||||
class="form-control"
|
||||
placeholder="https://..."
|
||||
required
|
||||
>
|
||||
<div class="form-help">
|
||||
Or use configured URL:
|
||||
<button
|
||||
type="button"
|
||||
@click="newImport.csv_url = vendor.letzshop_csv_url_fr"
|
||||
class="btn-link"
|
||||
x-show="newImport.language === 'fr'"
|
||||
>
|
||||
Use FR URL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" @click="showImportModal = false" class="btn btn-secondary">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="importing">
|
||||
<span x-show="!importing">Start Import</span>
|
||||
<span x-show="importing" class="loading-spinner"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.vendorData = {{ vendor|tojson }};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function marketplaceImport() {
|
||||
return {
|
||||
vendor: window.vendorData,
|
||||
jobs: [],
|
||||
loading: false,
|
||||
importing: false,
|
||||
showImportModal: false,
|
||||
newImport: {
|
||||
language: '',
|
||||
csv_url: ''
|
||||
},
|
||||
|
||||
async loadJobs() {
|
||||
this.loading = true;
|
||||
try {
|
||||
this.jobs = await apiClient.get('/api/v1/vendor/marketplace/jobs');
|
||||
|
||||
// Poll for active jobs
|
||||
const activeJobs = this.jobs.filter(j =>
|
||||
j.status === 'pending' || j.status === 'processing'
|
||||
);
|
||||
|
||||
if (activeJobs.length > 0) {
|
||||
setTimeout(() => this.loadJobs(), 3000); // Poll every 3 seconds
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Failed to load imports', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async triggerImport() {
|
||||
this.importing = true;
|
||||
try {
|
||||
const job = await apiClient.post('/api/v1/vendor/marketplace/import', {
|
||||
csv_url: this.newImport.csv_url,
|
||||
language: this.newImport.language
|
||||
});
|
||||
|
||||
this.jobs.unshift(job);
|
||||
this.showImportModal = false;
|
||||
this.newImport = { language: '', csv_url: '' };
|
||||
|
||||
showNotification('Import started successfully', 'success');
|
||||
|
||||
// Start polling
|
||||
setTimeout(() => this.loadJobs(), 3000);
|
||||
} catch (error) {
|
||||
showNotification(error.message || 'Import failed', 'error');
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
|
||||
viewProducts(jobId) {
|
||||
window.location.href = `/vendor/marketplace/products?job_id=${jobId}`;
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
return new Date(dateString).toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% 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
|
||||
624
docs/__temp/__PROJECT_ROADMAP/slice3_doc.md
Normal file
624
docs/__temp/__PROJECT_ROADMAP/slice3_doc.md
Normal file
@@ -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
|
||||
887
docs/__temp/__PROJECT_ROADMAP/slice4_doc.md
Normal file
887
docs/__temp/__PROJECT_ROADMAP/slice4_doc.md
Normal file
@@ -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 %}
|
||||
<div x-data="shopHome()" x-init="loadFeaturedProducts()">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
<h1>Welcome to {{ vendor.name }}</h1>
|
||||
<p>{{ vendor.description }}</p>
|
||||
<a href="/shop/products" class="btn btn-primary btn-lg">
|
||||
Shop Now
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Products -->
|
||||
<div class="products-section">
|
||||
<h2>Featured Products</h2>
|
||||
<div class="product-grid">
|
||||
<template x-for="product in featuredProducts" :key="product.id">
|
||||
<div class="product-card">
|
||||
<a :href="`/shop/products/${product.id}`">
|
||||
<img :src="product.featured_image || '/static/images/no-image.png'"
|
||||
:alt="product.title">
|
||||
<h3 x-text="product.title"></h3>
|
||||
<p class="price">€<span x-text="product.price.toFixed(2)"></span></p>
|
||||
</a>
|
||||
<button @click="addToCart(product.id)" class="btn btn-primary btn-sm">
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
#### Product Detail (`templates/shop/product.html`)
|
||||
|
||||
```html
|
||||
{% extends "shop/base_shop.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="productDetail()" x-init="loadProduct()">
|
||||
<div class="product-detail">
|
||||
<!-- Product Images -->
|
||||
<div class="product-images">
|
||||
<img :src="product.featured_image" :alt="product.title" class="main-image">
|
||||
</div>
|
||||
|
||||
<!-- Product Info -->
|
||||
<div class="product-info">
|
||||
<h1 x-text="product.title"></h1>
|
||||
<div class="price-section">
|
||||
<span class="price">€<span x-text="product.price"></span></span>
|
||||
<template x-if="product.compare_at_price">
|
||||
<span class="compare-price">€<span x-text="product.compare_at_price"></span></span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="product-description" x-html="product.description"></div>
|
||||
|
||||
<!-- Quantity Selector -->
|
||||
<div class="quantity-selector">
|
||||
<label>Quantity</label>
|
||||
<input
|
||||
type="number"
|
||||
x-model.number="quantity"
|
||||
:min="1"
|
||||
:max="product.stock_quantity"
|
||||
>
|
||||
<span class="stock-info" x-text="`${product.stock_quantity} in stock`"></span>
|
||||
</div>
|
||||
|
||||
<!-- Add to Cart -->
|
||||
<button
|
||||
@click="addToCart()"
|
||||
class="btn btn-primary btn-lg"
|
||||
:disabled="!canAddToCart || adding"
|
||||
>
|
||||
<span x-show="!adding">Add to Cart</span>
|
||||
<span x-show="adding" class="loading-spinner"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.productId = {{ product.id }};
|
||||
window.vendorId = {{ vendor.id }};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function productDetail() {
|
||||
return {
|
||||
product: {},
|
||||
quantity: 1,
|
||||
adding: false,
|
||||
loading: false,
|
||||
|
||||
get canAddToCart() {
|
||||
return this.product.stock_quantity >= this.quantity && this.quantity > 0;
|
||||
},
|
||||
|
||||
async loadProduct() {
|
||||
this.loading = true;
|
||||
try {
|
||||
this.product = await apiClient.get(
|
||||
`/api/v1/public/vendors/${window.vendorId}/products/${window.productId}`
|
||||
);
|
||||
} catch (error) {
|
||||
showNotification('Failed to load product', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async addToCart() {
|
||||
this.adding = true;
|
||||
try {
|
||||
const sessionId = getOrCreateSessionId();
|
||||
await apiClient.post(
|
||||
`/api/v1/public/vendors/${window.vendorId}/cart/${sessionId}/items`,
|
||||
{
|
||||
product_id: this.product.id,
|
||||
quantity: this.quantity
|
||||
}
|
||||
);
|
||||
|
||||
showNotification('Added to cart!', 'success');
|
||||
updateCartCount(); // Update cart icon
|
||||
} catch (error) {
|
||||
showNotification(error.message || 'Failed to add to cart', 'error');
|
||||
} finally {
|
||||
this.adding = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% 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
|
||||
1628
docs/__temp/__PROJECT_ROADMAP/slice5_doc.md
Normal file
1628
docs/__temp/__PROJECT_ROADMAP/slice5_doc.md
Normal file
File diff suppressed because it is too large
Load Diff
306
docs/__temp/__PROJECT_ROADMAP/slice_overview.md
Normal file
306
docs/__temp/__PROJECT_ROADMAP/slice_overview.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Multi-Tenant Ecommerce Platform - Vertical Slices Overview
|
||||
|
||||
## 📋 Development Approach
|
||||
|
||||
This project follows a **vertical slice development approach**, delivering complete, working user workflows incrementally. Each slice is fully functional and provides immediate value.
|
||||
|
||||
## 🎯 Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Framework**: FastAPI (Python 3.11+)
|
||||
- **Database**: PostgreSQL with SQLAlchemy ORM
|
||||
- **Authentication**: JWT tokens with bcrypt
|
||||
- **Background Jobs**: Celery (for async tasks)
|
||||
- **API Documentation**: Auto-generated OpenAPI/Swagger
|
||||
|
||||
### Frontend
|
||||
- **Templating**: Jinja2 (server-side rendering)
|
||||
- **JavaScript Framework**: Alpine.js v3.x (15KB, CDN-based)
|
||||
- **Styling**: Custom CSS with CSS variables
|
||||
- **AJAX**: Vanilla JavaScript with Fetch API
|
||||
- **No Build Step**: Everything runs directly in the browser
|
||||
|
||||
### Why Alpine.js + Jinja2?
|
||||
- ✅ **Lightweight**: Only 15KB, no build step required
|
||||
- ✅ **Perfect Jinja2 Integration**: Works seamlessly with server-side templates
|
||||
- ✅ **Reactive State**: Modern UX without framework complexity
|
||||
- ✅ **Scoped Components**: Natural vendor isolation
|
||||
- ✅ **Progressive Enhancement**: Works even if JS fails
|
||||
- ✅ **Minimal Learning Curve**: Feels like inline JavaScript
|
||||
|
||||
## 📚 Slice Documentation Structure
|
||||
|
||||
Each slice has its own comprehensive markdown file:
|
||||
|
||||
### Slice 1: Multi-Tenant Foundation ✅ IN PROGRESS
|
||||
**File**: `01_slice1_admin_vendor_foundation.md`
|
||||
- Admin creates vendors through admin interface
|
||||
- Vendor owner login with context detection
|
||||
- Complete vendor data isolation
|
||||
- **Status**: Backend mostly complete, frontend in progress
|
||||
|
||||
### Slice 2: Marketplace Integration
|
||||
**File**: `02_slice2_marketplace_import.md`
|
||||
- CSV import from Letzshop marketplace
|
||||
- Background job processing
|
||||
- Product staging area
|
||||
- Import status tracking with Alpine.js
|
||||
|
||||
### Slice 3: Product Catalog Management
|
||||
**File**: `03_slice3_product_catalog.md`
|
||||
- Browse imported products in staging
|
||||
- Select and publish to vendor catalog
|
||||
- Product customization (pricing, descriptions)
|
||||
- Inventory management
|
||||
|
||||
### Slice 4: Customer Shopping Experience
|
||||
**File**: `04_slice4_customer_shopping.md`
|
||||
- Public product browsing
|
||||
- Customer registration/login
|
||||
- Shopping cart with Alpine.js reactivity
|
||||
- Product search functionality
|
||||
|
||||
### Slice 5: Order Processing
|
||||
**File**: `05_slice5_order_processing.md`
|
||||
- Checkout workflow
|
||||
- Order placement
|
||||
- Order management (vendor side)
|
||||
- Order history (customer side)
|
||||
|
||||
## 🎯 Slice Completion Criteria
|
||||
|
||||
Each slice must pass these gates before moving to the next:
|
||||
|
||||
### Technical Criteria
|
||||
- [ ] All backend endpoints implemented and tested
|
||||
- [ ] Frontend pages created with Jinja2 templates
|
||||
- [ ] Alpine.js components working (where applicable)
|
||||
- [ ] Database migrations applied successfully
|
||||
- [ ] Service layer business logic complete
|
||||
- [ ] Exception handling implemented
|
||||
- [ ] API documentation updated
|
||||
|
||||
### Quality Criteria
|
||||
- [ ] Manual testing complete (all user flows)
|
||||
- [ ] Security validation (vendor isolation)
|
||||
- [ ] Performance acceptable (basic load testing)
|
||||
- [ ] No console errors in browser
|
||||
- [ ] Responsive design works on mobile
|
||||
- [ ] Code follows project conventions
|
||||
|
||||
### Documentation Criteria
|
||||
- [ ] Slice markdown file updated
|
||||
- [ ] API endpoints documented
|
||||
- [ ] Frontend components documented
|
||||
- [ ] Database changes documented
|
||||
- [ ] Testing checklist completed
|
||||
|
||||
## 🗓️ Estimated Timeline
|
||||
|
||||
### Week 1: Slice 1 - Foundation ⏳ Current
|
||||
- Days 1-3: Backend completion (vendor context, admin APIs)
|
||||
- Days 4-5: Frontend completion (admin pages, vendor login)
|
||||
- **Deliverable**: Admin can create vendors, vendor owners can log in
|
||||
|
||||
### Week 2: Slice 2 - Import
|
||||
- Days 1-3: Import backend (CSV processing, job tracking)
|
||||
- Days 4-5: Import frontend (upload UI, status tracking)
|
||||
- **Deliverable**: Vendors can import products from Letzshop
|
||||
|
||||
### Week 3: Slice 3 - Catalog
|
||||
- Days 1-3: Catalog backend (product publishing, inventory)
|
||||
- Days 4-5: Catalog frontend (product management UI)
|
||||
- **Deliverable**: Vendors can manage product catalog
|
||||
|
||||
### Week 4: Slice 4 - Shopping
|
||||
- Days 1-3: Customer backend (registration, cart, products)
|
||||
- Days 4-5: Shop frontend (product browsing, cart)
|
||||
- **Deliverable**: Customers can browse and add to cart
|
||||
|
||||
### Week 5: Slice 5 - Orders
|
||||
- Days 1-3: Order backend (checkout, order management)
|
||||
- Days 4-5: Order frontend (checkout flow, order history)
|
||||
- **Deliverable**: Complete order workflow functional
|
||||
|
||||
## 📊 Progress Tracking
|
||||
|
||||
### ✅ Completed
|
||||
- Database schema design
|
||||
- Core models (User, Vendor, Roles)
|
||||
- Authentication system
|
||||
- Admin service layer
|
||||
- Vendor context detection middleware
|
||||
|
||||
### 🔄 In Progress (Slice 1)
|
||||
- Admin frontend pages (login, dashboard, vendors)
|
||||
- Vendor frontend pages (login, dashboard)
|
||||
- Admin API endpoints refinement
|
||||
- Frontend-backend integration
|
||||
|
||||
### 📋 Upcoming (Slice 2)
|
||||
- MarketplaceProduct model
|
||||
- ImportJob model
|
||||
- CSV processing service
|
||||
- Import frontend with Alpine.js
|
||||
|
||||
## 🎨 Frontend Architecture Pattern
|
||||
|
||||
### Page Structure (Jinja2 + Alpine.js)
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="componentName()">
|
||||
<!-- Alpine.js reactive component -->
|
||||
<h1 x-text="title"></h1>
|
||||
|
||||
<!-- Jinja2 for initial data -->
|
||||
<script>
|
||||
window.initialData = {{ data|tojson }};
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/admin/component.js"></script>
|
||||
{% 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.
|
||||
Reference in New Issue
Block a user