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:
684
local_backend/routers/pricing.py
Normal file
684
local_backend/routers/pricing.py
Normal file
@@ -0,0 +1,684 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from models.pricing import (
|
||||
PriceGroup, PriceModifier, PriceModifierCondition, PriceModifierTarget,
|
||||
Deal, DealCondition, DealTarget,
|
||||
PriceEventLog, WaiterDiscountSettings,
|
||||
)
|
||||
from schemas.pricing import (
|
||||
PriceGroupCreate, PriceGroupUpdate, PriceGroupOut,
|
||||
PriceModifierCreate, PriceModifierUpdate, PriceModifierOut,
|
||||
PriceModifierReorderRequest,
|
||||
DealCreate, DealUpdate, DealOut,
|
||||
WaiterDiscountSettingsIn, WaiterDiscountSettingsOut,
|
||||
PriceEventOut,
|
||||
)
|
||||
from routers.deps import get_current_user, require_manager
|
||||
from models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRICE GROUPS
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/groups", response_model=List[PriceGroupOut])
|
||||
def list_price_groups(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
return db.query(PriceGroup).order_by(PriceGroup.name).all()
|
||||
|
||||
|
||||
@router.post("/groups", response_model=PriceGroupOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_price_group(
|
||||
body: PriceGroupCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
pg = PriceGroup(
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
color=body.color,
|
||||
is_active=int(body.is_active),
|
||||
auto_enable_time=body.auto_enable_time,
|
||||
auto_disable_time=body.auto_disable_time,
|
||||
auto_days=json.dumps(body.auto_days) if body.auto_days is not None else None,
|
||||
created_by=user.id,
|
||||
)
|
||||
db.add(pg)
|
||||
db.commit()
|
||||
db.refresh(pg)
|
||||
return pg
|
||||
|
||||
|
||||
@router.put("/groups/{group_id}", response_model=PriceGroupOut)
|
||||
def update_price_group(
|
||||
group_id: int,
|
||||
body: PriceGroupUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
pg = db.query(PriceGroup).filter(PriceGroup.id == group_id).first()
|
||||
if not pg:
|
||||
raise HTTPException(status_code=404, detail="Price group not found")
|
||||
pg.name = body.name
|
||||
pg.description = body.description
|
||||
pg.color = body.color
|
||||
pg.is_active = int(body.is_active)
|
||||
pg.auto_enable_time = body.auto_enable_time
|
||||
pg.auto_disable_time = body.auto_disable_time
|
||||
pg.auto_days = json.dumps(body.auto_days) if body.auto_days is not None else None
|
||||
db.commit()
|
||||
db.refresh(pg)
|
||||
return pg
|
||||
|
||||
|
||||
@router.patch("/groups/{group_id}/toggle", response_model=PriceGroupOut)
|
||||
def toggle_price_group(
|
||||
group_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
pg = db.query(PriceGroup).filter(PriceGroup.id == group_id).first()
|
||||
if not pg:
|
||||
raise HTTPException(status_code=404, detail="Price group not found")
|
||||
pg.is_active = 0 if pg.is_active else 1
|
||||
db.commit()
|
||||
db.refresh(pg)
|
||||
return pg
|
||||
|
||||
|
||||
@router.delete("/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_price_group(
|
||||
group_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
pg = db.query(PriceGroup).filter(PriceGroup.id == group_id).first()
|
||||
if not pg:
|
||||
raise HTTPException(status_code=404, detail="Price group not found")
|
||||
db.delete(pg)
|
||||
db.commit()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRICE MODIFIERS
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/modifiers", response_model=List[PriceModifierOut])
|
||||
def list_modifiers(
|
||||
scope: Optional[str] = None,
|
||||
item_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
q = db.query(PriceModifier)
|
||||
if scope:
|
||||
q = q.filter(PriceModifier.scope == scope)
|
||||
if item_id is not None:
|
||||
q = q.filter(PriceModifier.item_id == item_id)
|
||||
return q.order_by(PriceModifier.sort_order).all()
|
||||
|
||||
|
||||
@router.get("/modifiers/favorites", response_model=List[PriceModifierOut])
|
||||
def list_favorite_modifiers(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Returns modifiers flagged as favorites for the dashboard quick-toggle panel."""
|
||||
return (
|
||||
db.query(PriceModifier)
|
||||
.filter(PriceModifier.is_favorite == 1)
|
||||
.order_by(PriceModifier.sort_order)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
@router.post("/modifiers", response_model=PriceModifierOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_modifier(
|
||||
body: PriceModifierCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
m = PriceModifier(
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
color=body.color,
|
||||
is_active=int(body.is_active),
|
||||
is_favorite=int(body.is_favorite),
|
||||
allow_stack=int(body.allow_stack),
|
||||
sort_order=body.sort_order,
|
||||
scope=body.scope,
|
||||
item_id=body.item_id,
|
||||
action_type=body.action_type,
|
||||
action_value=body.action_value,
|
||||
round_to=body.round_to,
|
||||
created_by=user.id,
|
||||
)
|
||||
db.add(m)
|
||||
db.flush()
|
||||
_sync_conditions(m, body.conditions, db, model="modifier")
|
||||
_sync_modifier_targets(m, body.targets, db)
|
||||
db.commit()
|
||||
db.refresh(m)
|
||||
return m
|
||||
|
||||
|
||||
@router.put("/modifiers/{modifier_id}", response_model=PriceModifierOut)
|
||||
def update_modifier(
|
||||
modifier_id: int,
|
||||
body: PriceModifierUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
m = db.query(PriceModifier).filter(PriceModifier.id == modifier_id).first()
|
||||
if not m:
|
||||
raise HTTPException(status_code=404, detail="Modifier not found")
|
||||
m.name = body.name
|
||||
m.description = body.description
|
||||
m.color = body.color
|
||||
m.is_active = int(body.is_active)
|
||||
m.is_favorite = int(body.is_favorite)
|
||||
m.allow_stack = int(body.allow_stack)
|
||||
m.sort_order = body.sort_order
|
||||
m.scope = body.scope
|
||||
m.item_id = body.item_id
|
||||
m.action_type = body.action_type
|
||||
m.action_value = body.action_value
|
||||
m.round_to = body.round_to
|
||||
m.updated_at = _utcnow()
|
||||
m.updated_by = user.id
|
||||
# Replace conditions and targets
|
||||
for c in list(m.conditions):
|
||||
db.delete(c)
|
||||
for t in list(m.targets):
|
||||
db.delete(t)
|
||||
db.flush()
|
||||
_sync_conditions(m, body.conditions, db, model="modifier")
|
||||
_sync_modifier_targets(m, body.targets, db)
|
||||
db.commit()
|
||||
db.refresh(m)
|
||||
return m
|
||||
|
||||
|
||||
@router.patch("/modifiers/{modifier_id}/toggle", response_model=PriceModifierOut)
|
||||
def toggle_modifier(
|
||||
modifier_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
m = db.query(PriceModifier).filter(PriceModifier.id == modifier_id).first()
|
||||
if not m:
|
||||
raise HTTPException(status_code=404, detail="Modifier not found")
|
||||
m.is_active = 0 if m.is_active else 1
|
||||
m.updated_at = _utcnow()
|
||||
m.updated_by = user.id
|
||||
db.commit()
|
||||
db.refresh(m)
|
||||
return m
|
||||
|
||||
|
||||
@router.patch("/modifiers/reorder")
|
||||
def reorder_modifiers(
|
||||
body: PriceModifierReorderRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
for item in body.items:
|
||||
db.query(PriceModifier).filter(PriceModifier.id == item.id).update(
|
||||
{"sort_order": item.sort_order}
|
||||
)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/modifiers/{modifier_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_modifier(
|
||||
modifier_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
m = db.query(PriceModifier).filter(PriceModifier.id == modifier_id).first()
|
||||
if not m:
|
||||
raise HTTPException(status_code=404, detail="Modifier not found")
|
||||
db.delete(m)
|
||||
db.commit()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DEALS
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/deals", response_model=List[DealOut])
|
||||
def list_deals(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
return db.query(Deal).order_by(Deal.sort_order).all()
|
||||
|
||||
|
||||
@router.post("/deals", response_model=DealOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_deal(
|
||||
body: DealCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
d = Deal(
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
color=body.color,
|
||||
is_active=int(body.is_active),
|
||||
sort_order=body.sort_order,
|
||||
action_type=body.action_type,
|
||||
action_modifier_id=body.action_modifier_id,
|
||||
action_value=body.action_value,
|
||||
action_free_item_id=body.action_free_item_id,
|
||||
action_free_target_type=body.action_free_target_type,
|
||||
action_free_target_ids=(
|
||||
json.dumps(body.action_free_target_ids)
|
||||
if body.action_free_target_ids is not None else None
|
||||
),
|
||||
action_free_quantity=body.action_free_quantity,
|
||||
created_by=user.id,
|
||||
)
|
||||
db.add(d)
|
||||
db.flush()
|
||||
_sync_conditions(d, body.conditions, db, model="deal")
|
||||
_sync_deal_targets(d, body.targets, db)
|
||||
db.commit()
|
||||
db.refresh(d)
|
||||
return d
|
||||
|
||||
|
||||
@router.put("/deals/{deal_id}", response_model=DealOut)
|
||||
def update_deal(
|
||||
deal_id: int,
|
||||
body: DealUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
d = db.query(Deal).filter(Deal.id == deal_id).first()
|
||||
if not d:
|
||||
raise HTTPException(status_code=404, detail="Deal not found")
|
||||
d.name = body.name
|
||||
d.description = body.description
|
||||
d.color = body.color
|
||||
d.is_active = int(body.is_active)
|
||||
d.sort_order = body.sort_order
|
||||
d.action_type = body.action_type
|
||||
d.action_modifier_id = body.action_modifier_id
|
||||
d.action_value = body.action_value
|
||||
d.action_free_item_id = body.action_free_item_id
|
||||
d.action_free_target_type = body.action_free_target_type
|
||||
d.action_free_target_ids = (
|
||||
json.dumps(body.action_free_target_ids)
|
||||
if body.action_free_target_ids is not None else None
|
||||
)
|
||||
d.action_free_quantity = body.action_free_quantity
|
||||
for c in list(d.conditions):
|
||||
db.delete(c)
|
||||
for t in list(d.targets):
|
||||
db.delete(t)
|
||||
db.flush()
|
||||
_sync_conditions(d, body.conditions, db, model="deal")
|
||||
_sync_deal_targets(d, body.targets, db)
|
||||
db.commit()
|
||||
db.refresh(d)
|
||||
return d
|
||||
|
||||
|
||||
@router.patch("/deals/{deal_id}/toggle", response_model=DealOut)
|
||||
def toggle_deal(
|
||||
deal_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
d = db.query(Deal).filter(Deal.id == deal_id).first()
|
||||
if not d:
|
||||
raise HTTPException(status_code=404, detail="Deal not found")
|
||||
d.is_active = 0 if d.is_active else 1
|
||||
db.commit()
|
||||
db.refresh(d)
|
||||
return d
|
||||
|
||||
|
||||
@router.delete("/deals/{deal_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_deal(
|
||||
deal_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
d = db.query(Deal).filter(Deal.id == deal_id).first()
|
||||
if not d:
|
||||
raise HTTPException(status_code=404, detail="Deal not found")
|
||||
db.delete(d)
|
||||
db.commit()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WAITER DISCOUNT SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/discount-settings/global")
|
||||
def get_global_discount_settings(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
from models.settings import PosSettings
|
||||
keys = [
|
||||
"discounts.enabled",
|
||||
"discounts.max_total_value_workday",
|
||||
"discounts.max_total_value_shift",
|
||||
"discounts.max_items_per_shift",
|
||||
]
|
||||
rows = db.query(PosSettings).filter(PosSettings.key.in_(keys)).all()
|
||||
row_map = {r.key.split(".", 1)[1]: r.value for r in rows}
|
||||
# Coerce types so frontend toggle/number fields work correctly
|
||||
def _coerce(k, v):
|
||||
if v is None:
|
||||
return None
|
||||
if k == "enabled":
|
||||
return v.lower() in ("true", "1", "yes")
|
||||
try:
|
||||
return float(v) if "." in str(v) else int(v)
|
||||
except Exception:
|
||||
return v
|
||||
return {k: _coerce(k, v) for k, v in row_map.items()}
|
||||
|
||||
|
||||
@router.put("/discount-settings/global")
|
||||
def set_global_discount_settings(
|
||||
body: dict,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
from models.settings import PosSettings
|
||||
allowed = {
|
||||
"enabled", "max_total_value_workday",
|
||||
"max_total_value_shift", "max_items_per_shift",
|
||||
}
|
||||
now = _utcnow().isoformat()
|
||||
for k, v in body.items():
|
||||
if k not in allowed:
|
||||
continue
|
||||
full_key = f"discounts.{k}"
|
||||
row = db.query(PosSettings).filter(PosSettings.key == full_key).first()
|
||||
if row:
|
||||
row.value = str(v)
|
||||
row.updated_at = now
|
||||
else:
|
||||
db.add(PosSettings(key=full_key, value=str(v), updated_at=now))
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/discount-settings/me", response_model=WaiterDiscountSettingsOut)
|
||||
def get_my_discount_settings(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
s = db.query(WaiterDiscountSettings).filter(
|
||||
WaiterDiscountSettings.user_id == user.id
|
||||
).first()
|
||||
if not s:
|
||||
return WaiterDiscountSettingsOut(id=0, user_id=user.id, can_apply_discounts=False)
|
||||
return s
|
||||
|
||||
|
||||
@router.get("/discount-settings/{user_id}", response_model=WaiterDiscountSettingsOut)
|
||||
def get_waiter_discount_settings(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
s = db.query(WaiterDiscountSettings).filter(
|
||||
WaiterDiscountSettings.user_id == user_id
|
||||
).first()
|
||||
if not s:
|
||||
# Return defaults (all None = no limits, disabled)
|
||||
return WaiterDiscountSettingsOut(id=0, user_id=user_id, can_apply_discounts=False)
|
||||
return s
|
||||
|
||||
|
||||
@router.put("/discount-settings/{user_id}", response_model=WaiterDiscountSettingsOut)
|
||||
def set_waiter_discount_settings(
|
||||
user_id: int,
|
||||
body: WaiterDiscountSettingsIn,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
s = db.query(WaiterDiscountSettings).filter(
|
||||
WaiterDiscountSettings.user_id == user_id
|
||||
).first()
|
||||
if not s:
|
||||
s = WaiterDiscountSettings(user_id=user_id)
|
||||
db.add(s)
|
||||
s.can_apply_discounts = int(body.can_apply_discounts)
|
||||
s.max_discount_percent = body.max_discount_percent
|
||||
s.max_discount_amount = body.max_discount_amount
|
||||
s.max_total_value_shift = body.max_total_value_shift
|
||||
s.max_total_value_workday = body.max_total_value_workday
|
||||
s.max_items_per_shift = body.max_items_per_shift
|
||||
s.max_items_per_workday = body.max_items_per_workday
|
||||
s.max_items_per_order = body.max_items_per_order
|
||||
db.commit()
|
||||
db.refresh(s)
|
||||
return s
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRICE EVENT LOG (read-only, for order detail and reports)
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/events/order/{order_id}", response_model=List[PriceEventOut])
|
||||
def get_order_price_events(
|
||||
order_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
events = (
|
||||
db.query(PriceEventLog)
|
||||
.filter(PriceEventLog.order_id == order_id)
|
||||
.order_by(PriceEventLog.applied_at)
|
||||
.all()
|
||||
)
|
||||
result = []
|
||||
for ev in events:
|
||||
out = PriceEventOut.model_validate(ev)
|
||||
if ev.modifier:
|
||||
out.modifier_name = ev.modifier.name
|
||||
if ev.deal:
|
||||
out.deal_name = ev.deal.name
|
||||
if ev.applied_by:
|
||||
out.applied_by_username = ev.applied_by.username
|
||||
result.append(out)
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DEAL OFFER ACCEPT / DISMISS (PWA-facing)
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/deals/accept")
|
||||
def accept_deal_offer(
|
||||
body: dict,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Waiter confirms a deal offer. Logs the acceptance and directly adds free item(s)
|
||||
to the order with deal_id + linked_item_id set for binding.
|
||||
Returns added item IDs so the PWA can refresh.
|
||||
"""
|
||||
from models.pricing import PriceEventLog, Deal
|
||||
from models.order import Order, OrderItem
|
||||
from models.product import Product
|
||||
|
||||
deal_id = body.get("deal_id")
|
||||
order_id = body.get("order_id")
|
||||
# For free_choice: list of product_ids the waiter selected + their option snapshots
|
||||
# Shape: [{ product_id, quantity, selected_options, notes }] or legacy [int, ...]
|
||||
free_items_raw = body.get("free_items", [])
|
||||
# Trigger item — the order_item_id that caused the deal to fire (for linking)
|
||||
trigger_item_id = body.get("trigger_item_id", None)
|
||||
|
||||
deal = db.query(Deal).filter(Deal.id == deal_id).first()
|
||||
if not deal:
|
||||
raise HTTPException(status_code=404, detail="Deal not found")
|
||||
|
||||
order = db.query(Order).filter(Order.id == order_id).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="Order not found")
|
||||
|
||||
added_item_ids = []
|
||||
|
||||
if deal.action_type in ("free_item", "free_choice"):
|
||||
# Resolve which products to add
|
||||
if deal.action_type == "free_item" and deal.action_free_item_id:
|
||||
items_to_add = [{"product_id": deal.action_free_item_id, "quantity": deal.action_free_quantity,
|
||||
"selected_options": None, "notes": None}]
|
||||
else:
|
||||
# free_choice: caller provides list of chosen products
|
||||
items_to_add = []
|
||||
for entry in free_items_raw:
|
||||
if isinstance(entry, dict):
|
||||
items_to_add.append({
|
||||
"product_id": entry.get("product_id"),
|
||||
"quantity": entry.get("quantity", deal.action_free_quantity),
|
||||
"selected_options": entry.get("selected_options"),
|
||||
"notes": entry.get("notes"),
|
||||
})
|
||||
else:
|
||||
items_to_add.append({"product_id": int(entry), "quantity": deal.action_free_quantity,
|
||||
"selected_options": None, "notes": None})
|
||||
|
||||
for entry in items_to_add:
|
||||
product = db.query(Product).filter(Product.id == entry["product_id"]).first()
|
||||
if not product:
|
||||
continue
|
||||
base_price = product.base_price or 0.0
|
||||
new_item = OrderItem(
|
||||
order_id=order_id,
|
||||
product_id=product.id,
|
||||
added_by=user.id,
|
||||
quantity=entry["quantity"],
|
||||
unit_price=base_price, # full price so the breakdown is readable
|
||||
price_adjustment=-base_price, # negated to bring effective price to 0
|
||||
selected_options=json.dumps(entry["selected_options"]) if entry["selected_options"] else None,
|
||||
notes=entry.get("notes"),
|
||||
deal_id=deal_id,
|
||||
linked_item_id=trigger_item_id,
|
||||
)
|
||||
db.add(new_item)
|
||||
db.flush()
|
||||
added_item_ids.append(new_item.id)
|
||||
|
||||
db.add(PriceEventLog(
|
||||
order_id=order_id,
|
||||
order_item_id=new_item.id,
|
||||
event_type="free_item_added",
|
||||
deal_id=deal_id,
|
||||
price_before=product.base_price or 0.0,
|
||||
price_after=0.0,
|
||||
delta_amount=-(product.base_price or 0.0),
|
||||
applied_by_user_id=user.id,
|
||||
))
|
||||
|
||||
# Log acceptance
|
||||
db.add(PriceEventLog(
|
||||
order_id=order_id,
|
||||
order_item_id=trigger_item_id,
|
||||
event_type="deal_offer_accepted",
|
||||
deal_id=deal_id,
|
||||
applied_by_user_id=user.id,
|
||||
selected_item_ids=json.dumps([e.get("product_id") if isinstance(e, dict) else int(e) for e in free_items_raw]) if free_items_raw else None,
|
||||
))
|
||||
db.commit()
|
||||
|
||||
from services.sse_bus import broadcast_sync
|
||||
broadcast_sync()
|
||||
|
||||
return {
|
||||
"deal_id": deal_id,
|
||||
"action_type": deal.action_type,
|
||||
"added_item_ids": added_item_ids,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/deals/dismiss")
|
||||
def dismiss_deal_offer(
|
||||
body: dict,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Waiter dismisses a deal offer. Logged so it can be re-triggered later."""
|
||||
from models.pricing import PriceEventLog
|
||||
deal_id = body.get("deal_id")
|
||||
order_id = body.get("order_id")
|
||||
|
||||
db.add(PriceEventLog(
|
||||
order_id=order_id,
|
||||
order_item_id=None,
|
||||
event_type="deal_offer_dismissed",
|
||||
deal_id=deal_id,
|
||||
applied_by_user_id=user.id,
|
||||
))
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Internal helpers
|
||||
# =============================================================================
|
||||
|
||||
def _sync_conditions(parent, conditions_in, db: Session, model: str):
|
||||
"""Write condition rows for a modifier or deal."""
|
||||
for c in conditions_in:
|
||||
if model == "modifier":
|
||||
db.add(PriceModifierCondition(
|
||||
modifier_id=parent.id,
|
||||
condition_type=c.condition_type,
|
||||
params=json.dumps(c.params),
|
||||
))
|
||||
else:
|
||||
db.add(DealCondition(
|
||||
deal_id=parent.id,
|
||||
condition_type=c.condition_type,
|
||||
params=json.dumps(c.params),
|
||||
))
|
||||
|
||||
|
||||
def _sync_modifier_targets(modifier, targets_in, db: Session):
|
||||
for t in targets_in:
|
||||
db.add(PriceModifierTarget(
|
||||
modifier_id=modifier.id,
|
||||
target_type=t.target_type,
|
||||
target_id=t.target_id,
|
||||
target_tag=t.target_tag,
|
||||
target_ids=json.dumps(t.target_ids) if t.target_ids else None,
|
||||
target_tags=json.dumps(t.target_tags) if t.target_tags else None,
|
||||
))
|
||||
|
||||
|
||||
def _sync_deal_targets(deal, targets_in, db: Session):
|
||||
for t in targets_in:
|
||||
db.add(DealTarget(
|
||||
deal_id=deal.id,
|
||||
target_type=t.target_type,
|
||||
target_id=t.target_id,
|
||||
target_tag=t.target_tag,
|
||||
target_ids=json.dumps(t.target_ids) if t.target_ids else None,
|
||||
target_tags=json.dumps(t.target_tags) if t.target_tags else None,
|
||||
))
|
||||
Reference in New Issue
Block a user