feat(http): CMS pages + REST API for owners and customers

views.py (Jinja CMS pages, /restaurant/...):
  - /                   restaurant list / dashboard
  - /{slug}             menu builder
  - /{slug}/orders      order monitor
  - /{slug}/kds         kitchen display
  - /{slug}/settings    restaurant + Nostr settings

views_api.py (REST under /restaurant/api/v1/):

  Owner write-side (require_admin_key, ownership-checked):
    - restaurants CRUD (publishes kind 0 metadata to Nostr on
      create/update; signs with restaurant.nostr_pubkey override
      or LNbits Account fallback)
    - categories + subcategories CRUD
    - menu_items CRUD (publishes/replaces kind 30402 NIP-99
      listings on create/update; sends kind 5 NIP-09 deletion on
      delete)
    - modifier_groups + modifiers CRUD
    - availability_windows CRUD
    - orders status transitions (PUT /api/v1/orders/{id}/status/{new})
    - print_jobs/{id}/ack
    - settings (admin-only)

  Customer-facing (no auth, customer pubkey optional):
    - GET /api/v1/restaurants/{id}                profile
    - GET /api/v1/restaurants/{id}/menu           full menu tree
                                                  (categories +
                                                  subcategories +
                                                  items + modifiers +
                                                  availability) in
                                                  one round trip
    - POST /api/v1/orders/quote                   pre-flight balance
                                                  check; webapp calls
                                                  this *before* opening
                                                  any per-restaurant
                                                  invoice
    - POST /api/v1/orders                         place an order on
                                                  one restaurant,
                                                  returns bolt11

  KDS / order monitor (require_invoice_key, ownership-checked):
    - GET /api/v1/restaurants/{id}/orders
    - GET /api/v1/restaurants/{id}/print_jobs

crud.py: added get_print_job(job_id) helper used by the ack endpoint.
This commit is contained in:
Padreug 2026-04-29 23:44:38 +02:00
commit c37b17d474
3 changed files with 871 additions and 0 deletions

View file

@ -563,6 +563,14 @@ async def update_print_job(job: PrintJob) -> PrintJob:
return job
async def get_print_job(job_id: str) -> Optional[PrintJob]:
return await db.fetchone(
"SELECT * FROM restaurant.print_jobs WHERE id = :id",
{"id": job_id},
PrintJob,
)
async def get_print_jobs(
restaurant_id: str, status: Optional[str] = None
) -> list[PrintJob]:

135
views.py Normal file
View file

@ -0,0 +1,135 @@
"""
Server-rendered CMS routes for restaurant owners.
Mounted at `/restaurant/...`. Customer-facing pages live in the AIO
webapp (~/dev/webapp); this extension only renders the CMS.
Pages
-----
/restaurant/ dashboard (restaurant list)
/restaurant/{slug} menu builder
/restaurant/{slug}/orders order monitor
/restaurant/{slug}/kds kitchen display
/restaurant/{slug}/settings restaurant + Nostr settings
All pages require a logged-in LNbits user (check_user_exists).
"""
import json
from http import HTTPStatus
from fastapi import APIRouter, Depends, Request
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer
from .crud import get_restaurant_by_slug
from .models import Restaurant
restaurant_generic_router = APIRouter()
def restaurant_renderer():
return template_renderer(["restaurant/templates"])
def _restaurant_jsonable(restaurant: Restaurant) -> dict:
"""
Convert a Restaurant pydantic model to a plain JSON-serializable
dict for Jinja's `tojson` filter.
`restaurant.dict()` returns a dict with a `datetime` on `time`,
which Python's stdlib `JSONEncoder` (used by Jinja `tojson`) can't
serialize it errors out as
TypeError: JSONEncoder.default() missing 1 required positional argument: 'o'
Pydantic v1's `.json()` knows how to serialize datetime as
ISO-8601, so we round-trip via JSON to get a clean dict.
"""
return json.loads(restaurant.json())
@restaurant_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return restaurant_renderer().TemplateResponse(
"restaurant/index.html",
{"request": request, "user": user.json()},
)
@restaurant_generic_router.get("/{slug}", response_class=HTMLResponse)
async def menu_builder(
request: Request, slug: str, user: User = Depends(check_user_exists)
):
restaurant = await get_restaurant_by_slug(slug)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
return restaurant_renderer().TemplateResponse(
"restaurant/menu.html",
{
"request": request,
"user": user.json(),
"restaurant": _restaurant_jsonable(restaurant),
},
)
@restaurant_generic_router.get("/{slug}/orders", response_class=HTMLResponse)
async def orders(
request: Request, slug: str, user: User = Depends(check_user_exists)
):
restaurant = await get_restaurant_by_slug(slug)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
return restaurant_renderer().TemplateResponse(
"restaurant/orders.html",
{
"request": request,
"user": user.json(),
"restaurant": _restaurant_jsonable(restaurant),
},
)
@restaurant_generic_router.get("/{slug}/kds", response_class=HTMLResponse)
async def kds(
request: Request, slug: str, user: User = Depends(check_user_exists)
):
restaurant = await get_restaurant_by_slug(slug)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
return restaurant_renderer().TemplateResponse(
"restaurant/kds.html",
{
"request": request,
"user": user.json(),
"restaurant": _restaurant_jsonable(restaurant),
},
)
@restaurant_generic_router.get("/{slug}/settings", response_class=HTMLResponse)
async def settings_page(
request: Request, slug: str, user: User = Depends(check_user_exists)
):
restaurant = await get_restaurant_by_slug(slug)
if not restaurant:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
)
return restaurant_renderer().TemplateResponse(
"restaurant/settings.html",
{
"request": request,
"user": user.json(),
"restaurant": _restaurant_jsonable(restaurant),
},
)

728
views_api.py Normal file
View file

@ -0,0 +1,728 @@
"""
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 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_category,
create_menu_item,
create_modifier,
create_modifier_group,
create_restaurant,
create_subcategory,
delete_availability_window,
delete_category,
delete_menu_item,
delete_modifier,
delete_modifier_group,
delete_restaurant,
delete_subcategory,
get_availability_windows,
get_categories,
get_category,
get_menu_item,
get_menu_items,
get_modifier_groups,
get_modifiers,
get_order,
get_order_items,
get_orders,
get_print_job,
get_print_jobs,
get_restaurant,
get_restaurants,
get_settings,
get_subcategories,
update_menu_item,
update_print_job,
update_restaurant,
update_settings,
)
from .models import (
AvailabilityWindow,
Category,
CreateAvailabilityWindow,
CreateCategory,
CreateMenuItem,
CreateModifier,
CreateModifierGroup,
CreateOrder,
CreateRestaurant,
CreateSubcategory,
MenuItem,
Modifier,
ModifierGroup,
Order,
OrderInvoice,
OrderWithItems,
Restaurant,
RestaurantSettings,
Subcategory,
)
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 _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
event = build_menu_item_event(item, restaurant, pubkey)
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/{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 #
# --------------------------------------------------------------------- #
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/categories")
async def api_list_categories(restaurant_id: str) -> list[Category]:
return await get_categories(restaurant_id)
@restaurant_api_router.post("/api/v1/categories")
async def api_create_category(
data: CreateCategory,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Category:
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)
return await create_category(data)
@restaurant_api_router.delete("/api/v1/categories/{category_id}")
async def api_delete_category(
category_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
cat = await get_category(category_id)
if not cat:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Category not found."
)
restaurant = await get_restaurant(cat.restaurant_id)
if restaurant:
_require_owner(restaurant, wallet)
await delete_category(category_id)
return "", HTTPStatus.NO_CONTENT
@restaurant_api_router.get("/api/v1/categories/{category_id}/subcategories")
async def api_list_subcategories(category_id: str) -> list[Subcategory]:
return await get_subcategories(category_id)
@restaurant_api_router.post("/api/v1/subcategories")
async def api_create_subcategory(
data: CreateSubcategory,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Subcategory:
cat = await get_category(data.category_id)
if not cat:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Category not found."
)
restaurant = await get_restaurant(cat.restaurant_id)
if restaurant:
_require_owner(restaurant, wallet)
return await create_subcategory(data)
@restaurant_api_router.delete("/api/v1/subcategories/{subcategory_id}")
async def api_delete_subcategory(
subcategory_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
await delete_subcategory(subcategory_id)
return "", HTTPStatus.NO_CONTENT
# --------------------------------------------------------------------- #
# 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 full menu tree (categories,
subcategories, items, modifier groups, modifiers, availability) for
a restaurant in one round trip.
The webapp uses this once at load time, 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."
)
categories = await get_categories(restaurant_id)
items = await get_menu_items(restaurant_id)
cat_map: dict[str, dict] = {}
for cat in categories:
cat_dict = cat.dict()
cat_dict["subcategories"] = [
s.dict() for s in await get_subcategories(cat.id)
]
cat_dict["items"] = []
cat_map[cat.id] = cat_dict
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)
if item.category_id and item.category_id in cat_map:
cat_map[item.category_id]["items"].append(item_dict)
return {
"restaurant": restaurant.dict(),
"categories": list(cat_map.values()),
"items": enriched_items, # flat list; useful for search
}
@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)