restaurant/views.py
Padreug c37b17d474 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.
2026-05-09 07:11:06 +02:00

135 lines
4.2 KiB
Python

"""
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),
},
)