Add order page with real API calls
This commit is contained in:
@@ -55,8 +55,7 @@ async def get_orders_service(db = Depends(get_db)) -> OrdersService:
|
||||
status_history_repo=OrderStatusHistoryRepository(),
|
||||
inventory_client=get_inventory_client(),
|
||||
production_client=get_production_client(),
|
||||
sales_client=get_sales_client(),
|
||||
notification_client=None # Notification client not available
|
||||
sales_client=get_sales_client()
|
||||
)
|
||||
|
||||
|
||||
|
||||
152
services/orders/app/models/enums.py
Normal file
152
services/orders/app/models/enums.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# services/orders/app/models/enums.py
|
||||
"""
|
||||
Enum definitions for Orders Service
|
||||
Following the pattern used in the Inventory Service for better type safety and maintainability
|
||||
"""
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class CustomerType(enum.Enum):
|
||||
"""Customer type classifications"""
|
||||
INDIVIDUAL = "individual"
|
||||
BUSINESS = "business"
|
||||
CENTRAL_BAKERY = "central_bakery"
|
||||
|
||||
|
||||
class DeliveryMethod(enum.Enum):
|
||||
"""Order delivery methods"""
|
||||
DELIVERY = "delivery"
|
||||
PICKUP = "pickup"
|
||||
|
||||
|
||||
class PaymentTerms(enum.Enum):
|
||||
"""Payment terms for customers and orders"""
|
||||
IMMEDIATE = "immediate"
|
||||
NET_30 = "net_30"
|
||||
NET_60 = "net_60"
|
||||
|
||||
|
||||
class PaymentMethod(enum.Enum):
|
||||
"""Payment methods for orders"""
|
||||
CASH = "cash"
|
||||
CARD = "card"
|
||||
BANK_TRANSFER = "bank_transfer"
|
||||
ACCOUNT = "account"
|
||||
|
||||
|
||||
class PaymentStatus(enum.Enum):
|
||||
"""Payment status for orders"""
|
||||
PENDING = "pending"
|
||||
PARTIAL = "partial"
|
||||
PAID = "paid"
|
||||
FAILED = "failed"
|
||||
REFUNDED = "refunded"
|
||||
|
||||
|
||||
class CustomerSegment(enum.Enum):
|
||||
"""Customer segmentation categories"""
|
||||
VIP = "vip"
|
||||
REGULAR = "regular"
|
||||
WHOLESALE = "wholesale"
|
||||
|
||||
|
||||
class PriorityLevel(enum.Enum):
|
||||
"""Priority levels for orders and customers"""
|
||||
HIGH = "high"
|
||||
NORMAL = "normal"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
class OrderType(enum.Enum):
|
||||
"""Order type classifications"""
|
||||
STANDARD = "standard"
|
||||
RUSH = "rush"
|
||||
RECURRING = "recurring"
|
||||
SPECIAL = "special"
|
||||
|
||||
|
||||
class OrderStatus(enum.Enum):
|
||||
"""Order status workflow"""
|
||||
PENDING = "pending"
|
||||
CONFIRMED = "confirmed"
|
||||
IN_PRODUCTION = "in_production"
|
||||
READY = "ready"
|
||||
OUT_FOR_DELIVERY = "out_for_delivery"
|
||||
DELIVERED = "delivered"
|
||||
CANCELLED = "cancelled"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class OrderSource(enum.Enum):
|
||||
"""Source of order creation"""
|
||||
MANUAL = "manual"
|
||||
ONLINE = "online"
|
||||
PHONE = "phone"
|
||||
APP = "app"
|
||||
API = "api"
|
||||
|
||||
|
||||
class SalesChannel(enum.Enum):
|
||||
"""Sales channel classification"""
|
||||
DIRECT = "direct"
|
||||
WHOLESALE = "wholesale"
|
||||
RETAIL = "retail"
|
||||
|
||||
|
||||
class BusinessModel(enum.Enum):
|
||||
"""Business model types"""
|
||||
INDIVIDUAL_BAKERY = "individual_bakery"
|
||||
CENTRAL_BAKERY = "central_bakery"
|
||||
|
||||
|
||||
# Procurement-related enums
|
||||
class ProcurementPlanType(enum.Enum):
|
||||
"""Procurement plan types"""
|
||||
REGULAR = "regular"
|
||||
EMERGENCY = "emergency"
|
||||
SEASONAL = "seasonal"
|
||||
|
||||
|
||||
class ProcurementStrategy(enum.Enum):
|
||||
"""Procurement strategies"""
|
||||
JUST_IN_TIME = "just_in_time"
|
||||
BULK = "bulk"
|
||||
MIXED = "mixed"
|
||||
|
||||
|
||||
class RiskLevel(enum.Enum):
|
||||
"""Risk level classifications"""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class RequirementStatus(enum.Enum):
|
||||
"""Procurement requirement status"""
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
ORDERED = "ordered"
|
||||
PARTIALLY_RECEIVED = "partially_received"
|
||||
RECEIVED = "received"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class PlanStatus(enum.Enum):
|
||||
"""Procurement plan status"""
|
||||
DRAFT = "draft"
|
||||
PENDING_APPROVAL = "pending_approval"
|
||||
APPROVED = "approved"
|
||||
IN_EXECUTION = "in_execution"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class DeliveryStatus(enum.Enum):
|
||||
"""Delivery status for procurement"""
|
||||
PENDING = "pending"
|
||||
IN_TRANSIT = "in_transit"
|
||||
DELIVERED = "delivered"
|
||||
DELAYED = "delayed"
|
||||
CANCELLED = "cancelled"
|
||||
@@ -5,7 +5,7 @@
|
||||
Order-related repositories for Orders Service
|
||||
"""
|
||||
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime, date, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
@@ -98,12 +98,86 @@ class CustomerRepository(BaseRepository[Customer, dict, dict]):
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def count_created_since(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
since_date: datetime
|
||||
) -> int:
|
||||
"""Count customers created since a specific date"""
|
||||
try:
|
||||
query = select(func.count()).select_from(Customer).where(
|
||||
and_(
|
||||
Customer.tenant_id == tenant_id,
|
||||
Customer.created_at >= since_date
|
||||
)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
return result.scalar()
|
||||
except Exception as e:
|
||||
logger.error("Error counting customers created since date",
|
||||
tenant_id=str(tenant_id),
|
||||
since_date=str(since_date),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
|
||||
class OrderRepository(BaseRepository[CustomerOrder, OrderCreate, OrderUpdate]):
|
||||
"""Repository for customer order operations"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(CustomerOrder)
|
||||
|
||||
async def get_multi(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: Optional[UUID] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
order_by: Optional[str] = None,
|
||||
order_desc: bool = False
|
||||
) -> List[CustomerOrder]:
|
||||
"""Get multiple orders with eager loading of items and customer"""
|
||||
try:
|
||||
query = select(self.model).options(
|
||||
selectinload(CustomerOrder.items),
|
||||
selectinload(CustomerOrder.customer)
|
||||
)
|
||||
|
||||
# Apply tenant filter
|
||||
if tenant_id:
|
||||
query = query.where(self.model.tenant_id == tenant_id)
|
||||
|
||||
# Apply additional filters
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
if hasattr(self.model, key) and value is not None:
|
||||
field = getattr(self.model, key)
|
||||
if isinstance(value, list):
|
||||
query = query.where(field.in_(value))
|
||||
else:
|
||||
query = query.where(field == value)
|
||||
|
||||
# Apply ordering
|
||||
if order_by and hasattr(self.model, order_by):
|
||||
order_column = getattr(self.model, order_by)
|
||||
if order_desc:
|
||||
query = query.order_by(order_column.desc())
|
||||
else:
|
||||
query = query.order_by(order_column)
|
||||
else:
|
||||
# Default ordering by order_date desc
|
||||
query = query.order_by(CustomerOrder.order_date.desc())
|
||||
|
||||
# Apply pagination
|
||||
query = query.offset(skip).limit(limit)
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
except Exception as e:
|
||||
logger.error("Error getting multiple orders", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_with_items(
|
||||
self,
|
||||
|
||||
@@ -11,13 +11,20 @@ from typing import Optional, List, Dict, Any
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
from app.models.enums import (
|
||||
CustomerType, DeliveryMethod, PaymentTerms, PaymentMethod, PaymentStatus,
|
||||
CustomerSegment, PriorityLevel, OrderType, OrderStatus, OrderSource,
|
||||
SalesChannel, BusinessModel, ProcurementPlanType, ProcurementStrategy,
|
||||
RiskLevel, RequirementStatus, PlanStatus, DeliveryStatus
|
||||
)
|
||||
|
||||
|
||||
# ===== Customer Schemas =====
|
||||
|
||||
class CustomerBase(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
business_name: Optional[str] = Field(None, max_length=200)
|
||||
customer_type: str = Field(default="individual", pattern="^(individual|business|central_bakery)$")
|
||||
customer_type: CustomerType = Field(default=CustomerType.INDIVIDUAL)
|
||||
email: Optional[str] = Field(None, max_length=255)
|
||||
phone: Optional[str] = Field(None, max_length=50)
|
||||
address_line1: Optional[str] = Field(None, max_length=255)
|
||||
@@ -27,16 +34,20 @@ class CustomerBase(BaseModel):
|
||||
postal_code: Optional[str] = Field(None, max_length=20)
|
||||
country: str = Field(default="US", max_length=100)
|
||||
is_active: bool = Field(default=True)
|
||||
preferred_delivery_method: str = Field(default="delivery", pattern="^(delivery|pickup)$")
|
||||
payment_terms: str = Field(default="immediate", pattern="^(immediate|net_30|net_60)$")
|
||||
preferred_delivery_method: DeliveryMethod = Field(default=DeliveryMethod.DELIVERY)
|
||||
payment_terms: PaymentTerms = Field(default=PaymentTerms.IMMEDIATE)
|
||||
credit_limit: Optional[Decimal] = Field(None, ge=0)
|
||||
discount_percentage: Decimal = Field(default=Decimal("0.00"), ge=0, le=100)
|
||||
customer_segment: str = Field(default="regular", pattern="^(vip|regular|wholesale)$")
|
||||
priority_level: str = Field(default="normal", pattern="^(high|normal|low)$")
|
||||
customer_segment: CustomerSegment = Field(default=CustomerSegment.REGULAR)
|
||||
priority_level: PriorityLevel = Field(default=PriorityLevel.NORMAL)
|
||||
special_instructions: Optional[str] = None
|
||||
delivery_preferences: Optional[Dict[str, Any]] = None
|
||||
product_preferences: Optional[Dict[str, Any]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class CustomerCreate(CustomerBase):
|
||||
customer_code: str = Field(..., min_length=1, max_length=50)
|
||||
@@ -46,7 +57,7 @@ class CustomerCreate(CustomerBase):
|
||||
class CustomerUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
business_name: Optional[str] = Field(None, max_length=200)
|
||||
customer_type: Optional[str] = Field(None, pattern="^(individual|business|central_bakery)$")
|
||||
customer_type: Optional[CustomerType] = None
|
||||
email: Optional[str] = Field(None, max_length=255)
|
||||
phone: Optional[str] = Field(None, max_length=50)
|
||||
address_line1: Optional[str] = Field(None, max_length=255)
|
||||
@@ -56,16 +67,20 @@ class CustomerUpdate(BaseModel):
|
||||
postal_code: Optional[str] = Field(None, max_length=20)
|
||||
country: Optional[str] = Field(None, max_length=100)
|
||||
is_active: Optional[bool] = None
|
||||
preferred_delivery_method: Optional[str] = Field(None, pattern="^(delivery|pickup)$")
|
||||
payment_terms: Optional[str] = Field(None, pattern="^(immediate|net_30|net_60)$")
|
||||
preferred_delivery_method: Optional[DeliveryMethod] = None
|
||||
payment_terms: Optional[PaymentTerms] = None
|
||||
credit_limit: Optional[Decimal] = Field(None, ge=0)
|
||||
discount_percentage: Optional[Decimal] = Field(None, ge=0, le=100)
|
||||
customer_segment: Optional[str] = Field(None, pattern="^(vip|regular|wholesale)$")
|
||||
priority_level: Optional[str] = Field(None, pattern="^(high|normal|low)$")
|
||||
customer_segment: Optional[CustomerSegment] = None
|
||||
priority_level: Optional[PriorityLevel] = None
|
||||
special_instructions: Optional[str] = None
|
||||
delivery_preferences: Optional[Dict[str, Any]] = None
|
||||
product_preferences: Optional[Dict[str, Any]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class CustomerResponse(CustomerBase):
|
||||
id: UUID
|
||||
@@ -129,26 +144,30 @@ class OrderItemResponse(OrderItemBase):
|
||||
|
||||
class OrderBase(BaseModel):
|
||||
customer_id: UUID
|
||||
order_type: str = Field(default="standard", pattern="^(standard|rush|recurring|special)$")
|
||||
priority: str = Field(default="normal", pattern="^(high|normal|low)$")
|
||||
order_type: OrderType = Field(default=OrderType.STANDARD)
|
||||
priority: PriorityLevel = Field(default=PriorityLevel.NORMAL)
|
||||
requested_delivery_date: datetime
|
||||
delivery_method: str = Field(default="delivery", pattern="^(delivery|pickup)$")
|
||||
delivery_method: DeliveryMethod = Field(default=DeliveryMethod.DELIVERY)
|
||||
delivery_address: Optional[Dict[str, Any]] = None
|
||||
delivery_instructions: Optional[str] = None
|
||||
delivery_window_start: Optional[datetime] = None
|
||||
delivery_window_end: Optional[datetime] = None
|
||||
discount_percentage: Decimal = Field(default=Decimal("0.00"), ge=0, le=100)
|
||||
delivery_fee: Decimal = Field(default=Decimal("0.00"), ge=0)
|
||||
payment_method: Optional[str] = Field(None, pattern="^(cash|card|bank_transfer|account)$")
|
||||
payment_terms: str = Field(default="immediate", pattern="^(immediate|net_30|net_60)$")
|
||||
payment_method: Optional[PaymentMethod] = None
|
||||
payment_terms: PaymentTerms = Field(default=PaymentTerms.IMMEDIATE)
|
||||
special_instructions: Optional[str] = None
|
||||
custom_requirements: Optional[Dict[str, Any]] = None
|
||||
allergen_warnings: Optional[Dict[str, Any]] = None
|
||||
order_source: str = Field(default="manual", pattern="^(manual|online|phone|app|api)$")
|
||||
sales_channel: str = Field(default="direct", pattern="^(direct|wholesale|retail)$")
|
||||
order_source: OrderSource = Field(default=OrderSource.MANUAL)
|
||||
sales_channel: SalesChannel = Field(default=SalesChannel.DIRECT)
|
||||
order_origin: Optional[str] = Field(None, max_length=100)
|
||||
communication_preferences: Optional[Dict[str, Any]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class OrderCreate(OrderBase):
|
||||
tenant_id: UUID
|
||||
@@ -156,21 +175,25 @@ class OrderCreate(OrderBase):
|
||||
|
||||
|
||||
class OrderUpdate(BaseModel):
|
||||
status: Optional[str] = Field(None, pattern="^(pending|confirmed|in_production|ready|out_for_delivery|delivered|cancelled|failed)$")
|
||||
priority: Optional[str] = Field(None, pattern="^(high|normal|low)$")
|
||||
status: Optional[OrderStatus] = None
|
||||
priority: Optional[PriorityLevel] = None
|
||||
requested_delivery_date: Optional[datetime] = None
|
||||
confirmed_delivery_date: Optional[datetime] = None
|
||||
delivery_method: Optional[str] = Field(None, pattern="^(delivery|pickup)$")
|
||||
delivery_method: Optional[DeliveryMethod] = None
|
||||
delivery_address: Optional[Dict[str, Any]] = None
|
||||
delivery_instructions: Optional[str] = None
|
||||
delivery_window_start: Optional[datetime] = None
|
||||
delivery_window_end: Optional[datetime] = None
|
||||
payment_method: Optional[str] = Field(None, pattern="^(cash|card|bank_transfer|account)$")
|
||||
payment_status: Optional[str] = Field(None, pattern="^(pending|partial|paid|failed|refunded)$")
|
||||
payment_method: Optional[PaymentMethod] = None
|
||||
payment_status: Optional[PaymentStatus] = None
|
||||
special_instructions: Optional[str] = None
|
||||
custom_requirements: Optional[Dict[str, Any]] = None
|
||||
allergen_warnings: Optional[Dict[str, Any]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class OrderResponse(OrderBase):
|
||||
id: UUID
|
||||
@@ -205,17 +228,21 @@ class ProcurementRequirementBase(BaseModel):
|
||||
product_name: str = Field(..., min_length=1, max_length=200)
|
||||
product_sku: Optional[str] = Field(None, max_length=100)
|
||||
product_category: Optional[str] = Field(None, max_length=100)
|
||||
product_type: str = Field(default="ingredient", pattern="^(ingredient|packaging|supplies)$")
|
||||
product_type: str = Field(default="ingredient") # TODO: Create ProductType enum if needed
|
||||
required_quantity: Decimal = Field(..., gt=0)
|
||||
unit_of_measure: str = Field(..., min_length=1, max_length=50)
|
||||
safety_stock_quantity: Decimal = Field(default=Decimal("0.000"), ge=0)
|
||||
required_by_date: date
|
||||
priority: str = Field(default="normal", pattern="^(critical|high|normal|low)$")
|
||||
priority: PriorityLevel = Field(default=PriorityLevel.NORMAL)
|
||||
preferred_supplier_id: Optional[UUID] = None
|
||||
quality_specifications: Optional[Dict[str, Any]] = None
|
||||
special_requirements: Optional[str] = None
|
||||
storage_requirements: Optional[str] = Field(None, max_length=200)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class ProcurementRequirementCreate(ProcurementRequirementBase):
|
||||
pass
|
||||
@@ -248,13 +275,17 @@ class ProcurementPlanBase(BaseModel):
|
||||
plan_period_start: date
|
||||
plan_period_end: date
|
||||
planning_horizon_days: int = Field(default=14, ge=1, le=365)
|
||||
plan_type: str = Field(default="regular", pattern="^(regular|emergency|seasonal)$")
|
||||
priority: str = Field(default="normal", pattern="^(high|normal|low)$")
|
||||
business_model: Optional[str] = Field(None, pattern="^(individual_bakery|central_bakery)$")
|
||||
procurement_strategy: str = Field(default="just_in_time", pattern="^(just_in_time|bulk|mixed)$")
|
||||
plan_type: ProcurementPlanType = Field(default=ProcurementPlanType.REGULAR)
|
||||
priority: PriorityLevel = Field(default=PriorityLevel.NORMAL)
|
||||
business_model: Optional[BusinessModel] = None
|
||||
procurement_strategy: ProcurementStrategy = Field(default=ProcurementStrategy.JUST_IN_TIME)
|
||||
safety_stock_buffer: Decimal = Field(default=Decimal("20.00"), ge=0, le=100)
|
||||
special_requirements: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class ProcurementPlanCreate(ProcurementPlanBase):
|
||||
tenant_id: UUID
|
||||
|
||||
@@ -336,10 +336,10 @@ class OrdersService:
|
||||
|
||||
# Get new customers this month
|
||||
month_start = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
new_customers_this_month = await self.customer_repo.count(
|
||||
db,
|
||||
tenant_id,
|
||||
filters={"created_at": {"gte": month_start}}
|
||||
new_customers_this_month = await self.customer_repo.count_created_since(
|
||||
db,
|
||||
tenant_id,
|
||||
month_start
|
||||
)
|
||||
|
||||
# Get recent orders
|
||||
|
||||
290
services/orders/scripts/seed_test_data.py
Normal file
290
services/orders/scripts/seed_test_data.py
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to populate the database with test data for orders and customers
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
# Add the parent directory to the path to import our modules
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.database import get_session
|
||||
from app.models.customer import Customer, CustomerContact
|
||||
from app.models.order import CustomerOrder, OrderItem, OrderStatusHistory
|
||||
|
||||
# Test tenant ID - in a real environment this would be provided
|
||||
TEST_TENANT_ID = "946206b3-7446-436b-b29d-f265b28d9ff5"
|
||||
|
||||
# Sample customer data
|
||||
SAMPLE_CUSTOMERS = [
|
||||
{
|
||||
"name": "María García López",
|
||||
"customer_type": "individual",
|
||||
"email": "maria.garcia@email.com",
|
||||
"phone": "+34 612 345 678",
|
||||
"city": "Madrid",
|
||||
"country": "España",
|
||||
"customer_segment": "vip",
|
||||
"is_active": True
|
||||
},
|
||||
{
|
||||
"name": "Panadería San Juan",
|
||||
"business_name": "Panadería San Juan S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "pedidos@panaderiasjuan.com",
|
||||
"phone": "+34 687 654 321",
|
||||
"city": "Barcelona",
|
||||
"country": "España",
|
||||
"customer_segment": "wholesale",
|
||||
"is_active": True
|
||||
},
|
||||
{
|
||||
"name": "Carlos Rodríguez Martín",
|
||||
"customer_type": "individual",
|
||||
"email": "carlos.rodriguez@email.com",
|
||||
"phone": "+34 698 765 432",
|
||||
"city": "Valencia",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"is_active": True
|
||||
},
|
||||
{
|
||||
"name": "Ana Fernández Ruiz",
|
||||
"customer_type": "individual",
|
||||
"email": "ana.fernandez@email.com",
|
||||
"phone": "+34 634 567 890",
|
||||
"city": "Sevilla",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"is_active": True
|
||||
},
|
||||
{
|
||||
"name": "Café Central",
|
||||
"business_name": "Café Central Madrid S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "compras@cafecentral.es",
|
||||
"phone": "+34 623 456 789",
|
||||
"city": "Madrid",
|
||||
"country": "España",
|
||||
"customer_segment": "wholesale",
|
||||
"is_active": True
|
||||
},
|
||||
{
|
||||
"name": "Laura Martínez Silva",
|
||||
"customer_type": "individual",
|
||||
"email": "laura.martinez@email.com",
|
||||
"phone": "+34 645 789 012",
|
||||
"city": "Bilbao",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"is_active": False # Inactive customer for testing
|
||||
}
|
||||
]
|
||||
|
||||
# Sample products (in a real system these would come from a products service)
|
||||
SAMPLE_PRODUCTS = [
|
||||
{"id": str(uuid.uuid4()), "name": "Pan Integral Artesano", "price": Decimal("2.50"), "category": "Panadería"},
|
||||
{"id": str(uuid.uuid4()), "name": "Croissant de Mantequilla", "price": Decimal("1.80"), "category": "Bollería"},
|
||||
{"id": str(uuid.uuid4()), "name": "Tarta de Santiago", "price": Decimal("18.90"), "category": "Repostería"},
|
||||
{"id": str(uuid.uuid4()), "name": "Magdalenas de Limón", "price": Decimal("0.90"), "category": "Bollería"},
|
||||
{"id": str(uuid.uuid4()), "name": "Empanada de Atún", "price": Decimal("3.50"), "category": "Salado"},
|
||||
{"id": str(uuid.uuid4()), "name": "Brownie de Chocolate", "price": Decimal("3.20"), "category": "Repostería"},
|
||||
{"id": str(uuid.uuid4()), "name": "Baguette Francesa", "price": Decimal("2.80"), "category": "Panadería"},
|
||||
{"id": str(uuid.uuid4()), "name": "Palmera de Chocolate", "price": Decimal("2.40"), "category": "Bollería"},
|
||||
]
|
||||
|
||||
async def create_customers(session: AsyncSession) -> list[Customer]:
|
||||
"""Create sample customers"""
|
||||
customers = []
|
||||
|
||||
for i, customer_data in enumerate(SAMPLE_CUSTOMERS):
|
||||
customer = Customer(
|
||||
tenant_id=TEST_TENANT_ID,
|
||||
customer_code=f"CUST-{i+1:04d}",
|
||||
name=customer_data["name"],
|
||||
business_name=customer_data.get("business_name"),
|
||||
customer_type=customer_data["customer_type"],
|
||||
email=customer_data["email"],
|
||||
phone=customer_data["phone"],
|
||||
city=customer_data["city"],
|
||||
country=customer_data["country"],
|
||||
is_active=customer_data["is_active"],
|
||||
preferred_delivery_method="delivery" if random.choice([True, False]) else "pickup",
|
||||
payment_terms=random.choice(["immediate", "net_30"]),
|
||||
customer_segment=customer_data["customer_segment"],
|
||||
priority_level=random.choice(["normal", "high"]) if customer_data["customer_segment"] == "vip" else "normal",
|
||||
discount_percentage=Decimal("5.0") if customer_data["customer_segment"] == "vip" else
|
||||
Decimal("10.0") if customer_data["customer_segment"] == "wholesale" else Decimal("0.0"),
|
||||
total_orders=random.randint(5, 50),
|
||||
total_spent=Decimal(str(random.randint(100, 5000))),
|
||||
average_order_value=Decimal(str(random.randint(15, 150))),
|
||||
last_order_date=datetime.now() - timedelta(days=random.randint(1, 30))
|
||||
)
|
||||
|
||||
session.add(customer)
|
||||
customers.append(customer)
|
||||
|
||||
await session.commit()
|
||||
return customers
|
||||
|
||||
async def create_orders(session: AsyncSession, customers: list[Customer]):
|
||||
"""Create sample orders in different statuses"""
|
||||
order_statuses = [
|
||||
"pending", "confirmed", "in_production", "ready",
|
||||
"out_for_delivery", "delivered", "cancelled"
|
||||
]
|
||||
|
||||
order_types = ["standard", "rush", "recurring", "special"]
|
||||
priorities = ["low", "normal", "high"]
|
||||
delivery_methods = ["delivery", "pickup"]
|
||||
payment_statuses = ["pending", "partial", "paid", "failed"]
|
||||
|
||||
for i in range(25): # Create 25 sample orders
|
||||
customer = random.choice(customers)
|
||||
order_status = random.choice(order_statuses)
|
||||
|
||||
# Create order date in the last 30 days
|
||||
order_date = datetime.now() - timedelta(days=random.randint(0, 30))
|
||||
|
||||
# Create delivery date (1-7 days after order date)
|
||||
delivery_date = order_date + timedelta(days=random.randint(1, 7))
|
||||
|
||||
order = CustomerOrder(
|
||||
tenant_id=TEST_TENANT_ID,
|
||||
order_number=f"ORD-{datetime.now().year}-{i+1:04d}",
|
||||
customer_id=customer.id,
|
||||
status=order_status,
|
||||
order_type=random.choice(order_types),
|
||||
priority=random.choice(priorities),
|
||||
order_date=order_date,
|
||||
requested_delivery_date=delivery_date,
|
||||
confirmed_delivery_date=delivery_date if order_status not in ["pending", "cancelled"] else None,
|
||||
actual_delivery_date=delivery_date if order_status == "delivered" else None,
|
||||
delivery_method=random.choice(delivery_methods),
|
||||
delivery_instructions=random.choice([
|
||||
None, "Dejar en recepción", "Llamar al timbre", "Cuidado con el escalón"
|
||||
]),
|
||||
discount_percentage=customer.discount_percentage,
|
||||
payment_status=random.choice(payment_statuses) if order_status != "cancelled" else "failed",
|
||||
payment_method=random.choice(["cash", "card", "bank_transfer"]),
|
||||
payment_terms=customer.payment_terms,
|
||||
special_instructions=random.choice([
|
||||
None, "Sin gluten", "Decoración especial", "Entrega temprano", "Cliente VIP"
|
||||
]),
|
||||
order_source=random.choice(["manual", "online", "phone"]),
|
||||
sales_channel=random.choice(["direct", "wholesale"]),
|
||||
customer_notified_confirmed=order_status not in ["pending", "cancelled"],
|
||||
customer_notified_ready=order_status in ["ready", "out_for_delivery", "delivered"],
|
||||
customer_notified_delivered=order_status == "delivered",
|
||||
quality_score=Decimal(str(random.randint(70, 100) / 10)) if order_status == "delivered" else None,
|
||||
customer_rating=random.randint(3, 5) if order_status == "delivered" else None
|
||||
)
|
||||
|
||||
session.add(order)
|
||||
await session.flush() # Flush to get the order ID
|
||||
|
||||
# Create order items
|
||||
num_items = random.randint(1, 5)
|
||||
subtotal = Decimal("0.00")
|
||||
|
||||
for _ in range(num_items):
|
||||
product = random.choice(SAMPLE_PRODUCTS)
|
||||
quantity = random.randint(1, 10)
|
||||
unit_price = product["price"]
|
||||
line_total = unit_price * quantity
|
||||
|
||||
order_item = OrderItem(
|
||||
order_id=order.id,
|
||||
product_id=product["id"],
|
||||
product_name=product["name"],
|
||||
product_category=product["category"],
|
||||
quantity=quantity,
|
||||
unit_of_measure="unidad",
|
||||
unit_price=unit_price,
|
||||
line_discount=Decimal("0.00"),
|
||||
line_total=line_total,
|
||||
status=order_status if order_status != "cancelled" else "cancelled"
|
||||
)
|
||||
|
||||
session.add(order_item)
|
||||
subtotal += line_total
|
||||
|
||||
# Calculate financial totals
|
||||
discount_amount = subtotal * (order.discount_percentage / 100)
|
||||
tax_amount = (subtotal - discount_amount) * Decimal("0.21") # 21% VAT
|
||||
delivery_fee = Decimal("3.50") if order.delivery_method == "delivery" and subtotal < 25 else Decimal("0.00")
|
||||
total_amount = subtotal - discount_amount + tax_amount + delivery_fee
|
||||
|
||||
# Update order with calculated totals
|
||||
order.subtotal = subtotal
|
||||
order.discount_amount = discount_amount
|
||||
order.tax_amount = tax_amount
|
||||
order.delivery_fee = delivery_fee
|
||||
order.total_amount = total_amount
|
||||
|
||||
# Create status history
|
||||
status_history = OrderStatusHistory(
|
||||
order_id=order.id,
|
||||
from_status=None,
|
||||
to_status=order_status,
|
||||
event_type="status_change",
|
||||
event_description=f"Order created with status: {order_status}",
|
||||
change_source="system",
|
||||
changed_at=order_date,
|
||||
customer_notified=order_status != "pending"
|
||||
)
|
||||
|
||||
session.add(status_history)
|
||||
|
||||
# Add additional status changes for non-pending orders
|
||||
if order_status != "pending":
|
||||
current_date = order_date
|
||||
for status in ["confirmed", "in_production", "ready"]:
|
||||
if order_statuses.index(status) <= order_statuses.index(order_status):
|
||||
current_date += timedelta(hours=random.randint(2, 12))
|
||||
status_change = OrderStatusHistory(
|
||||
order_id=order.id,
|
||||
from_status="pending" if status == "confirmed" else None,
|
||||
to_status=status,
|
||||
event_type="status_change",
|
||||
event_description=f"Order status changed to: {status}",
|
||||
change_source="manual",
|
||||
changed_at=current_date,
|
||||
customer_notified=True
|
||||
)
|
||||
session.add(status_change)
|
||||
|
||||
await session.commit()
|
||||
|
||||
async def main():
|
||||
"""Main function to seed the database"""
|
||||
print("🌱 Starting database seeding...")
|
||||
|
||||
async for session in get_session():
|
||||
try:
|
||||
print("📋 Creating customers...")
|
||||
customers = await create_customers(session)
|
||||
print(f"✅ Created {len(customers)} customers")
|
||||
|
||||
print("📦 Creating orders...")
|
||||
await create_orders(session, customers)
|
||||
print("✅ Created orders with different statuses")
|
||||
|
||||
print("🎉 Database seeding completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error during seeding: {e}")
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user