Snapshot of in-progress work across local_backend, manager_dashboard, and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM, inventory, permissions), plus the nginx/docker-compose deploy fixes for the Unraid + NPM reverse-proxy setup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
385 lines
11 KiB
Python
385 lines
11 KiB
Python
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
|