restaurant/views_api.py
Padreug 6dae57f3f4 feat(api): public GET /restaurants/by-slug/{slug}
Prerequisite for the customer webapp module (aiolabs/webapp,
branch feat/restaurant-bundle): the webapp's /r/:slug route needs
to resolve a slug to a Restaurant payload without an admin key.

crud.get_restaurant_by_slug already exists (used by the server-
rendered CMS routes in views.py); just expose it as a public REST
endpoint. Mirrors api_get_restaurant by id and is declared before
the bare-id route so the static prefix wins FastAPI's path match.

Verified live against seeded 'Big Jay's Bustaurant':
  GET /restaurant/api/v1/restaurants/by-slug/big-jays-bustaurant
  -> 200 with the Restaurant payload.
2026-05-11 19:17:35 +02:00

858 lines
28 KiB
Python

"""
REST API for the restaurant extension.
Two audiences:
* **CMS (restaurant owner)** — write-side endpoints, gated by
require_admin_key. Restaurants, categories, menu items, modifier
groups + modifiers, availability windows, settings.
* **Customer (webapp)** — read-side endpoints (public menu) and
order placement (no auth, customer pubkey optional).
All write endpoints fan out to nostr_publisher when nostr is enabled
in settings, so menu updates propagate to subscribed clients in
real time.
"""
from http import HTTPStatus
from typing import Optional
from fastapi import APIRouter, Depends, Query
from loguru import logger
from pydantic import BaseModel
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.crud.users import get_account
from lnbits.core.crud.wallets import get_wallet
from lnbits.core.models import Account, WalletTypeInfo
from lnbits.decorators import (
check_admin,
check_user_exists,
require_admin_key,
require_invoice_key,
)
from .crud import (
create_availability_window,
create_menu_item,
create_menu_node,
create_modifier,
create_modifier_group,
create_restaurant,
delete_availability_window,
delete_menu_item,
delete_menu_node,
delete_modifier,
delete_modifier_group,
delete_restaurant,
get_availability_windows,
get_menu_item,
get_menu_items,
get_menu_node,
get_menu_nodes,
get_menu_tree,
get_modifier_groups,
get_modifiers,
get_order,
get_order_items,
get_orders,
get_print_job,
get_print_jobs,
get_restaurant,
get_restaurant_by_slug,
get_restaurants,
get_settings,
move_menu_node,
update_menu_item,
update_menu_node,
update_print_job,
update_restaurant,
update_settings,
)
from .models import (
AvailabilityWindow,
CreateAvailabilityWindow,
CreateMenuItem,
CreateMenuNode,
CreateModifier,
CreateModifierGroup,
CreateOrder,
CreateRestaurant,
MenuItem,
MenuNodeRow,
Modifier,
ModifierGroup,
Order,
OrderInvoice,
OrderWithItems,
Restaurant,
RestaurantSettings,
)
from .nostr_publisher import (
build_delete_event,
build_menu_item_event,
build_restaurant_metadata_event,
publish_event,
)
from .services import (
place_order,
quote_balance_required,
transition_order,
)
restaurant_api_router = APIRouter()
# --------------------------------------------------------------------- #
# Helpers #
# --------------------------------------------------------------------- #
async def _resolve_signing_keypair(
restaurant: Restaurant,
) -> Optional[tuple[str, str]]:
"""
Resolve the (pubkey, prvkey) pair for signing Nostr events on behalf
of a restaurant.
Order of precedence:
1. restaurant.nostr_pubkey is set → use a per-restaurant key.
(Storage of the corresponding prvkey is intentionally out of
scope here; for now this branch is a no-op until we ship a
secret-management approach. Returns None.)
2. Otherwise → fall back to the LNbits Account keypair of the
wallet owner.
"""
if restaurant.nostr_pubkey:
# TODO: per-restaurant secret key vault.
return None
wallet_obj = await get_wallet(restaurant.wallet)
if not wallet_obj:
return None
account = await get_account(wallet_obj.user)
if not account or not account.pubkey or not account.prvkey:
return None
return account.pubkey, account.prvkey
async def _publish_restaurant(restaurant: Restaurant) -> None:
settings = await get_settings()
if not settings.nostr_publish_enabled:
return
keypair = await _resolve_signing_keypair(restaurant)
if not keypair:
return
pubkey, prvkey = keypair
from . import nostr_client
event = build_restaurant_metadata_event(restaurant, pubkey)
published = await publish_event(nostr_client, event, prvkey)
if published:
restaurant.nostr_event_id = published.id
restaurant.nostr_event_created_at = published.created_at
if not restaurant.nostr_pubkey:
# Echo back the resolved pubkey so the row carries it for
# discovery (e.g. webapp follows this pubkey).
restaurant.nostr_pubkey = pubkey
await update_restaurant(restaurant)
async def _ancestor_names_for_node(node_id: Optional[str]) -> tuple[str, ...]:
"""
Walk the materialized `path` of a node, returning the chain of
node names root-first (including the leaf node itself).
Returns () if node_id is None or path can't be resolved.
"""
if not node_id:
return ()
leaf = await get_menu_node(node_id)
if not leaf:
return ()
ancestor_ids = leaf.path.split("/")
if not ancestor_ids:
return ()
# One round-trip per node — at most MAX_MENU_DEPTH+1 calls (≤4).
names: list[str] = []
for nid in ancestor_ids:
n = await get_menu_node(nid)
if n:
names.append(n.name)
return tuple(names)
async def _publish_menu_item(item: MenuItem) -> None:
settings = await get_settings()
if not settings.nostr_publish_enabled:
return
restaurant = await get_restaurant(item.restaurant_id)
if not restaurant:
return
keypair = await _resolve_signing_keypair(restaurant)
if not keypair:
return
pubkey, prvkey = keypair
from . import nostr_client
ancestors = await _ancestor_names_for_node(item.node_id)
event = build_menu_item_event(
item, restaurant, pubkey, ancestor_names=ancestors
)
published = await publish_event(nostr_client, event, prvkey)
if published:
item.nostr_event_id = published.id
item.nostr_event_created_at = published.created_at
await update_menu_item(item)
async def _publish_menu_item_delete(item: MenuItem) -> None:
settings = await get_settings()
if not settings.nostr_publish_enabled or not item.nostr_event_id:
return
restaurant = await get_restaurant(item.restaurant_id)
if not restaurant:
return
keypair = await _resolve_signing_keypair(restaurant)
if not keypair:
return
pubkey, prvkey = keypair
from . import nostr_client
event = build_delete_event(30402, item.id, pubkey, "Menu item removed")
await publish_event(nostr_client, event, prvkey)
def _require_owner(restaurant: Restaurant, wallet: WalletTypeInfo) -> None:
if restaurant.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Not your restaurant.",
)
# --------------------------------------------------------------------- #
# Restaurants #
# --------------------------------------------------------------------- #
@restaurant_api_router.get("/api/v1/restaurants")
async def api_list_restaurants(
all_wallets: bool = Query(False),
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> list[Restaurant]:
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
return await get_restaurants(wallet_ids)
@restaurant_api_router.get("/api/v1/restaurants/by-slug/{slug}")
async def api_get_restaurant_by_slug(slug: str) -> Restaurant:
"""Public — used by the customer webapp to resolve a URL slug
(e.g. /r/big-jays-bustaurant) to a restaurant. Mirrors
api_get_restaurant; declared *before* the bare-id route so the
static prefix wins the path match in FastAPI's router."""
restaurant = await get_restaurant_by_slug(slug)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
return restaurant
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}")
async def api_get_restaurant(restaurant_id: str) -> Restaurant:
"""Public — used by the webapp to fetch profile metadata."""
restaurant = await get_restaurant(restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
return restaurant
@restaurant_api_router.post("/api/v1/restaurants")
async def api_create_restaurant(
data: CreateRestaurant,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Restaurant:
if not data.wallet:
data.wallet = wallet.wallet.id
restaurant = await create_restaurant(wallet=data.wallet, data=data)
await _publish_restaurant(restaurant)
return restaurant
@restaurant_api_router.put("/api/v1/restaurants/{restaurant_id}")
async def api_update_restaurant(
restaurant_id: str,
data: CreateRestaurant,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Restaurant:
restaurant = await get_restaurant(restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
_require_owner(restaurant, wallet)
for k, v in data.dict().items():
if k == "wallet":
continue # never reassign wallet via update
setattr(restaurant, k, v)
restaurant = await update_restaurant(restaurant)
await _publish_restaurant(restaurant)
return restaurant
@restaurant_api_router.delete("/api/v1/restaurants/{restaurant_id}")
async def api_delete_restaurant(
restaurant_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
restaurant = await get_restaurant(restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
_require_owner(restaurant, wallet)
await delete_restaurant(restaurant_id)
return "", HTTPStatus.NO_CONTENT
# --------------------------------------------------------------------- #
# Categories + subcategories #
# --------------------------------------------------------------------- #
# --------------------------------------------------------------------- #
# Menu nodes (the tree) #
# --------------------------------------------------------------------- #
class _MoveNodeRequest(BaseModel):
"""Body for PUT /menu_nodes/{id}/move."""
new_parent_id: Optional[str] = None
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/menu_nodes")
async def api_list_menu_nodes(restaurant_id: str) -> list[MenuNodeRow]:
"""Flat list of all nodes for a restaurant — useful for parent
pickers and admin tooling. The hydrated tree is on
`/api/v1/restaurants/{id}/menu`."""
return await get_menu_nodes(restaurant_id)
@restaurant_api_router.get("/api/v1/menu_nodes/{node_id}")
async def api_get_menu_node(node_id: str) -> MenuNodeRow:
node = await get_menu_node(node_id)
if not node:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Node not found."
)
return node
@restaurant_api_router.post("/api/v1/menu_nodes")
async def api_create_menu_node(
data: CreateMenuNode,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MenuNodeRow:
restaurant = await get_restaurant(data.restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
_require_owner(restaurant, wallet)
try:
node = await create_menu_node(data)
except ValueError as ve:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(ve)
) from ve
return MenuNodeRow(**node.dict(exclude={"children", "items"}))
@restaurant_api_router.put("/api/v1/menu_nodes/{node_id}")
async def api_update_menu_node(
node_id: str,
data: CreateMenuNode,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MenuNodeRow:
"""Update editable fields (name, description, sort_order, image_url).
Tree position changes go through PUT /menu_nodes/{id}/move.
A name change triggers re-publishing every item in the subtree
so their NIP-99 listings carry the new ancestor `t` tag. ≤50
items per restaurant in practice — eager re-publish is cheap.
"""
node = await get_menu_node(node_id)
if not node:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Node not found."
)
restaurant = await get_restaurant(node.restaurant_id)
if restaurant:
_require_owner(restaurant, wallet)
name_changed = node.name != data.name
node.name = data.name
node.description = data.description
node.sort_order = data.sort_order
node.image_url = data.image_url
updated = await update_menu_node(node)
if name_changed:
await _republish_subtree_items(node_id)
return updated
async def _republish_subtree_items(node_id: str) -> None:
"""Re-publish every menu item under the given node's subtree,
so its kind-30402 events carry the updated ancestor `t` tag set."""
from .crud import db
rows = await db.fetchall(
"""
SELECT mi.* FROM restaurant.menu_items mi
JOIN restaurant.menu_nodes mn ON mn.id = mi.node_id
WHERE mn.path = (SELECT path FROM restaurant.menu_nodes WHERE id = :nid)
OR mn.path LIKE (SELECT path FROM restaurant.menu_nodes WHERE id = :nid) || '/%'
""",
{"nid": node_id},
model=MenuItem,
)
for it in rows:
try:
await _publish_menu_item(it)
except Exception as ex:
logger.warning(
f"[RESTAURANT] re-publish failed for item {it.id[:12]}..: {ex}"
)
@restaurant_api_router.put("/api/v1/menu_nodes/{node_id}/move")
async def api_move_menu_node(
node_id: str,
body: _MoveNodeRequest,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MenuNodeRow:
node = await get_menu_node(node_id)
if not node:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Node not found."
)
restaurant = await get_restaurant(node.restaurant_id)
if restaurant:
_require_owner(restaurant, wallet)
try:
return await move_menu_node(node_id, body.new_parent_id)
except ValueError as ve:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(ve)
) from ve
@restaurant_api_router.delete("/api/v1/menu_nodes/{node_id}")
async def api_delete_menu_node(
node_id: str,
cascade: bool = Query(default=False),
wallet: WalletTypeInfo = Depends(require_admin_key),
):
node = await get_menu_node(node_id)
if not node:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Node not found."
)
restaurant = await get_restaurant(node.restaurant_id)
if restaurant:
_require_owner(restaurant, wallet)
try:
await delete_menu_node(node_id, cascade=cascade)
except ValueError as ve:
# 409 reads more naturally than 400 for "blocked by children/items".
raise HTTPException(
status_code=HTTPStatus.CONFLICT, detail=str(ve)
) from ve
return "", HTTPStatus.NO_CONTENT
# --------------------------------------------------------------------- #
# Categories / subcategories — transitional shim (drop in commit 3) #
# --------------------------------------------------------------------- #
# --------------------------------------------------------------------- #
# Menu items #
# --------------------------------------------------------------------- #
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/menu")
async def api_get_menu(restaurant_id: str) -> dict:
"""
Public composite endpoint: returns the menu in two shapes in one
round trip.
* ``tree`` — the full hydrated tree (root nodes with nested
children + items, depth, path). Each item is the
bare MenuItem (no modifier hydration).
* ``items`` — flat enriched list (modifier groups, modifier
options, availability windows attached); useful
for search / filter and for hydrating the items
referenced from ``tree``.
The webapp loads this once and then trusts Nostr events for
incremental updates.
"""
restaurant = await get_restaurant(restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
# Hydrated tree — nodes with children + items already attached.
tree = await get_menu_tree(restaurant_id)
# Flat enriched items list (used by search-style consumers).
items = await get_menu_items(restaurant_id)
enriched_items: list[dict] = []
for item in items:
item_dict = item.dict()
item_dict["modifier_groups"] = []
for grp in await get_modifier_groups(item.id):
grp_dict = grp.dict()
grp_dict["modifiers"] = [m.dict() for m in await get_modifiers(grp.id)]
item_dict["modifier_groups"].append(grp_dict)
item_dict["availability_windows"] = [
w.dict() for w in await get_availability_windows(item.id)
]
enriched_items.append(item_dict)
return {
"restaurant": restaurant.dict(),
"tree": [t.dict() for t in tree],
"items": enriched_items,
}
@restaurant_api_router.get("/api/v1/menu_items/{item_id}")
async def api_get_menu_item(item_id: str) -> MenuItem:
item = await get_menu_item(item_id)
if not item:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Menu item not found."
)
return item
@restaurant_api_router.post("/api/v1/menu_items")
async def api_create_menu_item(
data: CreateMenuItem,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MenuItem:
restaurant = await get_restaurant(data.restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
_require_owner(restaurant, wallet)
item = await create_menu_item(data)
await _publish_menu_item(item)
return item
@restaurant_api_router.put("/api/v1/menu_items/{item_id}")
async def api_update_menu_item(
item_id: str,
data: CreateMenuItem,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MenuItem:
item = await get_menu_item(item_id)
if not item:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Menu item not found."
)
restaurant = await get_restaurant(item.restaurant_id)
if restaurant:
_require_owner(restaurant, wallet)
for k, v in data.dict().items():
if k == "restaurant_id":
continue # immutable
setattr(item, k, v)
item = await update_menu_item(item)
await _publish_menu_item(item) # re-publish (kind 30402 is replaceable)
return item
@restaurant_api_router.delete("/api/v1/menu_items/{item_id}")
async def api_delete_menu_item(
item_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
item = await get_menu_item(item_id)
if not item:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Menu item not found."
)
restaurant = await get_restaurant(item.restaurant_id)
if restaurant:
_require_owner(restaurant, wallet)
await _publish_menu_item_delete(item)
await delete_menu_item(item_id)
return "", HTTPStatus.NO_CONTENT
# --------------------------------------------------------------------- #
# Modifier groups + modifiers #
# --------------------------------------------------------------------- #
@restaurant_api_router.get("/api/v1/menu_items/{item_id}/modifier_groups")
async def api_list_modifier_groups(item_id: str) -> list[ModifierGroup]:
return await get_modifier_groups(item_id)
@restaurant_api_router.post("/api/v1/modifier_groups")
async def api_create_modifier_group(
data: CreateModifierGroup,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> ModifierGroup:
return await create_modifier_group(data)
@restaurant_api_router.delete("/api/v1/modifier_groups/{group_id}")
async def api_delete_modifier_group(
group_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
await delete_modifier_group(group_id)
return "", HTTPStatus.NO_CONTENT
@restaurant_api_router.get("/api/v1/modifier_groups/{group_id}/modifiers")
async def api_list_modifiers(group_id: str) -> list[Modifier]:
return await get_modifiers(group_id)
@restaurant_api_router.post("/api/v1/modifiers")
async def api_create_modifier(
data: CreateModifier,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Modifier:
return await create_modifier(data)
@restaurant_api_router.delete("/api/v1/modifiers/{modifier_id}")
async def api_delete_modifier(
modifier_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
await delete_modifier(modifier_id)
return "", HTTPStatus.NO_CONTENT
# --------------------------------------------------------------------- #
# Availability windows #
# --------------------------------------------------------------------- #
@restaurant_api_router.get(
"/api/v1/menu_items/{item_id}/availability_windows"
)
async def api_list_availability_windows(item_id: str) -> list[AvailabilityWindow]:
return await get_availability_windows(item_id)
@restaurant_api_router.post("/api/v1/availability_windows")
async def api_create_availability_window(
data: CreateAvailabilityWindow,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> AvailabilityWindow:
return await create_availability_window(data)
@restaurant_api_router.delete("/api/v1/availability_windows/{window_id}")
async def api_delete_availability_window(
window_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
await delete_availability_window(window_id)
return "", HTTPStatus.NO_CONTENT
# --------------------------------------------------------------------- #
# Orders (customer-facing + KDS) #
# --------------------------------------------------------------------- #
@restaurant_api_router.post("/api/v1/orders/quote")
async def api_quote(items: list[dict]) -> dict:
"""
Customer pre-flight: returns the total msat needed to pay this set
of line items. The webapp calls /quote *before* posting one order
per restaurant, so a customer with insufficient funds gets a single
clear error rather than partially paid orders.
"""
from .models import CreateOrderItem, SelectedModifier
parsed = [
CreateOrderItem(
menu_item_id=i["menu_item_id"],
quantity=int(i.get("quantity", 1)),
selected_modifiers=[
SelectedModifier(**m) for m in i.get("selected_modifiers", [])
],
note=i.get("note"),
)
for i in items
]
return {"required_msat": await quote_balance_required(parsed)}
@restaurant_api_router.post("/api/v1/orders")
async def api_create_order(data: CreateOrder) -> dict:
"""
Customer-facing — creates an order on a single restaurant and
returns the bolt11 to pay. The webapp posts N of these in parallel
(one per restaurant in the cart), having already pre-flighted with
/quote.
"""
try:
order, invoice = await place_order(data)
except ValueError as ve:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(ve)
) from ve
except Exception as ex:
logger.exception("[RESTAURANT] place_order failed")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
) from ex
return {"order": order.dict(), "invoice": invoice.dict() if invoice else None}
@restaurant_api_router.get("/api/v1/orders/{order_id}")
async def api_get_order(order_id: str) -> OrderWithItems:
order = await get_order(order_id)
if not order:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Order not found."
)
items = await get_order_items(order_id)
return OrderWithItems(order=order, items=items)
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/orders")
async def api_list_orders(
restaurant_id: str,
statuses: Optional[list[str]] = Query(default=None),
limit: int = Query(default=200, le=1000),
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> list[Order]:
"""KDS / order-monitor data source. Owner-only."""
restaurant = await get_restaurant(restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
if restaurant.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your restaurant."
)
return await get_orders(restaurant_id, statuses=statuses, limit=limit)
@restaurant_api_router.put("/api/v1/orders/{order_id}/status/{new_status}")
async def api_transition_order(
order_id: str,
new_status: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Order:
order = await get_order(order_id)
if not order:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Order not found."
)
if order.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your order."
)
try:
updated = await transition_order(order_id, new_status)
except ValueError as ve:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(ve)
) from ve
assert updated # not None — we just checked the order exists
return updated
# --------------------------------------------------------------------- #
# Print jobs #
# --------------------------------------------------------------------- #
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/print_jobs")
async def api_list_print_jobs(
restaurant_id: str,
status: Optional[str] = None,
wallet: WalletTypeInfo = Depends(require_invoice_key),
):
restaurant = await get_restaurant(restaurant_id)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
if restaurant.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your restaurant."
)
return await get_print_jobs(restaurant_id, status=status)
@restaurant_api_router.put("/api/v1/print_jobs/{job_id}/ack")
async def api_ack_print_job(
job_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Called by printer-pi after a successful print to mark the job done."""
from datetime import datetime, timezone
job = await get_print_job(job_id)
if not job:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Print job not found."
)
restaurant = await get_restaurant(job.restaurant_id)
if not restaurant or restaurant.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your print job."
)
job.status = "acknowledged"
job.acknowledged_at = datetime.now(timezone.utc)
await update_print_job(job)
return job
# --------------------------------------------------------------------- #
# Settings #
# --------------------------------------------------------------------- #
@restaurant_api_router.get("/api/v1/settings")
async def api_get_settings(
admin: Account = Depends(check_admin),
) -> RestaurantSettings:
return await get_settings()
@restaurant_api_router.put("/api/v1/settings")
async def api_update_settings(
data: RestaurantSettings,
admin: Account = Depends(check_admin),
) -> RestaurantSettings:
return await update_settings(data)