| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781 |
- """
- Cliente para la API de Toteat POS.
- Expone la misma superficie pública que `fudo/fudo.py` para que
- `services/fudo_service.py`, `services/data_service.py`, los routes y los
- scripts (`load_products.py`, `update_prices.py`) puedan migrar cambiando
- solo el import.
- Diferencias principales con Fudo cubiertas en este módulo:
- - Toteat no usa flujo de OAuth; las credenciales viajan como query params
- (`xir`, `xil`, `xiu`, `xapitoken`) en cada request. `get_token()` queda
- como no-op para mantener compatibilidad.
- - `/products` devuelve todo el menú en una sola llamada (con categorías y
- modificadores embebidos). Las funciones que en Fudo eran paginadas o
- separadas se construyen sobre un cache compartido.
- - El menú se traduce a la forma JSON:API que esperan los callers de Fudo
- (`{"id", "attributes", "relationships"}`), para mantener la compatibilidad
- con `services/data_service.py:get_all` y `update_prices.py`.
- - El flujo Fudo de crear venta vacía y luego agregar items uno por uno se
- mapea a POST /orders incremental: la primera llamada a `create_item`
- para una venta crea la orden en Toteat, las siguientes la actualizan
- (Toteat permite añadir productos a una orden de mesa existente enviando
- el `orderId` real).
- """
- import os
- import time
- import asyncio
- from logging import getLogger
- from typing import Any, Dict, List, Optional
- from uuid import uuid4
- import requests
- import aiohttp
- import redis
- from config.settings import DEVELOPMENT
- from models.items import Product
- logger = getLogger(__name__)
- # ---------------------------------------------------------------------------
- # Configuración
- # ---------------------------------------------------------------------------
- API_TOKEN = os.getenv("TOTEAT_API_TOKEN", "")
- RESTAURANT_ID = os.getenv("TOTEAT_RESTAURANT_ID", "")
- LOCAL_ID = os.getenv("TOTEAT_LOCAL_ID", "")
- USER_ID = os.getenv("TOTEAT_USER_ID", "")
- # Toteat recomienda usar siempre las URLs nuevas; el sistema redirige al
- # ambiente legacy si corresponde.
- BASE_URL = "https://api.toteat.com/mw/or/1.0"
- REQUEST_TIMEOUT = 15
- # Cache del menú: Toteat trae todos los productos en una sola request, pero
- # está limitado a 3 req/min. Cacheamos por unos minutos.
- _MENU_CACHE_TTL = 60 * 5 # 5 minutos
- _menu_cache: Dict[str, Any] = {"data": None, "expires": 0.0}
- # Cache de mesas: 3 req/min también.
- _TABLES_CACHE_TTL = 60
- _tables_cache: Dict[str, Any] = {"data": None, "expires": 0.0}
- # Mapeo en memoria de número de mesa → orderId committed en Toteat. Se llena
- # cuando `create_item` crea/actualiza una orden y se consulta desde
- # `get_active_sale` para devolver el id real.
- _table_to_order: Dict[int, str] = {}
- # Buffers de venta pendientes (sale_id sintético → estado interno).
- _pending_sales: Dict[str, Dict[str, Any]] = {}
- # Mapa inverso orderId real → estado de venta. Permite que `create_item` sea
- # llamado con el orderId real de Toteat (items 2+ en una misma orden) sin
- # perder el contexto de mesa y tabla necesario para el POST /orders.
- _order_to_state: Dict[str, Dict[str, Any]] = {}
- redis_client = redis.Redis(
- host=os.getenv("REDIS_HOST", "localhost"),
- port=int(os.getenv("REDIS_PORT", 6379)),
- db=1 if DEVELOPMENT else 0,
- decode_responses=True,
- )
- REDIS_TABLE_ORDER_KEY = "toteat:table_order:{table_number}"
- # ---------------------------------------------------------------------------
- # Helpers
- # ---------------------------------------------------------------------------
- def _auth_params() -> Dict[str, str]:
- return {
- "xir": RESTAURANT_ID,
- "xil": LOCAL_ID,
- "xiu": USER_ID,
- "xapitoken": API_TOKEN,
- }
- def _get(path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
- url = f"{BASE_URL}{path}"
- full_params = {**_auth_params(), **(params or {})}
- try:
- r = requests.get(url, params=full_params, timeout=REQUEST_TIMEOUT)
- except requests.RequestException as e:
- logger.error(f"Toteat GET {path} falló: {e}")
- return None
- if r.status_code != 200:
- logger.error(f"Toteat GET {path} status={r.status_code} body={r.text[:300]}")
- return None
- try:
- return r.json()
- except ValueError:
- logger.error(f"Toteat GET {path} respuesta no-JSON: {r.text[:300]}")
- return None
- def _post(path: str, body: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
- url = f"{BASE_URL}{path}"
- full_params = {**_auth_params(), **(params or {})}
- try:
- r = requests.post(url, params=full_params, json=body, timeout=REQUEST_TIMEOUT)
- except requests.RequestException as e:
- logger.error(f"Toteat POST {path} falló: {e}")
- return None
- if r.status_code not in (200, 201):
- logger.error(f"Toteat POST {path} status={r.status_code} body={r.text[:300]}")
- return None
- try:
- return r.json()
- except ValueError:
- logger.error(f"Toteat POST {path} respuesta no-JSON: {r.text[:300]}")
- return None
- def _persist_table_order(table_number: int, order_id: str) -> None:
- _table_to_order[table_number] = order_id
- try:
- redis_client.setex(
- REDIS_TABLE_ORDER_KEY.format(table_number=table_number),
- 60 * 60 * 8, # 8h de validez para una orden abierta
- order_id,
- )
- except Exception as e:
- logger.warning(f"No se pudo persistir order_id en Redis: {e}")
- def _load_table_order(table_number: int) -> Optional[str]:
- if table_number in _table_to_order:
- return _table_to_order[table_number]
- try:
- cached = redis_client.get(REDIS_TABLE_ORDER_KEY.format(table_number=table_number))
- if cached:
- _table_to_order[table_number] = str(cached)
- return str(cached)
- except Exception as e:
- logger.warning(f"No se pudo leer order_id desde Redis: {e}")
- return None
- def _clear_table_order(table_number: int) -> None:
- _table_to_order.pop(table_number, None)
- try:
- redis_client.delete(REDIS_TABLE_ORDER_KEY.format(table_number=table_number))
- except Exception:
- pass
- # ---------------------------------------------------------------------------
- # Mapeo Toteat → forma JSON:API estilo Fudo
- # ---------------------------------------------------------------------------
- #
- # Los callers existentes (data_service.ProductDataService.get_all,
- # update_prices.add_missing_products) leen los productos como:
- # product["id"]
- # product["attributes"]["name"|"price"|"active"|"imageUrl"|"description"|"enableQrMenu"]
- # product["relationships"]["productCategory"]["data"]["id"]
- # product["relationships"]["kitchen"]["data"]["id"]
- #
- # Para mantenerlos sin cambios, traducimos el shape de Toteat al de Fudo.
- #
- # Nota sobre kitchen_id: Toteat no expone "kitchen" como Fudo. Se mapea por
- # defecto al `categoryId`. Si en el futuro se necesita enrutar items por
- # alguna dimensión distinta, agregar un mapping configurable aquí.
- def _to_fudo_product(toteat_item: Dict[str, Any]) -> Dict[str, Any]:
- images = toteat_item.get("images") or []
- image_url = ""
- if images:
- first = images[0]
- if isinstance(first, dict):
- image_url = first.get("url") or first.get("image") or ""
- elif isinstance(first, str):
- image_url = first
- category_id = toteat_item.get("categoryId")
- is_modifier = bool(toteat_item.get("isModifier"))
- return {
- "id": str(toteat_item.get("id", "")),
- "type": "Product",
- "attributes": {
- "name": toteat_item.get("name", ""),
- "price": toteat_item.get("price", 0),
- "active": True, # Toteat solo devuelve activos por defecto
- "imageUrl": image_url,
- "description": toteat_item.get("description", ""),
- "enableQrMenu": not is_modifier,
- "isModifier": is_modifier,
- "sorting": toteat_item.get("sorting", ""),
- "localCode": toteat_item.get("localCode", ""),
- "referencePrice": toteat_item.get("referencePrice"),
- "modificationDate": toteat_item.get("modificationDate"),
- },
- "relationships": {
- "productCategory": {
- "data": {
- "id": str(category_id) if category_id is not None else "0",
- "type": "ProductCategory",
- }
- },
- "kitchen": {
- "data": {
- "id": str(category_id) if category_id is not None else "0",
- "type": "Kitchen",
- }
- },
- },
- "_toteat_raw": toteat_item,
- }
- # ---------------------------------------------------------------------------
- # API pública – mismo nombre que en fudo/fudo.py
- # ---------------------------------------------------------------------------
- def get_token() -> str:
- """Toteat no usa OAuth; mantenemos la firma para compatibilidad."""
- return API_TOKEN
- def clear_token() -> None:
- """No-op para compatibilidad con fudo.fudo.clear_token."""
- return None
- def _fetch_menu(force: bool = False) -> List[Dict[str, Any]]:
- now = time.time()
- if not force and _menu_cache["data"] is not None and _menu_cache["expires"] > now:
- return _menu_cache["data"]
- payload = _get("/products", params={"activeProducts": "true"})
- if not payload or not payload.get("ok"):
- logger.error(f"Toteat /products no devolvió data útil: {payload}")
- return _menu_cache["data"] or []
- raw = payload.get("data") or []
- _menu_cache["data"] = raw
- _menu_cache["expires"] = now + _MENU_CACHE_TTL
- return raw
- def get_modifiers() -> Dict[str, Any]:
- """En Toteat los modificadores vienen embebidos en `/products`. Para
- mantener compatibilidad devolvemos un dict similar al de Fudo."""
- items = _fetch_menu()
- modifiers = [i for i in items if i.get("isModifier")]
- return {"data": [_to_fudo_product(m) for m in modifiers]}
- def get_categories() -> List[Dict[str, Any]]:
- """Devuelve las categorías derivadas del menú de Toteat en forma JSON:API."""
- items = _fetch_menu()
- seen: Dict[str, str] = {}
- for it in items:
- cat_id = it.get("categoryId")
- cat_name = it.get("category")
- if cat_id is None or cat_name is None:
- continue
- seen[str(cat_id)] = cat_name
- return [
- {
- "id": cid,
- "type": "ProductCategory",
- "attributes": {"name": cname, "enableOnlineMenu": True},
- }
- for cid, cname in seen.items()
- ]
- def get_category_dict() -> Dict[str, str]:
- return {c["id"]: c["attributes"]["name"] for c in get_categories()}
- def get_category(id_category) -> Dict[str, Any]:
- cid = str(id_category)
- for c in get_categories():
- if c["id"] == cid:
- return {
- "id": c["id"],
- "name": c["attributes"]["name"],
- "enableOnlineMenu": c["attributes"].get("enableOnlineMenu", True),
- }
- return {"id": cid, "name": "Producto", "enableOnlineMenu": False}
- def get_product(product_id) -> Optional[Product]:
- pid = str(product_id)
- for it in _fetch_menu():
- if str(it.get("id")) == pid:
- mapped = _to_fudo_product(it)
- cat_id = mapped["relationships"]["productCategory"]["data"]["id"]
- return Product(
- id=mapped["id"],
- name=mapped["attributes"]["name"],
- type=get_category(cat_id)["name"] or "Producto",
- price=int(mapped["attributes"]["price"] or 0),
- image=mapped["attributes"]["imageUrl"],
- description=mapped["attributes"]["description"],
- status=1,
- kitchen_id=mapped["relationships"]["kitchen"]["data"]["id"] or None,
- promo_day=None,
- promo_price=None,
- promo_id=None,
- )
- logger.error(f"Producto {product_id} no encontrado en menú Toteat")
- return None
- def get_products(page: int = 1) -> List[Dict[str, Any]]:
- """Toteat no pagina /products; devolvemos todo en page=1 y vacío después
- para mantener el contrato de paginación de Fudo (loops `while data`)."""
- if page != 1:
- return []
- return [_to_fudo_product(p) for p in _fetch_menu() if not p.get("isModifier")]
- async def get_all_indexed_products() -> Dict[str, Dict[str, Any]]:
- """Equivalente al método async de Fudo. Como Toteat trae todo en una
- sola request, lo hacemos en un thread para no bloquear el event loop."""
- items = await asyncio.to_thread(_fetch_menu)
- indexed: Dict[str, Dict[str, Any]] = {}
- for it in items:
- if it.get("isModifier"):
- continue
- mapped = _to_fudo_product(it)
- indexed[mapped["id"]] = mapped
- return indexed
- async def get_all_products() -> List[Dict[str, Any]]:
- return list((await get_all_indexed_products()).values())
- # ---------------------------------------------------------------------------
- # Mesas
- # ---------------------------------------------------------------------------
- def _fetch_tables(force: bool = False) -> List[Dict[str, Any]]:
- now = time.time()
- if not force and _tables_cache["data"] is not None and _tables_cache["expires"] > now:
- return _tables_cache["data"]
- payload = _get("/tables")
- if not payload or not payload.get("ok"):
- logger.error(f"Toteat /tables no devolvió data útil: {payload}")
- return _tables_cache["data"] or []
- raw = payload.get("data") or []
- _tables_cache["data"] = raw
- _tables_cache["expires"] = now + _TABLES_CACHE_TTL
- return raw
- def _to_fudo_table(toteat_table: Dict[str, Any]) -> Dict[str, Any]:
- """Toteat /tables devuelve registros con campos como `tableId`, `name`,
- `available`, `capacity`. Algunos ambientes exponen `number`. Mapeamos al
- shape JSON:API que espera el código existente."""
- table_id = toteat_table.get("tableId") or toteat_table.get("id")
- number = toteat_table.get("number")
- if number is None:
- # Algunos ambientes guardan el número como `name` (string).
- try:
- number = int(toteat_table.get("name", "0"))
- except (TypeError, ValueError):
- number = 0
- available = toteat_table.get("available", True)
- # Active sale: Toteat no la embebe en /tables. Si tenemos un orderId
- # registrado en memoria/Redis para esta mesa lo exponemos. Si la mesa
- # está marcada como ocupada pero no tenemos orderId, dejamos vacío
- # (el caller terminará creando una venta nueva).
- cached_order = _load_table_order(int(number)) if number else None
- active_sales: List[Dict[str, str]] = []
- if cached_order:
- active_sales.append({"id": cached_order, "type": "Sale"})
- elif not available:
- # Mesa ocupada pero sin orderId conocido → intentamos descubrirlo.
- discovered = _discover_open_order_for_table(table_id)
- if discovered:
- _persist_table_order(int(number), discovered)
- active_sales.append({"id": discovered, "type": "Sale"})
- return {
- "id": str(table_id) if table_id is not None else "",
- "type": "Table",
- "attributes": {
- "number": number,
- "available": available,
- "capacity": toteat_table.get("capacity"),
- "name": toteat_table.get("name"),
- },
- "relationships": {
- "activeSales": {"data": active_sales},
- },
- "_toteat_raw": toteat_table,
- }
- def _discover_open_order_for_table(table_id: Any) -> Optional[str]:
- """Busca en Toteat órdenes abiertas y filtra por tableId."""
- payload = _get("/orderstatus", params={"listing": "true"})
- if not payload or not payload.get("ok"):
- return None
- data = payload.get("data") or []
- if not isinstance(data, list):
- return None
- target = str(table_id)
- for order in data:
- otable = order.get("tableId") or order.get("table") or order.get("table_id")
- if otable is not None and str(otable) == target:
- oid = order.get("orderId") or order.get("id")
- if oid is not None:
- return str(oid)
- return None
- def get_table(number: int) -> Optional[Dict[str, Any]]:
- for raw in _fetch_tables():
- raw_number = raw.get("number")
- if raw_number is None:
- try:
- raw_number = int(raw.get("name", "0"))
- except (TypeError, ValueError):
- raw_number = None
- if raw_number == number:
- return _to_fudo_table(raw)
- return None
- def get_active_sale(table: Optional[Dict[str, Any]]) -> Optional[Dict[str, str]]:
- if not table:
- return None
- data = table.get("relationships", {}).get("activeSales", {}).get("data") or []
- if not data:
- return None
- return data[0]
- def get_sale(sale_id) -> Optional[Dict[str, Any]]:
- payload = _get("/orderstatus", params={"ic": str(sale_id), "det": "true"})
- if not payload or not payload.get("ok"):
- return None
- return payload
- # ---------------------------------------------------------------------------
- # Creación / actualización de órdenes
- # ---------------------------------------------------------------------------
- #
- # Estrategia para mantener el flujo line-by-line de Fudo:
- #
- # 1. `create_sale(table_id)` registra una venta pendiente en memoria y
- # devuelve un id sintético "pending:<uuid>". No hace HTTP todavía.
- #
- # 2. `create_item(product_id, qty, sale_id, comment)`:
- # - Si `sale_id` empieza con "pending:" y aún no hay orderId real, hace
- # POST /orders con `orderId=0` y la línea actual. Toma el orderId de
- # la respuesta (cuando está disponible) y lo guarda contra la mesa.
- # - Si ya hay orderId real, hace POST /orders con `orderId=<real>` y la
- # línea nueva (Toteat permite añadir líneas a órdenes abiertas en mesa).
- #
- # 3. `get_active_sale(table)` devuelve el orderId real si está cacheado.
- def _table_id_for_pending(sale_id: str) -> Optional[int]:
- state = _pending_sales.get(sale_id)
- if not state:
- return None
- return state.get("table_number")
- def create_sale(table_id) -> Optional[Dict[str, str]]:
- """Equivalente a `fudo.create_sale`. No hace HTTP: registra una venta
- pendiente vinculada al `table_id` (que en este módulo es el `tableId`
- de Toteat). Para resolver el número de mesa hacemos lookup inverso."""
- table_number: Optional[int] = None
- for raw in _fetch_tables():
- if str(raw.get("tableId") or raw.get("id")) == str(table_id):
- n = raw.get("number")
- if n is None:
- try:
- n = int(raw.get("name", "0"))
- except (TypeError, ValueError):
- n = None
- table_number = n
- break
- if table_number is None:
- logger.error(f"create_sale: no se pudo resolver número de mesa para tableId={table_id}")
- return None
- sale_id = f"pending:{uuid4().hex}"
- _pending_sales[sale_id] = {
- "table_id": str(table_id),
- "table_number": int(table_number),
- "items": [],
- "order_id": None,
- }
- return {"id": sale_id, "type": "Sale"}
- def _build_line(product_id: str, quantity: int, comment: Optional[str]) -> Dict[str, Any]:
- line: Dict[str, Any] = {
- "productCode": str(product_id),
- "quantity": quantity,
- }
- if comment:
- line["comment"] = comment
- return line
- def _post_order(table_id: str, lines: List[Dict[str, Any]], order_id: int = 0,
- comment: Optional[str] = None) -> Optional[Dict[str, Any]]:
- body: Dict[str, Any] = {
- "restaurantId": int(RESTAURANT_ID) if RESTAURANT_ID else 0,
- "localNumber": int(LOCAL_ID) if LOCAL_ID else 0,
- "orderId": order_id,
- "tableId": int(table_id),
- "orderReference": uuid4().hex,
- "status": "new",
- "type": "order",
- "channel": "app",
- "vendorName": "Pedidos Express",
- "comment": comment or "Pedido desde pedidos express",
- "document": {
- "line": lines,
- "payments": [],
- },
- "operationDate": time.strftime("%Y-%m-%dT%H:%M:%S"),
- "modifiedDate": time.strftime("%Y-%m-%dT%H:%M:%S"),
- }
- return _post("/orders", body, params={"orderDetail": "true"})
- def create_item(product_id: str, quantity: int, sale_id: str,
- comment: Optional[str] = None) -> Optional[Dict[str, Any]]:
- """Agrega un producto a una venta. Si la venta es pendiente (todavía no
- existe en Toteat) hace el POST /orders inicial; si ya existe le añade la
- línea. Devuelve un dict truthy con `id` para mantener compatibilidad con
- los callers de Fudo."""
- state = _pending_sales.get(sale_id) or _order_to_state.get(sale_id)
- if state is None:
- logger.error(f"create_item: sale_id desconocido {sale_id}")
- return None
- line = _build_line(product_id, quantity, comment)
- state["items"].append(line)
- table_id = state["table_id"]
- table_number = state["table_number"]
- existing_order = state["order_id"] or _load_table_order(table_number)
- order_id_int = int(existing_order) if existing_order else 0
- response = _post_order(table_id, [line], order_id=order_id_int, comment=comment)
- if response is None or not response.get("ok"):
- logger.error(f"create_item: POST /orders falló para sale_id={sale_id} body={response}")
- return None
- # Toteat puede devolver el orderId en `data.orderId` o en el msg cuando
- # el ambiente es legacy. Hacemos lookup defensivo.
- order_id_returned: Optional[str] = None
- data = response.get("data") if isinstance(response.get("data"), dict) else None
- if data:
- for key in ("orderId", "id"):
- if data.get(key):
- order_id_returned = str(data[key])
- break
- if order_id_returned and not state["order_id"]:
- state["order_id"] = order_id_returned
- _persist_table_order(table_number, order_id_returned)
- _order_to_state[order_id_returned] = state
- return {
- "id": state["order_id"] or order_id_returned or sale_id,
- "type": "Item",
- "attributes": {"quantity": quantity},
- }
- # ---------------------------------------------------------------------------
- # Lectura de items en una mesa
- # ---------------------------------------------------------------------------
- def get_table_items(table_number: int) -> Optional[List[Dict[str, Any]]]:
- """Equivalente a `fudo.get_table_items`. Resuelve la mesa, encuentra su
- orden activa y devuelve `[{id, quantity}]` para cada línea."""
- table = get_table(table_number)
- if not table:
- return None
- active = get_active_sale(table)
- if not active:
- return None
- order = get_sale(active["id"])
- if not order:
- return None
- data = order.get("data") if isinstance(order.get("data"), dict) else order.get("data")
- if not data:
- return []
- # Toteat con det=true devuelve la orden completa. Las líneas suelen estar
- # en data.document.line o data.line; nos defendemos contra ambas.
- lines: List[Dict[str, Any]] = []
- if isinstance(data, dict):
- document = data.get("document") or {}
- lines = document.get("line") or data.get("line") or []
- elif isinstance(data, list):
- lines = data
- result: List[Dict[str, Any]] = []
- for line in lines:
- try:
- pid_raw = line.get("productCode") or line.get("productId") or line.get("product_id")
- if pid_raw is None:
- continue
- qty = line.get("quantity") or 1
- result.append({"id": int(pid_raw), "quantity": int(qty)})
- except (TypeError, ValueError):
- continue
- return result
- # ---------------------------------------------------------------------------
- # Paridad async con Fudo
- # ---------------------------------------------------------------------------
- async def fetch_page(session: aiohttp.ClientSession, url_template: str,
- token: str, page_num: int):
- """Stub de paridad. Toteat no expone /products paginado; devolvemos
- listas vacías para páginas > 1 y delegamos a `_fetch_menu` para la 1."""
- if page_num != 1:
- return page_num, []
- return page_num, await asyncio.to_thread(_fetch_menu)
- __all__ = [
- "get_token",
- "clear_token",
- "get_modifiers",
- "get_categories",
- "get_category_dict",
- "get_category",
- "get_product",
- "get_products",
- "get_all_indexed_products",
- "get_all_products",
- "get_table",
- "get_active_sale",
- "get_sale",
- "create_sale",
- "create_item",
- "get_table_items",
- ]
- # ---------------------------------------------------------------------------
- # Smoke tests
- # ---------------------------------------------------------------------------
- #
- # Ejecutar con: python -m toteat.toteat
- #
- # Requiere las env vars TOTEAT_API_TOKEN, TOTEAT_RESTAURANT_ID,
- # TOTEAT_LOCAL_ID, TOTEAT_USER_ID. Solo prueba lecturas seguras por defecto;
- # el bloque que crea órdenes está comentado y requiere descomentar
- # explícitamente para no impactar el ambiente real.
- def _smoke_tests() -> None:
- from rich import print as rprint
- rprint("[bold cyan]== Toteat smoke tests ==[/bold cyan]")
- rprint(f"BASE_URL = {BASE_URL}")
- missing = [
- name for name, val in [
- ("TOTEAT_API_TOKEN", API_TOKEN),
- ("TOTEAT_RESTAURANT_ID", RESTAURANT_ID),
- ("TOTEAT_LOCAL_ID", LOCAL_ID),
- ("TOTEAT_USER_ID", USER_ID),
- ] if not val
- ]
- if missing:
- rprint(f"[redFaltan env vars:[/red] {missing}")
- return
- rprint("\n[yellow]1. get_token()[/yellow]")
- rprint(f" → token len={len(get_token())}")
- rprint("\n[yellow]2. get_categories()[/yellow]")
- cats = get_categories()
- rprint(f" → {len(cats)} categorías")
- if cats:
- rprint(f" → ejemplo: {cats[0]}")
- rprint("\n[yellow]3. get_category_dict()[/yellow]")
- cat_dict = get_category_dict()
- rprint(f" → {len(cat_dict)} entradas")
- rprint("\n[yellow]4. get_products(page=1) (primeras 3)[/yellow]")
- prods = get_products(1)
- rprint(f" → {len(prods)} productos en página 1")
- for p in prods[:3]:
- rprint(
- f" • id={p['id']} name={p['attributes']['name']!r} "
- f"price={p['attributes']['price']} cat={p['relationships']['productCategory']['data']['id']}"
- )
- rprint("\n[yellow]5. get_products(page=2) (debe ser [])[/yellow]")
- rprint(f" → {get_products(2)}")
- rprint("\n[yellow]6. get_product(<primer id>)[/yellow]")
- if prods:
- first_id = prods[0]["id"]
- product = get_product(first_id)
- rprint(f" → {product}")
- rprint("\n[yellow]7. get_all_products() async[/yellow]")
- all_prods = asyncio.run(get_all_products())
- rprint(f" → {len(all_prods)} productos totales")
- rprint("\n[yellow]8. get_all_indexed_products() async[/yellow]")
- indexed = asyncio.run(get_all_indexed_products())
- rprint(f" → {len(indexed)} entradas indexadas (claves: {list(indexed)[:3]}...)")
- rprint("\n[yellow]9. get_modifiers() (puede ser []) [/yellow]")
- rprint(f" → {len(get_modifiers().get('data', []))} modificadores")
- rprint("\n[yellow]10. get_table(106)[/yellow]")
- table = get_table(106)
- rprint(f" → {table}")
- if table:
- rprint("\n[yellow]11. get_active_sale(table)[/yellow]")
- active = get_active_sale(table)
- rprint(f" → {active}")
- if active:
- rprint("\n[yellow]12. get_sale(<active.id>)[/yellow]")
- rprint(f" → keys: {list((get_sale(active['id']) or {}).keys())}")
- rprint("\n[yellow]13. get_table_items(106)[/yellow]")
- rprint(f" → {get_table_items(106)}")
- # ------------------------------------------------------------------
- # Pruebas de escritura — DESCOMENTAR SOLO EN AMBIENTE DEV
- # ------------------------------------------------------------------
- # rprint("\n[red]14. create_sale + create_item (ESCRITURA)[/red]")
- # if table:
- # sale = create_sale(table["id"])
- # rprint(f" → create_sale → {sale}")
- # if sale and prods:
- # item = create_item(int(prods[0]["id"]), 1, sale["id"], comment="smoke test")
- # rprint(f" → create_item → {item}")
- rprint("\n[bold green]✓ smoke tests OK[/bold green]")
- #pichula pal que lee
- if __name__ == "__main__":
- from dotenv import load_dotenv
- load_dotenv(".env")
- _smoke_tests()
|