Files
xenia-pos-local/local_backend/schemas/pricing.py
bonamin 34ae328b0d feat: bump client-services (accumulated feature work + deploy fixes)
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>
2026-07-19 10:00:14 +03:00

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