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>
This commit is contained in:
384
local_backend/schemas/pricing.py
Normal file
384
local_backend/schemas/pricing.py
Normal file
@@ -0,0 +1,384 @@
|
||||
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
|
||||
Reference in New Issue
Block a user