feat(services,tasks): order placement, settlement, invoice listener

services.py
- place_order: validates against live menu, prices line items
  authoritatively from DB (modifier ids resolved server-side, not
  trusted from input), creates LNbits invoice, persists order +
  items. Order id := payment_hash for zero-metadata listener
  lookups.
- mark_order_paid: idempotent paid -> [accepted if auto-accept] +
  stock decrement + queues a print job.
- transition_order: explicit state-machine guard for accept/ready/
  complete/cancel/refund.
- quote_balance_required: pre-flight total for the webapp's
  multi-restaurant balance check (per the user's requirement to
  verify funds before opening any per-restaurant invoice).

tasks.py
- Single invoice listener filtered on extra.tag == 'restaurant',
  looks up order by payment_hash, delegates to mark_order_paid.
  Wrapped in try/except so one bad payment doesn't kill the loop.
This commit is contained in:
Padreug 2026-04-29 23:38:39 +02:00
commit 201c387722
2 changed files with 387 additions and 0 deletions

46
tasks.py Normal file
View file

@ -0,0 +1,46 @@
"""
Background tasks.
The invoice listener is the *only* place where money-moves trigger
business logic. We keep it small and idempotent: filter by
extra.tag == 'restaurant', look up the order by payment_hash, and
hand off to services.mark_order_paid().
"""
import asyncio
from loguru import logger
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from .crud import get_order_by_payment_hash
from .services import mark_order_paid
async def wait_for_paid_invoices() -> None:
invoice_queue: asyncio.Queue = asyncio.Queue()
register_invoice_listener(invoice_queue, "ext_restaurant")
while True:
payment = await invoice_queue.get()
try:
await on_invoice_paid(payment)
except Exception as ex:
logger.exception(f"[RESTAURANT] invoice listener error: {ex}")
async def on_invoice_paid(payment: Payment) -> None:
if not payment.extra or payment.extra.get("tag") != "restaurant":
return
order = await get_order_by_payment_hash(payment.payment_hash)
if not order:
# Could be an order created on a different LNbits instance, or
# a payment whose order row was already deleted. Nothing to do.
logger.debug(
f"[RESTAURANT] No order for payment {payment.payment_hash[:12]}.."
)
return
await mark_order_paid(order.id)