from __future__ import annotations from typing import Optional, List, Any from pydantic import BaseModel, field_validator from datetime import datetime # --------------------------------------------------------------------------- # Price Groups # --------------------------------------------------------------------------- class PriceGroupCreate(BaseModel): name: str description: Optional[str] = None color: Optional[str] = None is_active: bool = False auto_enable_time: Optional[str] = None # "HH:MM" auto_disable_time: Optional[str] = None # "HH:MM" auto_days: Optional[List[int]] = None # [0..6] Mon=0 class PriceGroupUpdate(PriceGroupCreate): pass class PriceGroupOut(BaseModel): id: int name: str description: Optional[str] color: Optional[str] is_active: bool auto_enable_time: Optional[str] auto_disable_time: Optional[str] auto_days: Optional[List[int]] created_at: Optional[datetime] model_config = {"from_attributes": True} @field_validator("is_active", mode="before") @classmethod def coerce_bool(cls, v): return bool(v) @field_validator("auto_days", mode="before") @classmethod def parse_auto_days(cls, v): if isinstance(v, str): import json try: return json.loads(v) except Exception: return None return v # --------------------------------------------------------------------------- # Price Modifier Conditions # --------------------------------------------------------------------------- class ConditionIn(BaseModel): condition_type: str params: dict = {} class ConditionOut(BaseModel): id: int condition_type: str params: dict model_config = {"from_attributes": True} @field_validator("params", mode="before") @classmethod def parse_params(cls, v): if isinstance(v, str): import json try: return json.loads(v) except Exception: return {} return v or {} # --------------------------------------------------------------------------- # Price Modifier Targets # --------------------------------------------------------------------------- class ModifierTargetIn(BaseModel): target_type: str # 'all'|'item'|'category'|'prep_zone'|'tag' target_id: Optional[int] = None target_tag: Optional[str] = None target_ids: Optional[List[int]] = None target_tags: Optional[List[str]] = None class ModifierTargetOut(BaseModel): id: int target_type: str target_id: Optional[int] target_tag: Optional[str] target_ids: Optional[List[int]] = None target_tags: Optional[List[str]] = None model_config = {"from_attributes": True} @field_validator("target_ids", "target_tags", mode="before") @classmethod def parse_json_list(cls, v): if isinstance(v, str): import json return json.loads(v) return v # --------------------------------------------------------------------------- # Price Modifiers # --------------------------------------------------------------------------- class PriceModifierCreate(BaseModel): name: str description: Optional[str] = None color: Optional[str] = None is_active: bool = True is_favorite: bool = False allow_stack: bool = False sort_order: int = 0 scope: str = "global" # 'item' | 'global' item_id: Optional[int] = None action_type: str # 'set' | 'add_amount' | 'add_percent' action_value: float round_to: Optional[str] = None # '0.05'|'0.10'|'0.20'|'0.50'|'x.99'|'x.00' conditions: List[ConditionIn] = [] targets: List[ModifierTargetIn] = [] # only relevant for scope='global' class PriceModifierUpdate(PriceModifierCreate): pass class PriceModifierOut(BaseModel): id: int name: str description: Optional[str] color: Optional[str] is_active: bool is_favorite: bool allow_stack: bool sort_order: int scope: str item_id: Optional[int] action_type: str action_value: float round_to: Optional[str] conditions: List[ConditionOut] targets: List[ModifierTargetOut] created_at: Optional[datetime] updated_at: Optional[datetime] model_config = {"from_attributes": True} @field_validator("is_active", "is_favorite", "allow_stack", mode="before") @classmethod def coerce_bool(cls, v): return bool(v) class PriceModifierReorderItem(BaseModel): id: int sort_order: int class PriceModifierReorderRequest(BaseModel): items: List[PriceModifierReorderItem] # --------------------------------------------------------------------------- # Deals # --------------------------------------------------------------------------- class DealTargetIn(BaseModel): target_type: str # 'item'|'category'|'tag'|'any' target_id: Optional[int] = None target_tag: Optional[str] = None target_ids: Optional[List[int]] = None target_tags: Optional[List[str]] = None class DealTargetOut(BaseModel): id: int target_type: str target_id: Optional[int] target_tag: Optional[str] target_ids: Optional[List[int]] = None target_tags: Optional[List[str]] = None model_config = {"from_attributes": True} @field_validator("target_ids", "target_tags", mode="before") @classmethod def parse_json_list(cls, v): if isinstance(v, str): import json return json.loads(v) return v class DealCreate(BaseModel): name: str description: Optional[str] = None color: Optional[str] = None is_active: bool = True sort_order: int = 0 action_type: str # Action payload — which fields are set depends on action_type action_modifier_id: Optional[int] = None # for apply_modifier action_value: Optional[float] = None # for set_price / add_amount / add_percent action_free_item_id: Optional[int] = None # for free_item action_free_target_type: Optional[str] = None # for free_choice: 'item'|'category'|'tag' action_free_target_ids: Optional[List[Any]] = None # for free_choice action_free_quantity: int = 1 conditions: List[ConditionIn] = [] targets: List[DealTargetIn] = [] class DealUpdate(DealCreate): pass class DealOut(BaseModel): id: int name: str description: Optional[str] color: Optional[str] is_active: bool sort_order: int action_type: str action_modifier_id: Optional[int] action_value: Optional[float] action_free_item_id: Optional[int] action_free_target_type: Optional[str] action_free_target_ids: Optional[List[Any]] action_free_quantity: int conditions: List[ConditionOut] targets: List[DealTargetOut] created_at: Optional[datetime] model_config = {"from_attributes": True} @field_validator("is_active", mode="before") @classmethod def coerce_bool(cls, v): return bool(v) @field_validator("action_free_target_ids", mode="before") @classmethod def parse_target_ids(cls, v): if isinstance(v, str): import json try: return json.loads(v) except Exception: return None return v # --------------------------------------------------------------------------- # Waiter Discount Settings # --------------------------------------------------------------------------- class WaiterDiscountSettingsIn(BaseModel): can_apply_discounts: bool = False max_discount_percent: Optional[float] = None max_discount_amount: Optional[float] = None max_total_value_shift: Optional[float] = None max_total_value_workday: Optional[float] = None max_items_per_shift: Optional[int] = None max_items_per_workday: Optional[int] = None max_items_per_order: Optional[int] = None class WaiterDiscountSettingsOut(WaiterDiscountSettingsIn): id: int user_id: int model_config = {"from_attributes": True} @field_validator("can_apply_discounts", mode="before") @classmethod def coerce_bool(cls, v): return bool(v) # --------------------------------------------------------------------------- # Price Event Log # --------------------------------------------------------------------------- class PriceEventOut(BaseModel): id: int order_id: int order_item_id: Optional[int] event_type: str modifier_id: Optional[int] deal_id: Optional[int] price_before: Optional[float] price_after: Optional[float] delta_amount: Optional[float] applied_by_user_id: Optional[int] waiter_note: Optional[str] conditions_snapshot: Optional[dict] selected_item_ids: Optional[List[int]] applied_at: datetime # Resolved names (joined at query time) modifier_name: Optional[str] = None deal_name: Optional[str] = None applied_by_username: Optional[str] = None model_config = {"from_attributes": True} @field_validator("conditions_snapshot", mode="before") @classmethod def parse_snapshot(cls, v): if isinstance(v, str): import json try: return json.loads(v) except Exception: return None return v @field_validator("selected_item_ids", mode="before") @classmethod def parse_item_ids(cls, v): if isinstance(v, str): import json try: return json.loads(v) except Exception: return None return v # --------------------------------------------------------------------------- # Waiter discount apply (PWA-facing) # --------------------------------------------------------------------------- class ApplyDiscountRequest(BaseModel): item_ids: List[int] discount_percent: float # e.g. 10.0 = 10%; final price rounds to nearest 0.10 note: Optional[str] = None class ApplyDiscountResponse(BaseModel): applied: List[dict] # [{item_id, price_before, price_after, delta}] remaining_budget_shift: Optional[float] = None remaining_budget_workday: Optional[float] = None # --------------------------------------------------------------------------- # Deal offer response (returned on add_items when deals are newly triggered) # --------------------------------------------------------------------------- class DealOfferOut(BaseModel): deal_id: int deal_name: str action_type: str free_item_id: Optional[int] = None free_item_name: Optional[str] = None free_choices: List[dict] = [] # [{id, name}] free_quantity: int = 1 target_order_item_id: Optional[int] = None price_before: Optional[float] = None price_after: Optional[float] = None # --------------------------------------------------------------------------- # Deal offer accept (waiter confirms a deal) # --------------------------------------------------------------------------- class AcceptDealOfferRequest(BaseModel): deal_id: int selected_product_ids: Optional[List[int]] = None # for free_choice class DismissDealOfferRequest(BaseModel): deal_id: int