Add order page with real API calls

This commit is contained in:
Urtzi Alfaro
2025-09-19 11:44:38 +02:00
parent 447e2a5012
commit 105410c9d3
22 changed files with 2556 additions and 463 deletions

View File

@@ -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()
)

View 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"

View File

@@ -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,

View File

@@ -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

View File

@@ -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