fudo.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import math
  2. from time import time
  3. import requests
  4. from config.settings import DEVELOPMENT
  5. import os
  6. import redis
  7. from logging import getLogger
  8. from models.items import Product
  9. logger = getLogger(__name__)
  10. api_token = os.getenv('FUDO_API_KEY')
  11. api_secret = os.getenv('FUDO_API_SECRET')
  12. token = None
  13. token_exp = None
  14. # Configuración de Redis
  15. redis_client = redis.Redis(
  16. host=os.getenv('REDIS_HOST', 'localhost'),
  17. port=int(os.getenv('REDIS_PORT', 6379)),
  18. db=1 if DEVELOPMENT else 0,
  19. decode_responses=True
  20. )
  21. REDIS_TOKEN_KEY = 'fudo_api_token'
  22. def get_token():
  23. """
  24. Obtiene el token de autenticación de Fudo API.
  25. Primero verifica si existe un token válido en Redis.
  26. Si no existe o ha expirado, solicita uno nuevo y lo guarda en Redis con expiración automática.
  27. """
  28. global token, token_exp
  29. # Intento de obtener el token desde la RAM
  30. if token and token_exp and time() < token_exp:
  31. return token
  32. try:
  33. # Intentar obtener el token desde Redis
  34. cached_token = redis_client.get(REDIS_TOKEN_KEY)
  35. if cached_token:
  36. token = cached_token
  37. ttl = redis_client.ttl(REDIS_TOKEN_KEY)
  38. if ttl is None or int(str(ttl)) < 0:
  39. token_exp = None
  40. else:
  41. token_exp = int(str(ttl)) + int(time())
  42. return str(cached_token)
  43. except Exception as e:
  44. logger.error(f"Error al conectar con Redis: {e}")
  45. logger.info("Fallback: obteniendo token sin cache")
  46. # Si no hay token en cache, solicitar uno nuevo
  47. url = 'https://auth.fu.do/api'
  48. data = {
  49. "apiKey": api_token,
  50. "apiSecret": api_secret
  51. }
  52. r = requests.post(url, data=data)
  53. response_data = r.json()
  54. token = response_data['token']
  55. expiration_timestamp = response_data['exp']
  56. # Calcular TTL en segundos para Redis
  57. current_time = int(time())
  58. ttl_seconds = expiration_timestamp - current_time
  59. try:
  60. # Guardar el token en Redis con expiración automática
  61. if ttl_seconds > 0:
  62. redis_client.setex(REDIS_TOKEN_KEY, ttl_seconds, token)
  63. else:
  64. logger.warning("Warning: El token ya está expirado")
  65. except Exception as e:
  66. logger.error(f"Error al guardar en Redis: {e}")
  67. return token
  68. def get_categories():
  69. token = get_token()
  70. url = 'https://api.fu.do/v1alpha1/product-categories'
  71. headers = {
  72. 'Authorization': 'Bearer ' + token
  73. }
  74. r = requests.get(url, headers=headers)
  75. return r.json()
  76. def get_category(id_category:int):
  77. token = get_token()
  78. url = 'https://api.fu.do/v1alpha1/product-categories/{}'.format(id_category)
  79. headers = {
  80. 'Authorization': 'Bearer ' + token
  81. }
  82. r = requests.get(url, headers=headers)
  83. if r.status_code != 200:
  84. logger.error(f"Error al obtener producto: {r.json()['errors']}")
  85. data = r.json()["data"]
  86. return {
  87. "id": data["id"],
  88. "name": data["attributes"]["name"],
  89. "enableOnlineMenu": data["attributes"].get("enableOnlineMenu", False)
  90. }
  91. def get_product(id_category:int):
  92. url = 'https://api.fu.do/v1alpha1/products/{}'.format(id_category)
  93. token = get_token()
  94. headers = {
  95. 'Authorization': 'Bearer ' + token
  96. }
  97. r = requests.get(url, headers=headers)
  98. if r.status_code != 200:
  99. logger.error(f"Error al obtener producto: {r.json()['errors']}")
  100. data = r.json().get("data")
  101. if not data:
  102. return None
  103. return Product(
  104. id=int(data["id"]),
  105. name=data["attributes"]["name"],
  106. type=get_category(data["relationships"]["productCategory"]["data"]["id"])["name"] or "Producto",
  107. price=data["attributes"]["price"],
  108. image=data["attributes"]["imageUrl"],
  109. description=data["attributes"]["description"],
  110. status=1 if data["attributes"]["active"] and data else 0,
  111. promo_day=None,
  112. promo_price=None,
  113. promo_id=None
  114. )
  115. def get_products(page: int = 1):
  116. url = 'https://api.fu.do/v1alpha1/products?page[number]={}'.format(page)
  117. token = get_token()
  118. headers = {
  119. 'Authorization': 'Bearer ' + token
  120. }
  121. r = requests.get(url, headers=headers)
  122. return r.json()['data']
  123. def get_all_products():
  124. """Método para obtener todos los productos de la base de datos.
  125. Returns:
  126. Diccionario de productos con IDs como claves y datos de productos como valores.
  127. """
  128. url = 'https://api.fu.do/v1alpha1/products?page[number]={}'
  129. products = {}
  130. token = get_token()
  131. page = 1
  132. while True:
  133. r = requests.get(url.format(page), headers={'Authorization': 'Bearer ' + token})
  134. if r.status_code != 200:
  135. if products:
  136. return products
  137. else:
  138. return None
  139. data = r.json().get('data')
  140. if not data:
  141. return products
  142. for product in data:
  143. products[product['id']] = product
  144. page += 1
  145. N_PER_PAGE = 100
  146. def _get_page_bounds(page: int, token: str):
  147. """
  148. Función helper: Obtiene una página y devuelve el primer y último
  149. número de mesa en ella, y los datos.
  150. """
  151. url = (
  152. 'https://api.fu.do/v1alpha1/tables'
  153. f'?page[number]={page}&page[size]={N_PER_PAGE}'
  154. '&include=activeSales&sort=number'
  155. )
  156. headers = {'Authorization': 'Bearer ' + token}
  157. try:
  158. r = requests.get(url, headers=headers, timeout=10)
  159. if r.status_code != 200:
  160. return None, None, None # Error de API
  161. data = r.json().get('data', [])
  162. if not data:
  163. return 0, 0, [] # Página vacía
  164. first_number = data[0]['attributes']['number']
  165. last_number = data[-1]['attributes']['number']
  166. return first_number, last_number, data
  167. except requests.RequestException as e:
  168. print(f"Error de request en página {page}: {e}")
  169. return None, None, None
  170. def get_table(number: int):
  171. token = get_token()
  172. # --- FASE 1: BÚSQUEDA EXPONENCIAL (Encontrar rango) ---
  173. # Encontrar un 'high_bound' (página) donde el último N° sea >= 'number'
  174. page = 1
  175. low_bound_page = 1
  176. high_bound_page = 1
  177. # Primero, revisamos la página 1
  178. first_num, last_num, page_data = _get_page_bounds(page, token)
  179. if first_num is None:
  180. return None # Error en la primera petición
  181. if not page_data:
  182. return None # No hay mesas en total
  183. # Si está en la página 1
  184. if number >= first_num and number <= last_num:
  185. low_bound_page = 1
  186. high_bound_page = 1
  187. # Si es mayor, empezamos a saltar exponencialmente
  188. elif number > last_num:
  189. low_bound_page = 2
  190. page_jump = 2
  191. while True:
  192. current_page = low_bound_page + page_jump - 1
  193. first, last, data = _get_page_bounds(current_page, token)
  194. if not data:
  195. # Nos pasamos, el rango es entre low y la página actual
  196. high_bound_page = current_page - 1
  197. break
  198. if number <= last:
  199. # Encontramos el techo. El rango es [low_bound_page, current_page]
  200. high_bound_page = current_page
  201. break
  202. # Si no, actualizamos el 'piso' y duplicamos el salto
  203. low_bound_page = current_page + 1
  204. page_jump *= 2
  205. # --- FASE 2: BÚSQUEDA BINARIA (En el rango) ---
  206. target_page_data = []
  207. while low_bound_page <= high_bound_page:
  208. mid_page = (low_bound_page + high_bound_page) // 2
  209. first, last, data = _get_page_bounds(mid_page, token)
  210. if not data:
  211. # Página vacía, buscar en la mitad inferior
  212. high_bound_page = mid_page - 1
  213. continue
  214. if number >= first and number <= last:
  215. # ¡Encontramos la página correcta!
  216. target_page_data = data
  217. break
  218. elif number < first:
  219. # Está en una página anterior
  220. high_bound_page = mid_page - 1
  221. else: # number > last
  222. # Está en una página posterior
  223. low_bound_page = mid_page + 1
  224. # Filtramos la página que encontramos
  225. try:
  226. return list(filter(lambda x: x['attributes']['number'] == number, target_page_data))[0]
  227. except IndexError:
  228. # Esto no debería pasar si la lógica es correcta,
  229. # pero es una salvaguarda
  230. return None
  231. def get_sale(sale_id:int):
  232. url = 'https://api.fu.do/v1alpha1/sales/{}'.format(sale_id)
  233. token = get_token()
  234. headers = {
  235. 'Authorization': 'Bearer ' + token
  236. }
  237. r = requests.get(url, headers=headers)
  238. if r.status_code != 200:
  239. logger.error('Error al obtener tablas:' + str(r.json()['errors']))
  240. return None
  241. return r.json()
  242. def create_sale(table_id:int):
  243. url = 'https://api.fu.do/v1alpha1/sales'
  244. token = get_token()
  245. headers = {
  246. 'Authorization': 'Bearer ' + token
  247. }
  248. data = {
  249. "data": {
  250. "type": "Sale",
  251. "attributes": {
  252. "people": 1,
  253. "saleType": "EAT-IN",
  254. "comment": "Pedido desde la app pedidos express"
  255. },
  256. "relationships": {
  257. "table": {
  258. "data": {
  259. "id": str(table_id),
  260. "type": "Table"
  261. }
  262. },
  263. "waiter": {
  264. "data": {
  265. "type": "User",
  266. "id": "76"
  267. }
  268. }
  269. }
  270. }
  271. }
  272. r = requests.post(url, headers=headers, json=data)
  273. if r.status_code != 201:
  274. logger.error('Error al crear la venta:', r.json())
  275. return None
  276. return r.json()["data"]
  277. def create_item(product_id:int, quantity:int, sale_id:int, comment = None):
  278. url = 'https://api.fu.do/v1alpha1/items'
  279. token = get_token()
  280. headers = {
  281. 'Authorization': 'Bearer ' + token
  282. }
  283. data = {
  284. "quantity": quantity,
  285. "origin": "MOBILE",
  286. "comment": "Pedido desde pedidos express" + (f" - {comment}" if comment else ""),
  287. }
  288. data = {
  289. "data":{
  290. "type": "Item",
  291. "attributes": data,
  292. "relationships": {
  293. "product": {
  294. "data": {
  295. "type": "Product",
  296. "id": str(product_id)
  297. }
  298. },
  299. "sale": {
  300. "data": {
  301. "type": "Sale",
  302. "id": str(sale_id)
  303. }
  304. }
  305. },
  306. }
  307. }
  308. r = requests.post(url, headers=headers, json=data)
  309. if r.status_code != 201:
  310. logger.error(r.json())
  311. return None
  312. return r.json()["data"]
  313. def get_active_sale(table):
  314. data = table['relationships']['activeSales']['data']
  315. if len(data) == 0:
  316. return None
  317. return data[0]
  318. def clear_token():
  319. """
  320. Elimina el token cached de Redis.
  321. Útil cuando el token es inválido o se necesita forzar una renovación.
  322. """
  323. try:
  324. redis_client.delete(REDIS_TOKEN_KEY)
  325. logger.info("Token eliminado del cache")
  326. except Exception as e:
  327. logger.error(f"Error al eliminar token de Redis: {e}")
  328. """
  329. Instrucciones para hacer un pedido:
  330. 1. Obtener el token de autenticación con `get_token()` (ahora usa Redis cache).
  331. 2. Obtener la mesa con `get_table(numero_de_mesa)`.
  332. 3. Ver si tiene una activeSale, en caso contrario crear una con `create_sale(id_mesa)`.
  333. 4. Agregar los items a la venta con `create_item(id_producto, cantidad, id_venta, comentario)`.
  334. Configuración de Redis:
  335. - Host: REDIS_HOST (default: localhost)
  336. - Puerto: REDIS_PORT (default: 6379)
  337. - Base de datos: REDIS_DB (default: 0)
  338. """
  339. if __name__ == "__main__":
  340. from rich import print
  341. print(get_product(1))