fudo.py 11 KB

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