Commit graph

4 commits

Author SHA1 Message Date
b7fa1aec4a refactor(http): drop categories/subcategories shim
Remove the transitional layer added in commits 1+2:

models.py
  - Drop Category, Subcategory, CreateCategory, CreateSubcategory.

crud.py
  - Drop create_category / update_category / get_category /
    get_categories / delete_category and the subcategory variants
    along with the _node_row_to_category / _node_row_to_subcategory
    helpers. Tree state is owned exclusively by menu_node CRUD now.

views_api.py
  - Remove old endpoints:
      GET    /api/v1/restaurants/{id}/categories
      POST   /api/v1/categories
      DELETE /api/v1/categories/{id}
      GET    /api/v1/categories/{id}/subcategories
      POST   /api/v1/subcategories
      DELETE /api/v1/subcategories/{id}
    Hits return 404 now.
  - GET /api/v1/restaurants/{id}/menu loses the synthetic
    'categories' projection. Response is {restaurant, tree, items}.

static/js/api.js
  - Drop listCategories / createCategory / deleteCategory and the
    subcategory wrappers.

The CMS menu builder is broken between this commit and commit 4.
The plan acknowledged this trade-off: keeping commits revertible
beats the cost of an unshipped UI page rendering a stale empty
sidebar for one commit's lifetime.
2026-05-09 07:11:06 +02:00
6272df1288 feat(db,models,crud): m002 menu tree + node CRUD with shim
Replace the fixed two-level (categories + subcategories) menu shape
with an arbitrary-depth tree, capped at 4 levels. The legacy
Atitlan.io project this carries forward already used a self-FK tree;
real menus need the depth (Drinks -> Hot Beverages -> Coffee-based
-> Espressos). As a side benefit, the current CMS has no UI for
subcategories at all, so this refactor incidentally fixes that gap.

Pattern: adjacency list (parent_id self-FK) + denormalized
materialized path (TEXT, '/'-separated) + denormalized depth.
Rejected closure table (overkill at n=5..50) and Postgres ltree
(not portable to SQLite). Subtree queries become
'WHERE path LIKE :p || '%''; subtree moves are a single
SUBSTR + concat UPDATE; max-depth and cycle checks are O(1).

migrations.py
  m002_menu_tree:
    - CREATE TABLE menu_nodes (id, restaurant_id, parent_id,
      name, description, sort_order, image_url, depth, path, time)
      with indexes on (restaurant_id), (parent_id), (path).
    - Backfill depth-0 from categories; depth-1 from subcategories
      with path = parent.id || '/' || own.id.
    - ALTER menu_items ADD COLUMN node_id; backfill via
      COALESCE(subcategory_id, category_id). Index on node_id.
    - DROP subcategory_id, category_id; DROP TABLE subcategories,
      categories.

models.py
  - New MAX_MENU_DEPTH = 3 (zero-indexed; 4 levels total).
  - New MenuNodeRow (DB I/O shape) + MenuNode (extends with
    children + items for hydrated tree responses; never persisted).
  - New CreateMenuNode.
  - MenuItem.node_id is Optional (orphans allowed when a parent
    is deleted with cascade=False); CreateMenuItem.node_id is
    required (newly created items must land somewhere).
  - Category / Subcategory / Create* kept temporarily as
    transitional shim shapes for the old endpoints; dropped in
    commit 3.

crud.py
  - New: create/update/get/get_all/move/delete_menu_node and
    get_menu_tree. move_menu_node uses single-statement subtree
    rewrite (path = new_prefix || SUBSTR(path, old_len + 1)).
    Cycle check: new_parent's path must not contain node_id.
    Depth check: max descendant depth + delta_depth <= MAX_MENU_DEPTH.
    delete_menu_node default cascade=False (block on children/items);
    cascade=True detaches items (sets node_id NULL) rather than
    hard-deletes, since items carry nostr_event_ids and are
    revenue-bearing.
  - get_menu_tree fetches nodes + items in two queries and assembles
    the tree in O(n+m) Python — no recursive CTEs, identical on
    SQLite + Postgres.
  - Old create_category / get_categories / create_subcategory etc.
    rewritten as thin shims that translate to/from menu_nodes.
    Old endpoints keep working.
  - delete_restaurant cascade now deletes from menu_nodes
    (single statement) instead of categories + subcategories.

views_api.py
  - GET /restaurants/{id}/menu temporarily sources from menu_nodes
    via the shims; surfaces items only at depth-0 nodes for now
    (commit 2 replaces the whole block with a real tree response).

static/js/menu.js + templates/restaurant/menu.html
  - Rename category_id -> node_id in the item dialog payload so
    POST /menu_items satisfies the new CreateMenuItem schema.
    The CMS still renders against the depth-0 'categories'
    projection; full q-tree rewrite lands in commit 4.
2026-05-09 07:11:06 +02:00
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
5f4b416f5f feat(crud): async CRUD layer for all entities
- Restaurants: create / update / get / get_by_slug / get_by_wallets /
  get_all / delete (with ordered cascade through dependent rows)
- Categories + subcategories with cascade
- Menu items with adjust_stock helper for atomic decrement
- Modifier groups + modifiers with cascade
- Availability windows
- Orders + order items (id := payment_hash so the invoice listener
  can look up by payment_hash with zero metadata round-trip)
- Print jobs queue
- Settings (single-row config table)

JSON columns are passed through pydantic pre-validators on read so
nested models (OpenHours, lists, etc.) round-trip cleanly across
SQLite + Postgres.
2026-05-09 07:11:06 +02:00