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
|
||||
|
||||
Reference in New Issue
Block a user