Дерево папок для: D:/Боты_для_Компаний_кафешки/warmhomeforligo
================================================================================

warmhomeforligo/
  .env
  ----
  BOT_TOKEN=f9LHodD0cOKMaw7kc9d1vBoizMn_wy69rpqBMzq-cT6OS4YtaFIgUZ-vnSnXzl-oEGNRVVEueIzNKox7nOpO
  CHAT_ID=-76564857544885
  BOT_ID=351253510
  IMGBB_API_KEY=76347511d8afbc36a3463e3a039a8ffe
  IMGBB_API_URL=https://api.imgbb.com/1/upload
  GROUP_INVITE_LINK=https://max.ru/join/RZcLAlm1VIY2oOWw6gHfN3h_mrkCsrU_x4qJgVq8BIA
  MAX_API_URL=https://platform-api2.max.ru
  LOG_RETENTION_DAYS=30
  .gitignore
  ----------
  # Файлы с секретами
  .env
  
  # База данных
  *.db
  *.sqlite
  *.sqlite3
  
  # Кэш Python
  __pycache__/
  *.pyc
  *.pyo
  
  # Временные файлы
  *.tmp
  *.log
  
  # IDE
  .vscode/
  .idea/
  *.iml
  
  # Системные файлы
  Thumbs.db
  .DS_Store
  Dockerfile
  ----------
  FROM python:3.11-slim
  
  WORKDIR /app
  
  # Устанавливаем ca-certificates и копируем сертификаты Минцифры
  COPY russian_trusted_root_ca.cer /usr/local/share/ca-certificates/
  COPY russian_trusted_sub_ca.cer /usr/local/share/ca-certificates/
  
  # Добавляем сертификаты в систему
  RUN apt-get update && apt-get install -y ca-certificates && \
      update-ca-certificates --verbose
  
  # Копируем и устанавливаем зависимости
  COPY requirements.txt .
  RUN pip install --no-cache-dir -r requirements.txt
  
  # Копируем код
  COPY . .
  
  CMD ["python", "bot.py"]
  README.md
  ---------
  # Бот Скидки на Напитки от "Кофий"
  
  
  
  ## С чего начать?
  
  Для быстрого старта на Mos.Hub мы подготовили список рекомендованных следующих шагов.
  
  Вы профессионал? Тогда просто отредактируйте этот файл README.md так, как считаете нужным. Хотите сделать это еще легче? [Используйте шаблон в конце данного документа](#отредактируйте-этот-файл-readme)!
  
  ## Добавьте свои файлы в репозиторий
  
  - [ ] Создайте или загрузите файлы
  - [ ] Добавьте файлы посредством командной строки или сделайте пуш существующего репозитория Git при помощи следующих команд:
  
  ```
  cd existing_repo
  git remote add origin https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo.git
  git branch -M main
  git push -uf origin main
  ```
  
  ## Интегрируйте собственные инструменты
  
  - [ ] [Настройте интеграцию](https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo/-/settings/integrations)
  
  ## Работайте совместно с командой
  
  - [ ] Пригласите к участию участников команды или коллег
  - [ ] Создайте новый запрос на слияние
  - [ ] Автоматически закрывайте задачи при помощи запроса на слияние
  - [ ] Включите согласование запросов на слияние
  - [ ] Выполняйте автоматическое слияние веток при успешном завершении конвейера CI/CD
  
  ***
  
  # Отредактируйте этот файл README
  
  Когда вы готовы сделать этот файл README своим, просто отредактируйте его и используйте удобный шаблон ниже (или сделайте все по-своему - это ведь отправная точка ***вашего*** проекта).
  
  ## Предложения для написания хорошего файла README
  Все проекты отличаются друг от друга, так что для начала решите, какие из разделов применимы к вашему проекту. Эти разделы используются в качестве шаблона для большинства проектов разработки ПО с открытым исходным кодом. Также примите во внимание, что слишком длинный и детальный README все же лучше, чем слишком короткий. Если ваш README вам кажется слишком длинным, рассмотрите иные способы документирования, не предполагающие сокращение детальности информации.
  
  ## Название проекта
  Выберите такое название проекта, которое говорит само за себя
  
  ## Описание
  Дайте людям знать о специфических возможностях вашего проекта. Предоставьте контекст и добавьте ссылки на все дополнительные материалы, с которыми посетители вашего проекта могут быть незнакомы. Сюда же можно добавить список фич и подраздел с описанием окружений. Если есть проекты похожие на ваш, этот раздел будет тем местом, где можно описать ключевые отличия вашего проекта от других аналогичных проектов.
  
  ## Бейджи
  В некоторых файлах README вы могли видеть маленькие значки, обозначающие разнообразные метаданные, такие как статус прохождения проектов различных тестов. Вы можете воспользоваться [сервисом Shields](https://shields.io/) для добавление некоторых подобных бейджей в ваш README. Многие другие сервисы, включая Mos.Hub, также предоставляют инструкции по добавлению бейджей.
  
  ## Скриншоты
  В зависимости от того, над чем вы работаете, хорошей идеей может быть включение в README скриншотов или даже видео (обычно в виде GIF-анимации). Инструменты наподобие ttygif могут вам в этом помочь.
  
  ## Установка
  В рамках конкретной экосистемы разработки могут существовать общеизвестные способы установки софта, такие как Yarn, NuGet или Homebrew. Тем не менее, примите во внимание возможность того, что читатель вашего README может быть новичком, которому могут потребоваться более подробные указания. Перечисление конкретных шагов поможет устранить неопределенность и побудить людей начать использование вашего проекта как можно скорее.
  
  Если ваш проект работает только в специфическом контексте (например, только на конкретной версии языка программирования), или он требует ручной установки специфических зависимостей, рекомендуется добавить дополнительный подраздел "Требования"
  
  ## Примеры использования
  По возможности сопровождайте ваш проект примерами использования и показывайте ожидаемый результат работы приложения. Полезно встроить небольшие примеры использования непосредственно в файл README, а также дать ссылки на более сложные примеры, если они слишком длинные для включения в файл README.
  
  ## Поддержка
  Объясните людям, где  они могут получить техническую поддержку. Это может быть трекер задач, чат, адрес электронной почты и т.п.
  
  ## Дорожная карта
  Если у вас есть идеи для будущих релизов, их можно перечислить в данном разделе README.
  
  ## Внести свой вклад
  Если вы открыты ко вкладам сообщества, явно напишите об этом, а также перечислите требования, которые должны выполнить желающие внести свой вклад.
  
  Для людей, которые хотят внести изменения в ваш проект, будет полезно иметь документацию о том, как им начать делать это. Возможно, им будет нужно запустить какой-либо скрипт или установить некие переменные окружения. Явно опишите требуемые шаги. Эти инструкции будут полезны вам самим в будущем.
  
  Вы также можете внести в документ команды для запуска необходимых линтеров или тестов. Эти шаги помогут обеспечить высокое качество кода и уменьшить вероятность изменений, которые могут что-то непреднамеренно сломать. Инструкции по запуску тестов особенно полезны в случае, если для тестов требуются настройки сторонних инструментов, таких как сервер Selenium для тестирования веб-приложений.
  
  ## Авторы и благодарности
  Выразите свою благодарность тем, кто вносит вклад в ваш проект.
  
  ## Лицензия
  Для проектов с открытым исходным кодом необходимо указать лицензию, то есть явно написать о том, на каких условиях другие люди могут использовать ваш проект. Отсутствие информации о лицензии будет расцениваться как запрет легального использования вашего проекта. О видах лицензий можно [прочитать тут](https://ru.hexlet.io/blog/posts/litsenzii-svobodnogo-po-kakie-oni-byvayut-i-kak-vybrat-podhodyaschuyu-dlya-vashego-produkta).
  
  ## Статус проекта
  Если у вас не хватает сил или времени на ваш проект, сообщите об этом в начале файла README. Сообщите о том, что работа над проектом будет происходить медленно или будет совсем остановлена. Кто-нибудь может захотеть сделать форк вашего проекта, или найдется доброволец, готовый взять на себя роль сопровождающего вашего проекта, чтобы проект продолжал развиваться. Вы также можете явно написать о том, что ищете сопровождающего на ваш проект.

  bot.py
  ------
  import traceback
  import asyncio
  import aiohttp
  import json
  import os
  import tempfile
  import base64
  import io
  import logging
  import time
  import ssl
  import cv2
  import numpy as np
  from logging.handlers import TimedRotatingFileHandler
  from datetime import datetime
  from database import get_all_organizations
  from PIL import Image
  
  # ============================================================
  # ИМПОРТ ИЗ DATABASE (ОДИН РАЗ, ВСЕ ФУНКЦИИ)
  # ============================================================
  from database import (
      init_db,
      add_promocode,
      get_active_code,
      get_code_by_text,
      use_promocode,
      is_barista,
      add_barista,
      remove_barista,
      get_all_baristas,
      get_stats_by_org,        # ← новая функция (по организации)
      get_all_active_codes,
      generate_code,
      get_organization_by_chat_id  # ← новая функция
  )
  
  # ============================================================
  # ОТКЛЮЧАЕМ SSL
  # ============================================================
  ssl._create_default_https_context = ssl._create_unverified_context
  
  # ============================================================
  # НАСТРОЙКА ЛОГИРОВАНИЯ
  # ============================================================
  LOG_FILE = "bot.log"
  
  log_formatter = logging.Formatter(
      '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
      datefmt='%Y-%m-%d %H:%M:%S'
  )
  
  from config import BOT_TOKEN, CHAT_ID, BOT_ID, IMGBB_API_KEY, IMGBB_API_URL, GROUP_INVITE_LINK, MAX_API_URL, LOG_RETENTION_DAYS
  
  file_handler = TimedRotatingFileHandler(
      LOG_FILE,
      when="midnight",
      interval=1,
      backupCount=LOG_RETENTION_DAYS,
      encoding='utf-8'
  )
  file_handler.setFormatter(log_formatter)
  
  console_handler = logging.StreamHandler()
  console_handler.setFormatter(log_formatter)
  
  logger = logging.getLogger()
  logger.setLevel(logging.INFO)
  logger.addHandler(file_handler)
  logger.addHandler(console_handler)
  
  # ============================================================
  # QR-ГЕНЕРАТОР
  # ============================================================
  from qr_generator import generate_qr_base64
  
  # ============================================================
  # ТЕКСТЫ СЕРВИСНЫХ СООБЩЕНИЙ
  # ============================================================
  
  HELP_TEXT_ADMIN = """☕ Полный список команд:
  
  👤 Общие команды:
  /start - приветствие
  /help - показать эту справку
  
  👨‍🍳 Команды для баристы:
  Отсканируйте QR-код сканером — бот автоматически распознает его и погасит промокод.
  📸 Отправьте фото с QR-кодом — бот автоматически распознает его и погасит промокод.
  /use LIGO-XXXX - погасить промокод вручную
  
  👑 Админ-команды:
  /stats - статистика промокодов
  /list_baristas - список барист
  /add_barista ID Имя - добавить баристу
  /remove_barista ID - удалить баристу
  /restore - восстановить кнопку
  /revoke LIGO-XXXX - отозвать промокод
  
  ❌ Неизвестная команда. Напишите /help для списка команд."""
  
  HELP_TEXT_BARISTA = """☕ Доступные команды для баристы:
  
  /start - приветствие
  /help - показать эту справку
  
  Отсканируйте QR-код сканером — бот автоматически распознает его и погасит промокод.
  📸 Отправьте фото с QR-кодом — бот автоматически распознает его и погасит промокод.
  
  /use LIGO-XXXX - погасить промокод вручную
  
  ❌ Неизвестная команда. Напишите /help для списка команд."""
  
  HELP_TEXT_USER = """☕ Я создан для того, чтобы дарить Вам скидки! 🎁
  Просто нажми кнопку и получай промокод."""
  
  UNKNOWN_COMMAND_ADMIN = HELP_TEXT_ADMIN
  UNKNOWN_COMMAND_BARISTA = HELP_TEXT_BARISTA
  UNKNOWN_COMMAND_USER = HELP_TEXT_USER
  
  # ============================================================
  # ФУНКЦИИ ДЛЯ РАБОТЫ С API MAX
  # ============================================================
  
  async def decode_qr_from_image(image_data: bytes) -> str:
      try:
          nparr = np.frombuffer(image_data, np.uint8)
          img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
          detector = cv2.QRCodeDetector()
          data, bbox, _ = detector.detectAndDecode(img)
          if data:
              return data.strip()
          return None
      except Exception as e:
          logger.error(f"Ошибка распознавания QR: {e}")
          return None
  
  async def send_photo(user_id, image_base64, caption=""):
      try:
          if image_base64.startswith("data:image/png;base64,"):
              image_base64 = image_base64[len("data:image/png;base64,"):].strip()
  
          try:
              image_bytes = base64.b64decode(image_base64)
          except Exception as e:
              logger.error(f"Ошибка декодирования base64 для пользователя {user_id}: {e}")
              return False
  
          tmp_fd, tmp_path = tempfile.mkstemp(suffix=".png")
          try:
              with os.fdopen(tmp_fd, "wb") as tmp_file:
                  tmp_file.write(image_bytes)
  
              # Загружаем на ImgBB
              import requests
              with open(tmp_path, "rb") as f:
                  files = {"image": f}
                  data = {"key": IMGBB_API_KEY}
                  resp = requests.post(IMGBB_API_URL, files=files, data=data, timeout=30)
  
                  if resp.status_code != 200:
                      logger.error(f"Ошибка загрузки на ImgBB для пользователя {user_id}: {resp.status_code} - {resp.text}")
                      return False
  
                  result = resp.json()
                  if not result.get("success"):
                      logger.error(f"Ошибка ImgBB для пользователя {user_id}: {result}")
                      return False
  
                  image_url = result["data"]["url"]
                  logger.info(f"QR загружен на ImgBB для пользователя {user_id}: {image_url}")
  
              # Отправляем URL в MAX
              url = f"{MAX_API_URL}/messages?user_id={user_id}"
              headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
              payload = {
                  "text": caption,
                  "attachments": [
                      {
                          "type": "image",
                          "payload": {"url": image_url}
                      }
                  ]
              }
  
              # ДИАГНОСТИКА
              logger.info(f"📤 Отправка в MAX: user_id={user_id}")
              logger.info(f"📤 URL: {url}")
              logger.info(f"📤 Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}")
  
              async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                  async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                      if response.status == 200:
                          logger.info(f"✅ QR-код успешно отправлен пользователю {user_id}")
                          return True
                      else:
                          error = await response.text()
                          logger.error(f"❌ Статус: {response.status}")
                          logger.error(f"❌ Заголовки ответа: {response.headers}")
                          logger.error(f"❌ Тело ошибки: {error}")
                          
                          if "dialog.not.found" in error or "dialog.suspended" in error or "chat.denied" in error:
                              logger.warning(f"⚠️ Диалог с пользователем {user_id} не активен")
                              return "blocked"
                          return False
  
              async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                  async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                      if response.status == 200:
                          logger.info(f"QR-код успешно отправлен пользователю {user_id}")
                          return True
                      else:
                          error = await response.text()
                          logger.error(f"Ошибка отправки QR-кода пользователю {user_id}: {error}")
                          
                          if "dialog.not.found" in error or "dialog.suspended" in error or "chat.denied" in error:
                              logger.warning(f"Диалог с пользователем {user_id} не активен")
                              return "blocked"
                          return False
          finally:
              try:
                  os.unlink(tmp_path)
              except Exception as e:
                  logger.warning(f"Ошибка удаления временного файла: {e}")
                  
      except Exception as e:
          logger.error(f"Критическая ошибка при отправке фото пользователю {user_id}: {e}")
          return False
  
  async def send_private_message(user_id, text):
      url = f"{MAX_API_URL}/messages?user_id={user_id}"
      headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
      
      try:
          async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
              async with session.post(url, headers=headers, json={"text": text}, timeout=30) as response:
                  if response.status == 200:
                      logger.info(f"ЛС отправлено {user_id}")
                      return True
                  else:
                      error = await response.text()
                      logger.error(f"Ошибка ЛС: {error}")
                      return False
      except asyncio.TimeoutError:
          logger.error(f"Таймаут при отправке ЛС пользователю {user_id}")
          return False
      except Exception as e:
          logger.error(f"Ошибка при отправке ЛС: {e}")
          return False
  
  
  async def send_message(recipient_id, text, keyboard=None, is_user=False):
      if is_user:
          url = f"{MAX_API_URL}/messages?user_id={recipient_id}"
      else:
          url = f"{MAX_API_URL}/messages?chat_id={recipient_id}"
      
      headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
      payload = {"text": text}
      if keyboard:
          payload["attachments"] = [
              {
                  "type": "inline_keyboard",
                  "payload": {
                      "buttons": [
                          [
                              {
                                  "type": "callback",
                                  "text": keyboard["text"],
                                  "payload": keyboard["payload"]
                              }
                          ]
                      ]
                  }
              }
          ]
      
      try:
          async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
              async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                  if response.status == 200:
                      data = await response.json()
                      message_id = data.get("message", {}).get("body", {}).get("mid")
                      logger.info(f"Сообщение отправлено, ID: {message_id}")
                      return message_id
                  else:
                      error = await response.text()
                      logger.error(f"Ошибка отправки: {error}")
                      return None
      except asyncio.TimeoutError:
          logger.error(f"Таймаут при отправке сообщения")
          return None
      except Exception as e:
          logger.error(f"Ошибка при отправке сообщения: {e}")
          return None
  
  
  async def delete_message(chat_id, message_id):
      url = f"{MAX_API_URL}/messages?message_id={message_id}"
      headers = {"Authorization": BOT_TOKEN}
      
      try:
          async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
              async with session.delete(url, headers=headers, timeout=30) as response:
                  if response.status == 200:
                      data = await response.json()
                      if data.get("success", False):
                          logger.info(f"Сообщение {message_id} удалено")
                          return True
                  return False
      except asyncio.TimeoutError:
          logger.error(f"Таймаут при удалении сообщения {message_id}")
          return False
      except Exception as e:
          logger.error(f"Ошибка при удалении сообщения: {e}")
          return False
  
  async def answer_callback(callback_id, notification_text=None):
      url = f"{MAX_API_URL}/answers?callback_id={callback_id}"
      headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
      
      payload = {}
      if notification_text:
          payload["notification"] = notification_text
      else:
          payload["notification"] = "✅"  # пустое уведомление
      
      try:
          async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
              async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                  if response.status != 200:
                      error = await response.text()
                      logger.error(f"Ошибка при ответе на callback: {error}")
      except asyncio.TimeoutError:
          logger.error(f"Таймаут при ответе на callback {callback_id}")
      except Exception as e:
          logger.error(f"Ошибка отправки уведомления: {e}")
  
  async def pin_message(chat_id, message_id):
      url = f"{MAX_API_URL}/chats/{chat_id}/pin"
      headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
      payload = {"message_id": message_id}
      
      try:
          async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
              async with session.put(url, headers=headers, json=payload, timeout=30) as response:
                  if response.status == 200:
                      logger.info(f"Сообщение {message_id} закреплено в чате {chat_id}")
                      return True
                  else:
                      error = await response.text()
                      logger.error(f"Ошибка закрепления сообщения: {error}")
                      return False
      except asyncio.TimeoutError:
          logger.error(f"Таймаут при закреплении сообщения {message_id}")
          return False
      except Exception as e:
          logger.error(f"Ошибка при закреплении сообщения: {e}")
          return False
  
  # ============================================================
  # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
  # ============================================================
  
  async def send_welcome_and_button(user_id):
      """Отправляет приветствие и кнопку пользователю в ЛС."""
      # Проверяем, состоит ли пользователь хотя бы в одной организации
      user_orgs = await get_user_organizations(user_id)
      
      if not user_orgs:
          await send_private_message(
              user_id,
              "⛔ Вы не являетесь сотрудником ни одной из организаций, участвующих в акции.\n\n"
              "Пожалуйста, вступите в группу вашей организации и попробуйте снова."
          )
          return
      
      await send_private_message(
          user_id,
          "☕ Рад видеть тебя в нашей кофейне! Жми кнопку и забирай промокод!"
      )
      await send_message(
          user_id,
          "☕ Нажми на кнопку, чтобы получить промокод на скидку!",
          {"text": "☕ Хочу скидку!", "payload": "get_discount"},
          is_user=True
      )
  
  async def is_user_admin(user_id, chat_id):
      """Проверяет, является ли пользователь администратором в любой организации"""
      # Если передан chat_id, сначала проверяем конкретную организацию
      if chat_id:
          org = await get_organization_by_chat_id(chat_id)
          if org:
              return await _is_admin_in_chat(user_id, chat_id)
      
      # Если chat_id не передан или организация не найдена — проверяем все
      orgs = await get_all_organizations()
      for org in orgs:
          if await _is_admin_in_chat(user_id, org['chat_id']):
              return True
      return False
  
  async def _is_admin_in_chat(user_id, chat_id):
      """Вспомогательная функция для проверки админа в конкретном чате"""
      url = f"{MAX_API_URL}/chats/{chat_id}/members"
      headers = {"Authorization": BOT_TOKEN}
      
      try:
          async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
              async with session.get(url, headers=headers, timeout=30) as response:
                  if response.status != 200:
                      return False
                  data = await response.json()
                  for member in data.get("members", []):
                      if member.get("user_id") == user_id:
                          return member.get("is_owner", False) or member.get("is_admin", False)
                  return False
      except Exception as e:
          logger.error(f"Ошибка проверки прав пользователя: {e}")
          return False
  
  async def is_user_in_chat(user_id, chat_id):
      logger.info(f"🔍 is_user_in_chat: user_id={user_id}, chat_id={chat_id}")
      url = f"{MAX_API_URL}/chats/{chat_id}/members"
      headers = {"Authorization": BOT_TOKEN}
      
      try:
          async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
              async with session.get(url, headers=headers, timeout=30) as response:
                  if response.status == 200:
                      data = await response.json()
                      for member in data.get("members", []):
                          if member.get("user_id") == user_id:
                              logger.info(f"✅ Пользователь {user_id} найден в чате {chat_id}")
                              return True
                      logger.info(f"❌ Пользователь {user_id} не найден в чате {chat_id}")
                      return False
                  else:
                      logger.error(f"❌ Ошибка проверки членства: {response.status} - {await response.text()}")
                      return False
      except Exception as e:
          logger.error(f"❌ Ошибка в is_user_in_chat: {e}")
          return False
  
  # ============================================================
  # ОБРАБОТЧИКИ КОМАНД И CALLBACK
  # ============================================================
  
  async def handle_command(chat_id, user_id, user_name, command):
      is_admin = await is_user_admin(user_id, CHAT_ID)
      is_bar = await is_barista(user_id)
  
      if command == "/start":
          await send_welcome_and_button(user_id)
          return
  
      if command == "/help":
          if is_admin:
              await send_private_message(user_id, HELP_TEXT_ADMIN)
          elif is_bar:
              await send_private_message(user_id, HELP_TEXT_BARISTA)
          else:
              await send_private_message(user_id, HELP_TEXT_USER)
          return
  
      if command.startswith("/use"):
          if not is_bar:
              if is_admin:
                  await send_private_message(user_id, HELP_TEXT_ADMIN)
              else:
                  await send_private_message(user_id, HELP_TEXT_USER)
              return
  
          code = command.replace("/use", "").strip()
          if not code:
              await send_private_message(user_id, "❌ Формат: /use LIGO-XXXX")
              return
  
          promo = await get_code_by_text(code)
          if not promo:
              await send_private_message(user_id, "❌ Промокод не найден")
              return
  
          if promo["status"] == "used":
              await send_private_message(
                  user_id,
                  f"❌ Промокод уже был использован {promo['used_at']}"
              )
              await send_private_message(
                  promo["user_id"],
                  f"❌ Ваш промокод {code} был использован. Запросите новый."
              )
              return
  
          await use_promocode(code, user_id)
          await send_private_message(
              user_id,
              f"✅ Промокод {code} активирован! Скидка предоставлена."
          )
          await send_private_message(
              promo["user_id"],
              f"✅ Ваш промокод {code} был успешно использован. Можете запросить новый."
          )
          await send_welcome_and_button(promo["user_id"])
          return
  
      if not is_admin:
          if is_bar:
              await send_private_message(user_id, HELP_TEXT_BARISTA)
          else:
              await send_private_message(user_id, HELP_TEXT_USER)
          return
  
      if command.startswith("/add_barista"):
          parts = command.split()
          if len(parts) < 3:
              await send_private_message(user_id, "❌ Формат: /add_barista <user_id> <name>")
              return
          try:
              barista_id = int(parts[1])
              name = " ".join(parts[2:])
              await add_barista(barista_id, name, user_id)
              await send_private_message(user_id, f"✅ Бариста {name} (ID {barista_id}) добавлен.")
          except ValueError:
              await send_private_message(user_id, "❌ ID должен быть числом.")
          return
  
      if command.startswith("/remove_barista"):
          parts = command.split()
          if len(parts) < 2:
              await send_private_message(user_id, "❌ Формат: /remove_barista <user_id>")
              return
          try:
              barista_id = int(parts[1])
              await remove_barista(barista_id)
              await send_private_message(user_id, f"✅ Бариста (ID {barista_id}) удалён.")
          except ValueError:
              await send_private_message(user_id, "❌ ID должен быть числом.")
          return
  
      if command == "/list_baristas":
          baristas = await get_all_baristas()
          if not baristas:
              await send_private_message(user_id, "📭 Список барист пуст.")
              return
          text = "🧑‍🍳 Список барист:\n"
          for b in baristas:
              text += f"- {b['name']} (ID: {b['user_id']})\n"
          await send_private_message(user_id, text)
          return
  
      if command == "/stats":
          # Определяем организацию по chat_id (если команда из группы)
          org = await get_organization_by_chat_id(chat_id)
          if not org:
              # Если команда из ЛС — пытаемся найти организацию по последнему промокоду
              # или просто показываем общую статистику
              stats = await get_stats_by_org(None)  # если реализована общая статистика
              text = "📊 Общая статистика:\n"
          else:
              stats = await get_stats_by_org(org['id'])
              text = f"📊 Статистика для {org['name']}:\n"
          
          text += (
              f"Всего выпущено: {stats['total']}\n"
              f"Активных: {stats['active']}\n"
              f"Использовано: {stats['used']}"
          )
          await send_private_message(user_id, text)
          return
  
      if command.startswith("/revoke"):
          parts = command.split()
          if len(parts) < 2:
              await send_private_message(user_id, "❌ Формат: /revoke LIGO-XXXX")
              return
          code = parts[1].upper()
          promo = await get_code_by_text(code)
          if not promo:
              await send_private_message(user_id, "❌ Промокод не найден.")
              return
          if promo["status"] == "used":
              await send_private_message(user_id, "❌ Этот промокод уже использован.")
              return
          await use_promocode(code, user_id)
          await send_private_message(user_id, f"✅ Промокод {code} отозван.")
          return
  
      if command == "/restore":
          await send_welcome_and_button(user_id)
          return
  
      if is_admin:
          await send_private_message(user_id, HELP_TEXT_ADMIN)
      elif is_bar:
          await send_private_message(user_id, HELP_TEXT_BARISTA)
      else:
          await send_private_message(user_id, HELP_TEXT_USER)
  
  async def handle_callback(chat_id, user_id, user_name, callback_data, callback_id):
      try:
          answered = False
          
          if callback_data == "get_discount":
              # ✅ 1. ПРОВЕРЯЕМ, ЕСТЬ ЛИ УЖЕ АКТИВНЫЙ ПРОМОКОД
              existing = await get_active_code(user_id)
              if existing:
                  code = existing["code"]
                  qr_b64 = existing.get("qr_base64") or generate_qr_base64(code)
                  
                  await send_private_message(
                      user_id,
                      f"☕ У вас уже есть активный промокод: *{code}*"
                  )
                  
                  result = await send_photo(user_id, qr_b64, f"☕ Ваш промокод: *{code}*")
                  
                  if result == "blocked":
                      if not answered:
                          await answer_callback(callback_id)
                          answered = True
                      return
                  
                  elif result == "imgbb_failed":
                      await send_private_message(
                          user_id,
                          f"☕ Ваш промокод: *{code}*\n\n"
                          f"Извините, QR-код временно недоступен. 😇\n"
                          f"Покажите баристе текстовый промокод — он тоже принимается!"
                      )
                      
                      await send_private_message(
                          chat_id,
                          f"🚨 **КРИТИЧЕСКАЯ ОШИБКА!**\n\n"
                          f"❌ Сервис загрузки изображений (ImgBB) недоступен!\n"
                          f"📍 Пользователь: {user_name} (ID: {user_id})\n"
                          f"📅 Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
                          f"🔧 Срочно проверьте доступность ImgBB и API-ключ!"
                      )
                      
                      if not answered:
                          await answer_callback(callback_id)
                          answered = True
                      return
                  
                  if not answered:
                      await answer_callback(callback_id)
                      answered = True
                  return
              
              # ✅ 2. ПОЛУЧАЕМ СПИСОК ОРГАНИЗАЦИЙ, В КОТОРЫХ СОСТОИТ ПОЛЬЗОВАТЕЛЬ
              user_orgs = await get_user_organizations(user_id)
              logger.info(f"🔍 Организации пользователя {user_id}: {[org['name'] for org in user_orgs]}")
              
              if not user_orgs:
                  await send_private_message(
                      user_id,
                      "❌ Вы не состоите ни в одной из групп, участвующих в акции.\n"
                      "Пожалуйста, вступите в группу вашей организации и попробуйте снова."
                  )
                  return
              
              # ✅ 3. ФОРМИРУЕМ ПРЕФИКС ИЗ ВСЕХ ОРГАНИЗАЦИЙ
              prefixes = ''.join([org['prefix'] for org in user_orgs])
              org_id = user_orgs[0]['id']  # для сохранения берём первую организацию
              
              # ✅ 4. ГЕНЕРИРУЕМ КОД С ОБЩИМ ПРЕФИКСОМ
              code = generate_code(prefixes)
              qr_b64 = generate_qr_base64(code)
              
              logger.info(f"🔍 Генерация кода: prefix={prefixes}, code={code}, org_id={org_id}")
              
              # ✅ 5. СОХРАНЯЕМ С organization_id
              await add_promocode(user_id, user_name, code, org_id, qr_b64)
              
              # ✅ 6. ОТПРАВЛЯЕМ QR-КОД
              result = await send_photo(user_id, qr_b64, f"☕ Ваш промокод: *{code}*")
              
              if result == "blocked":
                  if not answered:
                      await answer_callback(callback_id)
                      answered = True
                  return
              
              elif result == "imgbb_failed":
                  await send_private_message(
                      user_id,
                      f"☕ Ваш промокод: *{code}*\n\n"
                      f"Извините, QR-код временно недоступен. 😇\n"
                      f"Покажите баристе текстовый промокод — он тоже принимается!"
                  )
                  
                  await send_private_message(
                      chat_id,
                      f"🚨 **КРИТИЧЕСКАЯ ОШИБКА!**\n\n"
                      f"❌ Сервис загрузки изображений (ImgBB) недоступен!\n"
                      f"📍 Пользователь: {user_name} (ID: {user_id})\n"
                      f"📅 Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
                      f"🔧 Срочно проверьте доступность ImgBB и API-ключ!"
                  )
                  
                  if not answered:
                      await answer_callback(callback_id)
                      answered = True
                  return
              
              if not answered:
                  await answer_callback(callback_id)
                  answered = True
                  
      except Exception as e:
          logger.error(f"Ошибка обработки callback: {e}")
          if not answered:
              try:
                  await answer_callback(callback_id)
              except:
                  pass
  # ============================================================
  # ОСНОВНАЯ ЛОГИКА ОБРАБОТКИ ОБНОВЛЕНИЙ
  # ============================================================
  
  async def process_update(update):
      try:
          if update.get("update_type") == "bot_started":
              user_id = update.get("user_id")
              if not user_id:
                  user_id = update.get("user", {}).get("user_id")
              if not user_id:
                  user_id = update.get("from", {}).get("user_id")
              
              user_name = update.get("user", {}).get("first_name") or update.get("from", {}).get("first_name") or "Пользователь"
              logger.info(f"🚀 Пользователь {user_name} (ID: {user_id}) активировал бота")
              await send_welcome_and_button(user_id)
              return
  
          if "callback" in update:
              try:
                  callback = update["callback"]
                  user_id = callback["user"]["user_id"]
                  user_name = callback["user"].get("first_name", "Сотрудник")
                  callback_data = callback.get("payload")
                  callback_id = callback.get("callback_id")
                  
                  # ✅ ДИАГНОСТИКА: выводим всё обновление
                  logger.info(f"📦 Полный callback update: {json.dumps(update, ensure_ascii=False, indent=2)}")
                  
                  # Пробуем получить chat_id разными способами
                  chat_id = update.get("message", {}).get("recipient", {}).get("chat_id")
                  if not chat_id:
                      chat_id = update.get("chat_id")
                  if not chat_id:
                      chat_id = callback.get("chat_id")
                  
                  logger.info(f"🔍 Определён chat_id: {chat_id}")
                  
                  await handle_callback(chat_id, user_id, user_name, callback_data, callback_id)
                  return
              except Exception as e:
                  logger.error(f"Ошибка обработки callback: {e}")
                  logger.error(f"Получено update: {update}")
  
  
          elif "message" in update:
              try:
                  message = update["message"]
                  sender = message.get("sender")
                  if not sender:
                      return
  
                  user_id = sender.get("user_id")
                  user_name = sender.get("first_name", "Сотрудник")
                  text = message.get("body", {}).get("text", "")
                  chat_id = message.get("recipient", {}).get("chat_id")
                  message_id = message.get("body", {}).get("mid")
  
                  if chat_id != CHAT_ID:
                      logger.info(f"ЛС от {user_id}: {text[:30] if text else '<фото>'} (chat_id={chat_id})")
                      
                      is_admin = await is_user_admin(user_id, CHAT_ID)
                      is_bar = await is_barista(user_id)
                      
                      if text and text.startswith("/"):
                          await handle_command(chat_id, user_id, user_name, text)
                          return
                      
                      if is_bar:
                          attachments = message.get("body", {}).get("attachments", [])
                          photo_attachments = [att for att in attachments if att.get("type") == "image"]
                          
                          if photo_attachments:
                              logger.info(f"Получено фото от баристы {user_id}, распознаём QR...")
                              photo_url = photo_attachments[0].get("payload", {}).get("url")
                              if not photo_url:
                                  await send_private_message(user_id, "❌ Не удалось получить фото")
                                  return
                              
                              try:
                                  async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                                      async with session.get(photo_url, timeout=30) as resp:
                                          if resp.status != 200:
                                              await send_private_message(user_id, "❌ Не удалось скачать фото")
                                              return
                                          image_data = await resp.read()
                              except Exception as e:
                                  await send_private_message(user_id, f"❌ Ошибка загрузки фото: {e}")
                                  return
                              
                              code = await decode_qr_from_image(image_data)
                              if not code:
                                  await send_private_message(user_id, "❌ Не удалось распознать QR-код")
                                  return
                              
                              logger.info(f"Распознан QR: {code}")
                              
                              promo = await get_code_by_text(code)
                              if not promo:
                                  await send_private_message(user_id, "❌ Промокод не найден")
                                  return
                              
                              if promo["status"] == "used":
                                  await send_private_message(
                                      user_id,
                                      f"❌ Промокод уже был использован {promo['used_at']}"
                                  )
                                  await send_private_message(
                                      promo["user_id"],
                                      f"❌ Ваш промокод {code} был использован. Запросите новый."
                                  )
                                  return
                              
                              try:
                                  await use_promocode(code, user_id)
                                  await send_private_message(
                                      user_id,
                                      f"✅ Промокод {code} активирован! Скидка предоставлена."
                                  )
                                  await send_private_message(
                                      promo["user_id"],
                                      f"✅ Ваш промокод {code} был успешно использован. Можете запросить новый."
                                  )
                                  await send_welcome_and_button(promo["user_id"])
                              except Exception as e:
                                  logger.error(f"Ошибка активации промокода: {e}")
                                  await send_private_message(
                                      user_id,
                                      "❌ Произошла ошибка при активации промокода. Попробуйте позже."
                                  )
                              return
                          
                          if text and text.strip():
                              code = text.strip()
                              promo = await get_code_by_text(code)
                              if promo:
                                  if promo["status"] == "used":
                                      await send_private_message(
                                          user_id,
                                          f"❌ Промокод уже был использован {promo['used_at']}"
                                      )
                                      await send_private_message(
                                          promo["user_id"],
                                          f"❌ Ваш промокод {code} был использован. Запросите новый."
                                      )
                                      return
                                  
                                  try:
                                      await use_promocode(code, user_id)
                                      await send_private_message(
                                          user_id,
                                          f"✅ Промокод {code} активирован! Скидка предоставлена."
                                      )
                                      await send_private_message(
                                          promo["user_id"],
                                          f"✅ Ваш промокод {code} был успешно использован. Можете запросить новый."
                                      )
                                      await send_welcome_and_button(promo["user_id"])
                                  except Exception as e:
                                      logger.error(f"Ошибка активации промокода: {e}")
                                      await send_private_message(
                                          user_id,
                                          "❌ Произошла ошибка при активации промокода. Попробуйте позже."
                                      )
                                  return
                      
                      try:
                          if is_admin:
                              await send_private_message(user_id, HELP_TEXT_ADMIN)
                          elif is_bar:
                              await send_private_message(user_id, HELP_TEXT_BARISTA)
                          else:
                              await send_private_message(user_id, HELP_TEXT_USER)
                              await send_welcome_and_button(user_id)
                      except Exception as e:
                          logger.error(f"Ошибка отправки справки: {e}")
                      return
  
                  # --- ГРУППА ---
                  if chat_id != CHAT_ID:  # если чат не ЛИГО, но всё равно обрабатываем
                      # Проверяем, зарегистрирована ли организация
                      org = await get_organization_by_chat_id(chat_id)
                      if not org:
                          logger.info(f"⚠️ Неизвестная группа {chat_id}, сообщение от {user_id}: {text}")
                          # Можно отправить уведомление админу или просто игнорировать
                          return
  
                  logger.info(f"📩 Группа от {user_id} (chat_id={chat_id}): {text}")
  
                  try:
                      is_admin = await is_user_admin(user_id, chat_id)
                      
                      if is_admin:
                          # Администратор — логируем, но не удаляем
                          logger.info(f"👑 Администратор {user_name} написал в группе {chat_id}: {text}")
                          # Если админ написал команду — обрабатываем
                          if text and text.startswith("/"):
                              await handle_command(chat_id, user_id, user_name, text)
                      else:
                          # Не-админ — удаляем сообщение
                          if message_id:
                              logger.info(f"🗑️ Удаляем сообщение от не-админа {user_id} в группе {chat_id}")
                              await delete_message(chat_id, message_id)
                  except Exception as e:
                      logger.error(f"Ошибка обработки сообщения из группы {chat_id}: {e}")
                  return
  
              except Exception as e:
                  logger.error(f"Ошибка обработки сообщения: {e}")
                  logger.error(f"Получено update: {update}")
  
          else:
              logger.info(f"Неизвестный тип обновления: {update.get('update_type')}")
  
      except Exception as e:
          logger.error(f"Критическая ошибка обработки update: {e}")
          logger.error(f"Получено update: {update}")
  
  
  async def get_user_organizations(user_id: int):
      orgs = await get_all_organizations()
      user_orgs = []
      
      for org in orgs:
          is_member = await is_user_in_chat(user_id, org['chat_id'])
          logger.info(f"🔍 Проверка членства в {org['name']} (chat_id={org['chat_id']}): {is_member}")
          if is_member:
              user_orgs.append(org)
      
      return user_orgs
  
  
  
  # ============================================================
  # ЗАПУСК БОТА
  # ============================================================
  
  async def listen():
      url = f"{MAX_API_URL}/updates"
      headers = {"Authorization": BOT_TOKEN}
      last_update_id = 0
      
      while True:
          try:
              params = {"timeout": 30, "offset": last_update_id + 1}
              async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                  async with session.get(url, headers=headers, params=params, timeout=30) as response:
                      if response.status == 200:
                          data = await response.json()
                          for update in data.get("updates", []):
                              await process_update(update)
                              if update.get("update_id", 0) > last_update_id:
                                  last_update_id = update["update_id"]
                      else:
                          error = await response.text()
                          logger.error(f"Ошибка получения обновлений: {error}")
                          await asyncio.sleep(5)
          except asyncio.TimeoutError:
              logger.warning("Таймаут при получении обновлений, пробую снова...")
              await asyncio.sleep(5)
          except Exception as e:
              logger.error(f"Ошибка в listen: {e}")
              logger.error(traceback.format_exc())
              await asyncio.sleep(5)
  
  # ============================================================
  # ЗАПУСК БОТА
  # ============================================================
  
  async def main():
      logger.info("🚀 Бот запущен!")
      await init_db()
      
      # ✅ ДИАГНОСТИКА: выводим все организации из БД
      try:
          from database import get_all_organizations
          orgs = await get_all_organizations()
          if orgs:
              logger.info(f"📋 Найдено организаций: {len(orgs)}")
              for org in orgs:
                  logger.info(f"  🏢 {org['name']} (ID: {org['id']}, chat_id: {org['chat_id']}, префикс: {org['prefix']})")
          else:
              logger.warning("⚠️ В БД нет ни одной организации!")
      except Exception as e:
          logger.error(f"❌ Ошибка при чтении организаций: {e}")
      
      print("=" * 50)
      print("☕ Бот Скидки на Напитки от \"Кофий\"")
      print("  Бот работает с БД и QR-кодами")
      print("  Команды для админа (в группе или ЛС):")
      print("    /stats - статистика")
      print("    /add_barista ID Имя - добавить баристу")
      print("    /remove_barista ID - удалить баристу")
      print("    /list_baristas - список барист")
      print("    /restore - восстановить кнопку")
      print("    /revoke LIGO-XXXX - отозвать промокод")
      print("  Команды для баристы (в ЛС):")
      print("    /use LIGO-XXXX - погасить промокод")
      print("    Или просто отправить фото с QR-кодом")
      print("=" * 50)
      
      await listen()
  
  # ============================================================
  # ТОЧКА ВХОДА
  # ============================================================
  
  if __name__ == "__main__":
      try:
          print("🚀 Запуск бота...")
          asyncio.run(main())
      except KeyboardInterrupt:
          print("\n⏹ Бот остановлен")
      except Exception as e:
          print(f"❌ КРИТИЧЕСКАЯ ОШИБКА: {e}")
          import traceback
          traceback.print_exc()
  config.py
  ---------
  import os
  from dotenv import load_dotenv
  
  load_dotenv()
  
  BOT_TOKEN = os.getenv("BOT_TOKEN")
  CHAT_ID = int(os.getenv("CHAT_ID"))
  BOT_ID = int(os.getenv("BOT_ID"))
  IMGBB_API_KEY = os.getenv("IMGBB_API_KEY")
  IMGBB_API_URL = os.getenv("IMGBB_API_URL")
  GROUP_INVITE_LINK = os.getenv("GROUP_INVITE_LINK")
  MAX_API_URL = os.getenv("MAX_API_URL")
  LOG_RETENTION_DAYS = int(os.getenv("LOG_RETENTION_DAYS", 30))  # <-- ДОБАВИТЬ, значение по умолчанию 30
  
  # Проверяем, что все переменные загружены
  if not all([BOT_TOKEN, CHAT_ID, BOT_ID, IMGBB_API_KEY, IMGBB_API_URL, GROUP_INVITE_LINK, MAX_API_URL]):
      raise ValueError("❌ Ошибка: не все переменные окружения загружены. Проверьте файл .env")
  database.py
  -----------
  import aiosqlite
  import os
  import random
  import string
  from datetime import datetime
  
  # Путь к БД в корне проекта
  DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "promocodes.db")
  
  # --- ИНИЦИАЛИЗАЦИЯ БАЗЫ ---
  
  async def init_db():
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute('''
              CREATE TABLE IF NOT EXISTS organizations (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  name TEXT NOT NULL,
                  prefix TEXT NOT NULL UNIQUE,
                  chat_id INTEGER UNIQUE NOT NULL,
                  invite_link TEXT,
                  is_active BOOLEAN DEFAULT 1,
                  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
              )
          ''')
          await db.execute('''
              CREATE TABLE IF NOT EXISTS promocodes (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  code TEXT UNIQUE NOT NULL,
                  user_id INTEGER NOT NULL,
                  user_name TEXT,
                  organization_id INTEGER NOT NULL,
                  issued_at DATETIME NOT NULL,
                  used_at DATETIME,
                  used_by INTEGER,
                  status TEXT DEFAULT 'active',
                  qr_base64 TEXT,
                  FOREIGN KEY (organization_id) REFERENCES organizations(id)
              )
          ''')
          await db.execute('''
              CREATE TABLE IF NOT EXISTS baristas (
                  id INTEGER PRIMARY KEY,
                  name TEXT,
                  added_by INTEGER,
                  added_at DATETIME DEFAULT CURRENT_TIMESTAMP
              )
          ''')
          await db.commit()
          print("✅ База данных инициализирована (мульти-орг)")
  
  # --- ОРГАНИЗАЦИИ ---
  
  async def get_organization_by_chat_id(chat_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute(
              "SELECT id, name, prefix FROM organizations WHERE chat_id = ? AND is_active = 1",
              (chat_id,)
          )
          row = await cursor.fetchone()
          if row:
              return {"id": row[0], "name": row[1], "prefix": row[2]}
          return None
  
  async def add_organization(name: str, prefix: str, chat_id: int, invite_link: str = None):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute(
              "INSERT INTO organizations (name, prefix, chat_id, invite_link) VALUES (?, ?, ?, ?)",
              (name, prefix, chat_id, invite_link)
          )
          await db.commit()
  
  async def get_all_organizations():
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute("SELECT id, name, prefix, chat_id, invite_link, is_active FROM organizations")
          rows = await cursor.fetchall()
          return [
              {
                  "id": row[0],
                  "name": row[1],
                  "prefix": row[2],
                  "chat_id": row[3],
                  "invite_link": row[4],
                  "is_active": bool(row[5])
              }
              for row in rows
          ]
  
  # --- ПРОМОКОДЫ (С ПРИВЯЗКОЙ К ОРГАНИЗАЦИИ) ---
  
  async def add_promocode(user_id: int, user_name: str, code: str, org_id: int, qr_base64: str = None):
      print(f"🔍 add_promocode: user_id={user_id}, code={code}, org_id={org_id}")
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute(
              """INSERT INTO promocodes 
                 (code, user_id, user_name, organization_id, issued_at, status, qr_base64) 
                 VALUES (?, ?, ?, ?, ?, ?, ?)""",
              (code, user_id, user_name, org_id, datetime.now(), "active", qr_base64)
          )
          await db.commit()
          print(f"✅ Промокод {code} сохранён в БД")
  
  async def get_active_code(user_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute(
              """SELECT code, issued_at, qr_base64, organization_id 
                 FROM promocodes 
                 WHERE user_id = ? AND status = 'active' 
                 ORDER BY issued_at DESC LIMIT 1""",
              (user_id,)
          )
          row = await cursor.fetchone()
          if row:
              return {
                  "code": row[0],
                  "issued_at": row[1],
                  "qr_base64": row[2],
                  "organization_id": row[3]
              }
          return None
  
  async def get_code_by_text(code: str):
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute("SELECT * FROM promocodes WHERE code = ?", (code,))
          row = await cursor.fetchone()
          if row:
              return {
                  "id": row[0],
                  "code": row[1],
                  "user_id": row[2],
                  "user_name": row[3],
                  "organization_id": row[4],
                  "issued_at": row[5],
                  "used_at": row[6],
                  "used_by": row[7],
                  "status": row[8],
                  "qr_base64": row[9]
              }
          return None
  
  async def use_promocode(code: str, barista_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute(
              "UPDATE promocodes SET status = 'used', used_at = ?, used_by = ? WHERE code = ?",
              (datetime.now(), barista_id, code)
          )
          await db.commit()
  
  async def get_stats_by_org(org_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          total = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE organization_id = ?", (org_id,))
          active = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE organization_id = ? AND status = 'active'", (org_id,))
          used = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE organization_id = ? AND status = 'used'", (org_id,))
          return {
              "total": total[0][0] if total else 0,
              "active": active[0][0] if active else 0,
              "used": used[0][0] if used else 0
          }
  
  # --- ГЕНЕРАТОР КОДА С ПРЕФИКСОМ ---
  
  def generate_code(prefix: str) -> str:
      import sqlite3
      while True:
          part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
          code = f"{prefix}{part}"
          conn = sqlite3.connect(DB_PATH)
          cursor = conn.execute("SELECT COUNT(*) FROM promocodes WHERE code = ?", (code,))
          count = cursor.fetchone()[0]
          conn.close()
          if count == 0:
              return code
  
  # --- БАРИСТЫ ---
  
  async def is_barista(user_id: int) -> bool:
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute("SELECT id FROM baristas WHERE id = ?", (user_id,))
          row = await cursor.fetchone()
          return row is not None
  
  async def add_barista(user_id: int, name: str, added_by: int):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute(
              "INSERT OR REPLACE INTO baristas (id, name, added_by, added_at) VALUES (?, ?, ?, ?)",
              (user_id, name, added_by, datetime.now())
          )
          await db.commit()
  
  async def remove_barista(user_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute("DELETE FROM baristas WHERE id = ?", (user_id,))
          await db.commit()
  
  async def get_all_baristas():
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute("SELECT id, name FROM baristas")
          rows = await cursor.fetchall()
          return [{"user_id": row[0], "name": row[1]} for row in rows]
  
  # --- ОБЩАЯ СТАТИСТИКА (ДЛЯ ОБРАТНОЙ СОВМЕСТИМОСТИ) ---
  
  async def get_stats():
      async with aiosqlite.connect(DB_PATH) as db:
          total = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes")
          active = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE status = 'active'")
          used = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE status = 'used'")
          return {
              "total": total[0][0] if total else 0,
              "active": active[0][0] if active else 0,
              "used": used[0][0] if used else 0
          }
  
  async def get_all_active_codes():
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute('''
              SELECT 
                  p.code, 
                  p.user_name, 
                  p.issued_at, 
                  p.status,
                  o.name as org_name
              FROM promocodes p
              LEFT JOIN organizations o ON p.organization_id = o.id
              WHERE p.status = 'active'
              ORDER BY p.issued_at DESC
          ''')
          rows = await cursor.fetchall()
          return [
              {
                  "code": row[0],
                  "user_name": row[1],
                  "issued_at": row[2],
                  "status": row[3],
                  "org_name": row[4]
              }
              for row in rows
          ]
  
  
  async def get_user_organization(user_id: int):
      """Определяет организацию пользователя по его членству в группах"""
      async with aiosqlite.connect(DB_PATH) as db:
          # Получаем все активные организации
          cursor = await db.execute("SELECT id, name, prefix, chat_id FROM organizations WHERE is_active = 1")
          orgs = await cursor.fetchall()
          
          for org in orgs:
              org_chat_id = org[3]
              # Проверяем, состоит ли пользователь в этой группе
              # Здесь нужен вызов API MAX, но у нас есть is_user_in_chat
              # Мы его вызовем в bot.py, а сюда передадим результат
              pass
          return None
      
  async def get_all_codes(status: str = None):
      """Возвращает все промокоды с фильтром по статусу"""
      async with aiosqlite.connect(DB_PATH) as db:
          if status:
              cursor = await db.execute('''
                  SELECT 
                      p.code, 
                      p.user_name,
                      p.user_id,
                      p.issued_at, 
                      p.status,
                      o.name as org_name,
                      p.used_at,
                      p.organization_id
                  FROM promocodes p
                  LEFT JOIN organizations o ON p.organization_id = o.id
                  WHERE p.status = ?
                  ORDER BY p.issued_at DESC
              ''', (status,))
          else:
              cursor = await db.execute('''
                  SELECT 
                      p.code, 
                      p.user_name,
                      p.user_id,
                      p.issued_at, 
                      p.status,
                      o.name as org_name,
                      p.used_at,
                      p.organization_id
                  FROM promocodes p
                  LEFT JOIN organizations o ON p.organization_id = o.id
                  ORDER BY p.issued_at DESC
              ''')
          rows = await cursor.fetchall()
          return [
              {
                  "code": row[0],
                  "user_name": row[1],
                  "user_id": row[2],
                  "issued_at": row[3],
                  "status": row[4],
                  "org_name": row[5],
                  "used_at": row[6],
                  "organization_id": row[7]
              }
              for row in rows
          ]
  database_old.py
  ---------------
  import aiosqlite
  import random
  import string
  from datetime import datetime
  
  DB_PATH = "promocodes.db"
  
  async def init_db():
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute('''
              CREATE TABLE IF NOT EXISTS promocodes (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  code TEXT UNIQUE NOT NULL,
                  user_id INTEGER NOT NULL,
                  user_name TEXT,
                  issued_at DATETIME NOT NULL,
                  used_at DATETIME,
                  used_by INTEGER,
                  status TEXT DEFAULT 'active',
                  qr_base64 TEXT
              )
          ''')
          await db.execute('''
              CREATE TABLE IF NOT EXISTS baristas (
                  id INTEGER PRIMARY KEY,
                  name TEXT,
                  added_by INTEGER,
                  added_at DATETIME DEFAULT CURRENT_TIMESTAMP
              )
          ''')
          await db.commit()
          print("✅ База данных инициализирована")
  
  async def add_promocode(user_id: int, user_name: str, code: str, qr_base64: str = None):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute(
              "INSERT INTO promocodes (code, user_id, user_name, issued_at, status, qr_base64) VALUES (?, ?, ?, ?, ?, ?)",
              (code, user_id, user_name, datetime.now(), "active", qr_base64)
          )
          await db.commit()
  
  async def get_active_code(user_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute(
              "SELECT code, issued_at, qr_base64 FROM promocodes WHERE user_id = ? AND status = 'active' ORDER BY issued_at DESC LIMIT 1",
              (user_id,)
          )
          row = await cursor.fetchone()
          if row:
              return {"code": row[0], "issued_at": row[1], "qr_base64": row[2]}
          return None
  
  async def get_code_by_text(code: str):
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute(
              "SELECT * FROM promocodes WHERE code = ?",
              (code,)
          )
          row = await cursor.fetchone()
          if row:
              return {
                  "id": row[0],
                  "code": row[1],
                  "user_id": row[2],
                  "user_name": row[3],
                  "issued_at": row[4],
                  "used_at": row[5],
                  "used_by": row[6],
                  "status": row[7],
                  "qr_base64": row[8]
              }
          return None
  
  async def use_promocode(code: str, barista_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute(
              "UPDATE promocodes SET status = 'used', used_at = ?, used_by = ? WHERE code = ?",
              (datetime.now(), barista_id, code)
          )
          await db.commit()
  
  async def is_barista(user_id: int) -> bool:
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute("SELECT id FROM baristas WHERE id = ?", (user_id,))
          row = await cursor.fetchone()
          return row is not None
  
  async def add_barista(user_id: int, name: str, added_by: int):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute(
              "INSERT OR REPLACE INTO baristas (id, name, added_by, added_at) VALUES (?, ?, ?, ?)",
              (user_id, name, added_by, datetime.now())
          )
          await db.commit()
  
  async def remove_barista(user_id: int):
      async with aiosqlite.connect(DB_PATH) as db:
          await db.execute("DELETE FROM baristas WHERE id = ?", (user_id,))
          await db.commit()
  
  async def get_all_baristas():
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute("SELECT id, name FROM baristas")
          rows = await cursor.fetchall()
          return [{"user_id": row[0], "name": row[1]} for row in rows]
  
  async def get_stats():
      async with aiosqlite.connect(DB_PATH) as db:
          cursor1 = await db.execute("SELECT COUNT(*) FROM promocodes")
          total = (await cursor1.fetchone())[0]
          cursor2 = await db.execute("SELECT COUNT(*) FROM promocodes WHERE status = 'active'")
          active = (await cursor2.fetchone())[0]
          cursor3 = await db.execute("SELECT COUNT(*) FROM promocodes WHERE status = 'used'")
          used = (await cursor3.fetchone())[0]
          return {"total": total, "active": active, "used": used}
  
  async def get_all_active_codes():
      """Возвращает список всех активных промокодов"""
      async with aiosqlite.connect(DB_PATH) as db:
          cursor = await db.execute(
              "SELECT code, user_id, user_name, issued_at FROM promocodes WHERE status = 'active'"
          )
          rows = await cursor.fetchall()
          return [{"code": row[0], "user_id": row[1], "user_name": row[2], "issued_at": row[3]} for row in rows]
  
  def generate_code() -> str:
      import sqlite3
      while True:
          part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
          code = f"LIGO-{part}"
          conn = sqlite3.connect(DB_PATH)
          cursor = conn.execute("SELECT COUNT(*) FROM promocodes WHERE code = ?", (code,))
          count = cursor.fetchone()[0]
          conn.close()
          if count == 0:
              return code
  directory_tree.txt
  ------------------
  Дерево папок для: D:/Боты_для_Компаний_кафешки/warmhomeforligo
  ================================================================================
  
  warmhomeforligo/
    .env
    ----
    BOT_TOKEN=f9LHodD0cOKMaw7kc9d1vBoizMn_wy69rpqBMzq-cT6OS4YtaFIgUZ-vnSnXzl-oEGNRVVEueIzNKox7nOpO
    CHAT_ID=-76564857544885
    BOT_ID=351253510
    IMGBB_API_KEY=76347511d8afbc36a3463e3a039a8ffe
    IMGBB_API_URL=https://api.imgbb.com/1/upload
    GROUP_INVITE_LINK=https://max.ru/join/RZcLAlm1VIY2oOWw6gHfN3h_mrkCsrU_x4qJgVq8BIA
    MAX_API_URL=https://platform-api2.max.ru
    LOG_RETENTION_DAYS=30
    .gitignore
    ----------
    # Файлы с секретами
    .env
    
    # База данных
    *.db
    *.sqlite
    *.sqlite3
    
    # Кэш Python
    __pycache__/
    *.pyc
    *.pyo
    
    # Временные файлы
    *.tmp
    *.log
    
    # IDE
    .vscode/
    .idea/
    *.iml
    
    # Системные файлы
    Thumbs.db
    .DS_Store
    Dockerfile
    ----------
    FROM python:3.11-slim
    
    WORKDIR /app
    
    # Устанавливаем ca-certificates и копируем сертификаты Минцифры
    COPY russian_trusted_root_ca.cer /usr/local/share/ca-certificates/
    COPY russian_trusted_sub_ca.cer /usr/local/share/ca-certificates/
    
    # Добавляем сертификаты в систему
    RUN apt-get update && apt-get install -y ca-certificates && \
        update-ca-certificates --verbose
    
    # Копируем и устанавливаем зависимости
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    # Копируем код
    COPY . .
    
    CMD ["python", "bot.py"]
    README.md
    ---------
    # Бот Скидки на Напитки от "Кофий"
    
    
    
    ## С чего начать?
    
    Для быстрого старта на Mos.Hub мы подготовили список рекомендованных следующих шагов.
    
    Вы профессионал? Тогда просто отредактируйте этот файл README.md так, как считаете нужным. Хотите сделать это еще легче? [Используйте шаблон в конце данного документа](#отредактируйте-этот-файл-readme)!
    
    ## Добавьте свои файлы в репозиторий
    
    - [ ] Создайте или загрузите файлы
    - [ ] Добавьте файлы посредством командной строки или сделайте пуш существующего репозитория Git при помощи следующих команд:
    
    ```
    cd existing_repo
    git remote add origin https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo.git
    git branch -M main
    git push -uf origin main
    ```
    
    ## Интегрируйте собственные инструменты
    
    - [ ] [Настройте интеграцию](https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo/-/settings/integrations)
    
    ## Работайте совместно с командой
    
    - [ ] Пригласите к участию участников команды или коллег
    - [ ] Создайте новый запрос на слияние
    - [ ] Автоматически закрывайте задачи при помощи запроса на слияние
    - [ ] Включите согласование запросов на слияние
    - [ ] Выполняйте автоматическое слияние веток при успешном завершении конвейера CI/CD
    
    ***
    
    # Отредактируйте этот файл README
    
    Когда вы готовы сделать этот файл README своим, просто отредактируйте его и используйте удобный шаблон ниже (или сделайте все по-своему - это ведь отправная точка ***вашего*** проекта).
    
    ## Предложения для написания хорошего файла README
    Все проекты отличаются друг от друга, так что для начала решите, какие из разделов применимы к вашему проекту. Эти разделы используются в качестве шаблона для большинства проектов разработки ПО с открытым исходным кодом. Также примите во внимание, что слишком длинный и детальный README все же лучше, чем слишком короткий. Если ваш README вам кажется слишком длинным, рассмотрите иные способы документирования, не предполагающие сокращение детальности информации.
    
    ## Название проекта
    Выберите такое название проекта, которое говорит само за себя
    
    ## Описание
    Дайте людям знать о специфических возможностях вашего проекта. Предоставьте контекст и добавьте ссылки на все дополнительные материалы, с которыми посетители вашего проекта могут быть незнакомы. Сюда же можно добавить список фич и подраздел с описанием окружений. Если есть проекты похожие на ваш, этот раздел будет тем местом, где можно описать ключевые отличия вашего проекта от других аналогичных проектов.
    
    ## Бейджи
    В некоторых файлах README вы могли видеть маленькие значки, обозначающие разнообразные метаданные, такие как статус прохождения проектов различных тестов. Вы можете воспользоваться [сервисом Shields](https://shields.io/) для добавление некоторых подобных бейджей в ваш README. Многие другие сервисы, включая Mos.Hub, также предоставляют инструкции по добавлению бейджей.
    
    ## Скриншоты
    В зависимости от того, над чем вы работаете, хорошей идеей может быть включение в README скриншотов или даже видео (обычно в виде GIF-анимации). Инструменты наподобие ttygif могут вам в этом помочь.
    
    ## Установка
    В рамках конкретной экосистемы разработки могут существовать общеизвестные способы установки софта, такие как Yarn, NuGet или Homebrew. Тем не менее, примите во внимание возможность того, что читатель вашего README может быть новичком, которому могут потребоваться более подробные указания. Перечисление конкретных шагов поможет устранить неопределенность и побудить людей начать использование вашего проекта как можно скорее.
    
    Если ваш проект работает только в специфическом контексте (например, только на конкретной версии языка программирования), или он требует ручной установки специфических зависимостей, рекомендуется добавить дополнительный подраздел "Требования"
    
    ## Примеры использования
    По возможности сопровождайте ваш проект примерами использования и показывайте ожидаемый результат работы приложения. Полезно встроить небольшие примеры использования непосредственно в файл README, а также дать ссылки на более сложные примеры, если они слишком длинные для включения в файл README.
    
    ## Поддержка
    Объясните людям, где  они могут получить техническую поддержку. Это может быть трекер задач, чат, адрес электронной почты и т.п.
    
    ## Дорожная карта
    Если у вас есть идеи для будущих релизов, их можно перечислить в данном разделе README.
    
    ## Внести свой вклад
    Если вы открыты ко вкладам сообщества, явно напишите об этом, а также перечислите требования, которые должны выполнить желающие внести свой вклад.
    
    Для людей, которые хотят внести изменения в ваш проект, будет полезно иметь документацию о том, как им начать делать это. Возможно, им будет нужно запустить какой-либо скрипт или установить некие переменные окружения. Явно опишите требуемые шаги. Эти инструкции будут полезны вам самим в будущем.
    
    Вы также можете внести в документ команды для запуска необходимых линтеров или тестов. Эти шаги помогут обеспечить высокое качество кода и уменьшить вероятность изменений, которые могут что-то непреднамеренно сломать. Инструкции по запуску тестов особенно полезны в случае, если для тестов требуются настройки сторонних инструментов, таких как сервер Selenium для тестирования веб-приложений.
    
    ## Авторы и благодарности
    Выразите свою благодарность тем, кто вносит вклад в ваш проект.
    
    ## Лицензия
    Для проектов с открытым исходным кодом необходимо указать лицензию, то есть явно написать о том, на каких условиях другие люди могут использовать ваш проект. Отсутствие информации о лицензии будет расцениваться как запрет легального использования вашего проекта. О видах лицензий можно [прочитать тут](https://ru.hexlet.io/blog/posts/litsenzii-svobodnogo-po-kakie-oni-byvayut-i-kak-vybrat-podhodyaschuyu-dlya-vashego-produkta).
    
    ## Статус проекта
    Если у вас не хватает сил или времени на ваш проект, сообщите об этом в начале файла README. Сообщите о том, что работа над проектом будет происходить медленно или будет совсем остановлена. Кто-нибудь может захотеть сделать форк вашего проекта, или найдется доброволец, готовый взять на себя роль сопровождающего вашего проекта, чтобы проект продолжал развиваться. Вы также можете явно написать о том, что ищете сопровождающего на ваш проект.
  
    bot.py
    ------
    import traceback
    import asyncio
    import aiohttp
    import json
    import os
    import tempfile
    import base64
    import io
    import logging
    import time
    import ssl
    import cv2
    import numpy as np
    from logging.handlers import TimedRotatingFileHandler
    from datetime import datetime
    from database import get_all_organizations
    from PIL import Image
    
    # ============================================================
    # ИМПОРТ ИЗ DATABASE (ОДИН РАЗ, ВСЕ ФУНКЦИИ)
    # ============================================================
    from database import (
        init_db,
        add_promocode,
        get_active_code,
        get_code_by_text,
        use_promocode,
        is_barista,
        add_barista,
        remove_barista,
        get_all_baristas,
        get_stats_by_org,        # ← новая функция (по организации)
        get_all_active_codes,
        generate_code,
        get_organization_by_chat_id  # ← новая функция
    )
    
    # ============================================================
    # ОТКЛЮЧАЕМ SSL
    # ============================================================
    ssl._create_default_https_context = ssl._create_unverified_context
    
    # ============================================================
    # НАСТРОЙКА ЛОГИРОВАНИЯ
    # ============================================================
    LOG_FILE = "bot.log"
    
    log_formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    from config import BOT_TOKEN, CHAT_ID, BOT_ID, IMGBB_API_KEY, IMGBB_API_URL, GROUP_INVITE_LINK, MAX_API_URL, LOG_RETENTION_DAYS
    
    file_handler = TimedRotatingFileHandler(
        LOG_FILE,
        when="midnight",
        interval=1,
        backupCount=LOG_RETENTION_DAYS,
        encoding='utf-8'
    )
    file_handler.setFormatter(log_formatter)
    
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(log_formatter)
    
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    
    # ============================================================
    # QR-ГЕНЕРАТОР
    # ============================================================
    from qr_generator import generate_qr_base64
    
    # ============================================================
    # ТЕКСТЫ СЕРВИСНЫХ СООБЩЕНИЙ
    # ============================================================
    
    HELP_TEXT_ADMIN = """☕ Полный список команд:
    
    👤 Общие команды:
    /start - приветствие
    /help - показать эту справку
    
    👨‍🍳 Команды для баристы:
    Отсканируйте QR-код сканером — бот автоматически распознает его и погасит промокод.
    📸 Отправьте фото с QR-кодом — бот автоматически распознает его и погасит промокод.
    /use LIGO-XXXX - погасить промокод вручную
    
    👑 Админ-команды:
    /stats - статистика промокодов
    /list_baristas - список барист
    /add_barista ID Имя - добавить баристу
    /remove_barista ID - удалить баристу
    /restore - восстановить кнопку
    /revoke LIGO-XXXX - отозвать промокод
    
    ❌ Неизвестная команда. Напишите /help для списка команд."""
    
    HELP_TEXT_BARISTA = """☕ Доступные команды для баристы:
    
    /start - приветствие
    /help - показать эту справку
    
    Отсканируйте QR-код сканером — бот автоматически распознает его и погасит промокод.
    📸 Отправьте фото с QR-кодом — бот автоматически распознает его и погасит промокод.
    
    /use LIGO-XXXX - погасить промокод вручную
    
    ❌ Неизвестная команда. Напишите /help для списка команд."""
    
    HELP_TEXT_USER = """☕ Я создан для того, чтобы дарить Вам скидки! 🎁
    Просто нажми кнопку и получай промокод."""
    
    UNKNOWN_COMMAND_ADMIN = HELP_TEXT_ADMIN
    UNKNOWN_COMMAND_BARISTA = HELP_TEXT_BARISTA
    UNKNOWN_COMMAND_USER = HELP_TEXT_USER
    
    # ============================================================
    # ФУНКЦИИ ДЛЯ РАБОТЫ С API MAX
    # ============================================================
    
    async def decode_qr_from_image(image_data: bytes) -> str:
        try:
            nparr = np.frombuffer(image_data, np.uint8)
            img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
            detector = cv2.QRCodeDetector()
            data, bbox, _ = detector.detectAndDecode(img)
            if data:
                return data.strip()
            return None
        except Exception as e:
            logger.error(f"Ошибка распознавания QR: {e}")
            return None
    
    async def send_photo(user_id, image_base64, caption=""):
        try:
            if image_base64.startswith("data:image/png;base64,"):
                image_base64 = image_base64[len("data:image/png;base64,"):].strip()
    
            try:
                image_bytes = base64.b64decode(image_base64)
            except Exception as e:
                logger.error(f"Ошибка декодирования base64 для пользователя {user_id}: {e}")
                return False
    
            tmp_fd, tmp_path = tempfile.mkstemp(suffix=".png")
            try:
                with os.fdopen(tmp_fd, "wb") as tmp_file:
                    tmp_file.write(image_bytes)
    
                # Загружаем на ImgBB
                import requests
                with open(tmp_path, "rb") as f:
                    files = {"image": f}
                    data = {"key": IMGBB_API_KEY}
                    resp = requests.post(IMGBB_API_URL, files=files, data=data, timeout=30)
    
                    if resp.status_code != 200:
                        logger.error(f"Ошибка загрузки на ImgBB для пользователя {user_id}: {resp.status_code} - {resp.text}")
                        return False
    
                    result = resp.json()
                    if not result.get("success"):
                        logger.error(f"Ошибка ImgBB для пользователя {user_id}: {result}")
                        return False
    
                    image_url = result["data"]["url"]
                    logger.info(f"QR загружен на ImgBB для пользователя {user_id}: {image_url}")
    
                # Отправляем URL в MAX
                url = f"{MAX_API_URL}/messages?user_id={user_id}"
                headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
                payload = {
                    "text": caption,
                    "attachments": [
                        {
                            "type": "image",
                            "payload": {"url": image_url}
                        }
                    ]
                }
    
                # ДИАГНОСТИКА
                logger.info(f"📤 Отправка в MAX: user_id={user_id}")
                logger.info(f"📤 URL: {url}")
                logger.info(f"📤 Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}")
    
                async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                    async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                        if response.status == 200:
                            logger.info(f"✅ QR-код успешно отправлен пользователю {user_id}")
                            return True
                        else:
                            error = await response.text()
                            logger.error(f"❌ Статус: {response.status}")
                            logger.error(f"❌ Заголовки ответа: {response.headers}")
                            logger.error(f"❌ Тело ошибки: {error}")
                            
                            if "dialog.not.found" in error or "dialog.suspended" in error or "chat.denied" in error:
                                logger.warning(f"⚠️ Диалог с пользователем {user_id} не активен")
                                return "blocked"
                            return False
    
                async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                    async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                        if response.status == 200:
                            logger.info(f"QR-код успешно отправлен пользователю {user_id}")
                            return True
                        else:
                            error = await response.text()
                            logger.error(f"Ошибка отправки QR-кода пользователю {user_id}: {error}")
                            
                            if "dialog.not.found" in error or "dialog.suspended" in error or "chat.denied" in error:
                                logger.warning(f"Диалог с пользователем {user_id} не активен")
                                return "blocked"
                            return False
            finally:
                try:
                    os.unlink(tmp_path)
                except Exception as e:
                    logger.warning(f"Ошибка удаления временного файла: {e}")
                    
        except Exception as e:
            logger.error(f"Критическая ошибка при отправке фото пользователю {user_id}: {e}")
            return False
    
    async def send_private_message(user_id, text):
        url = f"{MAX_API_URL}/messages?user_id={user_id}"
        headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
        
        try:
            async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                async with session.post(url, headers=headers, json={"text": text}, timeout=30) as response:
                    if response.status == 200:
                        logger.info(f"ЛС отправлено {user_id}")
                        return True
                    else:
                        error = await response.text()
                        logger.error(f"Ошибка ЛС: {error}")
                        return False
        except asyncio.TimeoutError:
            logger.error(f"Таймаут при отправке ЛС пользователю {user_id}")
            return False
        except Exception as e:
            logger.error(f"Ошибка при отправке ЛС: {e}")
            return False
    
    
    async def send_message(recipient_id, text, keyboard=None, is_user=False):
        if is_user:
            url = f"{MAX_API_URL}/messages?user_id={recipient_id}"
        else:
            url = f"{MAX_API_URL}/messages?chat_id={recipient_id}"
        
        headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
        payload = {"text": text}
        if keyboard:
            payload["attachments"] = [
                {
                    "type": "inline_keyboard",
                    "payload": {
                        "buttons": [
                            [
                                {
                                    "type": "callback",
                                    "text": keyboard["text"],
                                    "payload": keyboard["payload"]
                                }
                            ]
                        ]
                    }
                }
            ]
        
        try:
            async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                    if response.status == 200:
                        data = await response.json()
                        message_id = data.get("message", {}).get("body", {}).get("mid")
                        logger.info(f"Сообщение отправлено, ID: {message_id}")
                        return message_id
                    else:
                        error = await response.text()
                        logger.error(f"Ошибка отправки: {error}")
                        return None
        except asyncio.TimeoutError:
            logger.error(f"Таймаут при отправке сообщения")
            return None
        except Exception as e:
            logger.error(f"Ошибка при отправке сообщения: {e}")
            return None
    
    
    async def delete_message(chat_id, message_id):
        url = f"{MAX_API_URL}/messages?message_id={message_id}"
        headers = {"Authorization": BOT_TOKEN}
        
        try:
            async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                async with session.delete(url, headers=headers, timeout=30) as response:
                    if response.status == 200:
                        data = await response.json()
                        if data.get("success", False):
                            logger.info(f"Сообщение {message_id} удалено")
                            return True
                    return False
        except asyncio.TimeoutError:
            logger.error(f"Таймаут при удалении сообщения {message_id}")
            return False
        except Exception as e:
            logger.error(f"Ошибка при удалении сообщения: {e}")
            return False
    
    async def answer_callback(callback_id, notification_text=None):
        url = f"{MAX_API_URL}/answers?callback_id={callback_id}"
        headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
        
        payload = {}
        if notification_text:
            payload["notification"] = notification_text
        else:
            payload["notification"] = "✅"  # пустое уведомление
        
        try:
            async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                async with session.post(url, headers=headers, json=payload, timeout=30) as response:
                    if response.status != 200:
                        error = await response.text()
                        logger.error(f"Ошибка при ответе на callback: {error}")
        except asyncio.TimeoutError:
            logger.error(f"Таймаут при ответе на callback {callback_id}")
        except Exception as e:
            logger.error(f"Ошибка отправки уведомления: {e}")
    
    async def pin_message(chat_id, message_id):
        url = f"{MAX_API_URL}/chats/{chat_id}/pin"
        headers = {"Authorization": BOT_TOKEN, "Content-Type": "application/json"}
        payload = {"message_id": message_id}
        
        try:
            async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                async with session.put(url, headers=headers, json=payload, timeout=30) as response:
                    if response.status == 200:
                        logger.info(f"Сообщение {message_id} закреплено в чате {chat_id}")
                        return True
                    else:
                        error = await response.text()
                        logger.error(f"Ошибка закрепления сообщения: {error}")
                        return False
        except asyncio.TimeoutError:
            logger.error(f"Таймаут при закреплении сообщения {message_id}")
            return False
        except Exception as e:
            logger.error(f"Ошибка при закреплении сообщения: {e}")
            return False
    
    # ============================================================
    # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
    # ============================================================
    
    async def send_welcome_and_button(user_id):
        """Отправляет приветствие и кнопку пользователю в ЛС."""
        # Проверяем, состоит ли пользователь хотя бы в одной организации
        user_orgs = await get_user_organizations(user_id)
        
        if not user_orgs:
            await send_private_message(
                user_id,
                "⛔ Вы не являетесь сотрудником ни одной из организаций, участвующих в акции.\n\n"
                "Пожалуйста, вступите в группу вашей организации и попробуйте снова."
            )
            return
        
        await send_private_message(
            user_id,
            "☕ Рад видеть тебя в нашей кофейне! Жми кнопку и забирай промокод!"
        )
        await send_message(
            user_id,
            "☕ Нажми на кнопку, чтобы получить промокод на скидку!",
            {"text": "☕ Хочу скидку!", "payload": "get_discount"},
            is_user=True
        )
    
    async def is_user_admin(user_id, chat_id):
        """Проверяет, является ли пользователь администратором в любой организации"""
        # Если передан chat_id, сначала проверяем конкретную организацию
        if chat_id:
            org = await get_organization_by_chat_id(chat_id)
            if org:
                return await _is_admin_in_chat(user_id, chat_id)
        
        # Если chat_id не передан или организация не найдена — проверяем все
        orgs = await get_all_organizations()
        for org in orgs:
            if await _is_admin_in_chat(user_id, org['chat_id']):
                return True
        return False
    
    async def _is_admin_in_chat(user_id, chat_id):
        """Вспомогательная функция для проверки админа в конкретном чате"""
        url = f"{MAX_API_URL}/chats/{chat_id}/members"
        headers = {"Authorization": BOT_TOKEN}
        
        try:
            async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                async with session.get(url, headers=headers, timeout=30) as response:
                    if response.status != 200:
                        return False
                    data = await response.json()
                    for member in data.get("members", []):
                        if member.get("user_id") == user_id:
                            return member.get("is_owner", False) or member.get("is_admin", False)
                    return False
        except Exception as e:
            logger.error(f"Ошибка проверки прав пользователя: {e}")
            return False
    
    async def is_user_in_chat(user_id, chat_id):
        logger.info(f"🔍 is_user_in_chat: user_id={user_id}, chat_id={chat_id}")
        url = f"{MAX_API_URL}/chats/{chat_id}/members"
        headers = {"Authorization": BOT_TOKEN}
        
        try:
            async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                async with session.get(url, headers=headers, timeout=30) as response:
                    if response.status == 200:
                        data = await response.json()
                        for member in data.get("members", []):
                            if member.get("user_id") == user_id:
                                logger.info(f"✅ Пользователь {user_id} найден в чате {chat_id}")
                                return True
                        logger.info(f"❌ Пользователь {user_id} не найден в чате {chat_id}")
                        return False
                    else:
                        logger.error(f"❌ Ошибка проверки членства: {response.status} - {await response.text()}")
                        return False
        except Exception as e:
            logger.error(f"❌ Ошибка в is_user_in_chat: {e}")
            return False
    
    # ============================================================
    # ОБРАБОТЧИКИ КОМАНД И CALLBACK
    # ============================================================
    
    async def handle_command(chat_id, user_id, user_name, command):
        is_admin = await is_user_admin(user_id, CHAT_ID)
        is_bar = await is_barista(user_id)
    
        if command == "/start":
            await send_welcome_and_button(user_id)
            return
    
        if command == "/help":
            if is_admin:
                await send_private_message(user_id, HELP_TEXT_ADMIN)
            elif is_bar:
                await send_private_message(user_id, HELP_TEXT_BARISTA)
            else:
                await send_private_message(user_id, HELP_TEXT_USER)
            return
    
        if command.startswith("/use"):
            if not is_bar:
                if is_admin:
                    await send_private_message(user_id, HELP_TEXT_ADMIN)
                else:
                    await send_private_message(user_id, HELP_TEXT_USER)
                return
    
            code = command.replace("/use", "").strip()
            if not code:
                await send_private_message(user_id, "❌ Формат: /use LIGO-XXXX")
                return
    
            promo = await get_code_by_text(code)
            if not promo:
                await send_private_message(user_id, "❌ Промокод не найден")
                return
    
            if promo["status"] == "used":
                await send_private_message(
                    user_id,
                    f"❌ Промокод уже был использован {promo['used_at']}"
                )
                await send_private_message(
                    promo["user_id"],
                    f"❌ Ваш промокод {code} был использован. Запросите новый."
                )
                return
    
            await use_promocode(code, user_id)
            await send_private_message(
                user_id,
                f"✅ Промокод {code} активирован! Скидка предоставлена."
            )
            await send_private_message(
                promo["user_id"],
                f"✅ Ваш промокод {code} был успешно использован. Можете запросить новый."
            )
            await send_welcome_and_button(promo["user_id"])
            return
    
        if not is_admin:
            if is_bar:
                await send_private_message(user_id, HELP_TEXT_BARISTA)
            else:
                await send_private_message(user_id, HELP_TEXT_USER)
            return
    
        if command.startswith("/add_barista"):
            parts = command.split()
            if len(parts) < 3:
                await send_private_message(user_id, "❌ Формат: /add_barista <user_id> <name>")
                return
            try:
                barista_id = int(parts[1])
                name = " ".join(parts[2:])
                await add_barista(barista_id, name, user_id)
                await send_private_message(user_id, f"✅ Бариста {name} (ID {barista_id}) добавлен.")
            except ValueError:
                await send_private_message(user_id, "❌ ID должен быть числом.")
            return
    
        if command.startswith("/remove_barista"):
            parts = command.split()
            if len(parts) < 2:
                await send_private_message(user_id, "❌ Формат: /remove_barista <user_id>")
                return
            try:
                barista_id = int(parts[1])
                await remove_barista(barista_id)
                await send_private_message(user_id, f"✅ Бариста (ID {barista_id}) удалён.")
            except ValueError:
                await send_private_message(user_id, "❌ ID должен быть числом.")
            return
    
        if command == "/list_baristas":
            baristas = await get_all_baristas()
            if not baristas:
                await send_private_message(user_id, "📭 Список барист пуст.")
                return
            text = "🧑‍🍳 Список барист:\n"
            for b in baristas:
                text += f"- {b['name']} (ID: {b['user_id']})\n"
            await send_private_message(user_id, text)
            return
    
        if command == "/stats":
            # Определяем организацию по chat_id (если команда из группы)
            org = await get_organization_by_chat_id(chat_id)
            if not org:
                # Если команда из ЛС — пытаемся найти организацию по последнему промокоду
                # или просто показываем общую статистику
                stats = await get_stats_by_org(None)  # если реализована общая статистика
                text = "📊 Общая статистика:\n"
            else:
                stats = await get_stats_by_org(org['id'])
                text = f"📊 Статистика для {org['name']}:\n"
            
            text += (
                f"Всего выпущено: {stats['total']}\n"
                f"Активных: {stats['active']}\n"
                f"Использовано: {stats['used']}"
            )
            await send_private_message(user_id, text)
            return
    
        if command.startswith("/revoke"):
            parts = command.split()
            if len(parts) < 2:
                await send_private_message(user_id, "❌ Формат: /revoke LIGO-XXXX")
                return
            code = parts[1].upper()
            promo = await get_code_by_text(code)
            if not promo:
                await send_private_message(user_id, "❌ Промокод не найден.")
                return
            if promo["status"] == "used":
                await send_private_message(user_id, "❌ Этот промокод уже использован.")
                return
            await use_promocode(code, user_id)
            await send_private_message(user_id, f"✅ Промокод {code} отозван.")
            return
    
        if command == "/restore":
            await send_welcome_and_button(user_id)
            return
    
        if is_admin:
            await send_private_message(user_id, HELP_TEXT_ADMIN)
        elif is_bar:
            await send_private_message(user_id, HELP_TEXT_BARISTA)
        else:
            await send_private_message(user_id, HELP_TEXT_USER)
    
    async def handle_callback(chat_id, user_id, user_name, callback_data, callback_id):
        try:
            answered = False
            
            if callback_data == "get_discount":
                # ✅ 1. ПРОВЕРЯЕМ, ЕСТЬ ЛИ УЖЕ АКТИВНЫЙ ПРОМОКОД
                existing = await get_active_code(user_id)
                if existing:
                    code = existing["code"]
                    qr_b64 = existing.get("qr_base64") or generate_qr_base64(code)
                    
                    await send_private_message(
                        user_id,
                        f"☕ У вас уже есть активный промокод: *{code}*"
                    )
                    
                    result = await send_photo(user_id, qr_b64, f"☕ Ваш промокод: *{code}*")
                    
                    if result == "blocked":
                        if not answered:
                            await answer_callback(callback_id)
                            answered = True
                        return
                    
                    elif result == "imgbb_failed":
                        await send_private_message(
                            user_id,
                            f"☕ Ваш промокод: *{code}*\n\n"
                            f"Извините, QR-код временно недоступен. 😇\n"
                            f"Покажите баристе текстовый промокод — он тоже принимается!"
                        )
                        
                        await send_private_message(
                            chat_id,
                            f"🚨 **КРИТИЧЕСКАЯ ОШИБКА!**\n\n"
                            f"❌ Сервис загрузки изображений (ImgBB) недоступен!\n"
                            f"📍 Пользователь: {user_name} (ID: {user_id})\n"
                            f"📅 Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
                            f"🔧 Срочно проверьте доступность ImgBB и API-ключ!"
                        )
                        
                        if not answered:
                            await answer_callback(callback_id)
                            answered = True
                        return
                    
                    if not answered:
                        await answer_callback(callback_id)
                        answered = True
                    return
                
                # ✅ 2. ПОЛУЧАЕМ СПИСОК ОРГАНИЗАЦИЙ, В КОТОРЫХ СОСТОИТ ПОЛЬЗОВАТЕЛЬ
                user_orgs = await get_user_organizations(user_id)
                logger.info(f"🔍 Организации пользователя {user_id}: {[org['name'] for org in user_orgs]}")
                
                if not user_orgs:
                    await send_private_message(
                        user_id,
                        "❌ Вы не состоите ни в одной из групп, участвующих в акции.\n"
                        "Пожалуйста, вступите в группу вашей организации и попробуйте снова."
                    )
                    return
                
                # ✅ 3. ФОРМИРУЕМ ПРЕФИКС ИЗ ВСЕХ ОРГАНИЗАЦИЙ
                prefixes = ''.join([org['prefix'] for org in user_orgs])
                org_id = user_orgs[0]['id']  # для сохранения берём первую организацию
                
                # ✅ 4. ГЕНЕРИРУЕМ КОД С ОБЩИМ ПРЕФИКСОМ
                code = generate_code(prefixes)
                qr_b64 = generate_qr_base64(code)
                
                logger.info(f"🔍 Генерация кода: prefix={prefixes}, code={code}, org_id={org_id}")
                
                # ✅ 5. СОХРАНЯЕМ С organization_id
                await add_promocode(user_id, user_name, code, org_id, qr_b64)
                
                # ✅ 6. ОТПРАВЛЯЕМ QR-КОД
                result = await send_photo(user_id, qr_b64, f"☕ Ваш промокод: *{code}*")
                
                if result == "blocked":
                    if not answered:
                        await answer_callback(callback_id)
                        answered = True
                    return
                
                elif result == "imgbb_failed":
                    await send_private_message(
                        user_id,
                        f"☕ Ваш промокод: *{code}*\n\n"
                        f"Извините, QR-код временно недоступен. 😇\n"
                        f"Покажите баристе текстовый промокод — он тоже принимается!"
                    )
                    
                    await send_private_message(
                        chat_id,
                        f"🚨 **КРИТИЧЕСКАЯ ОШИБКА!**\n\n"
                        f"❌ Сервис загрузки изображений (ImgBB) недоступен!\n"
                        f"📍 Пользователь: {user_name} (ID: {user_id})\n"
                        f"📅 Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
                        f"🔧 Срочно проверьте доступность ImgBB и API-ключ!"
                    )
                    
                    if not answered:
                        await answer_callback(callback_id)
                        answered = True
                    return
                
                if not answered:
                    await answer_callback(callback_id)
                    answered = True
                    
        except Exception as e:
            logger.error(f"Ошибка обработки callback: {e}")
            if not answered:
                try:
                    await answer_callback(callback_id)
                except:
                    pass
    # ============================================================
    # ОСНОВНАЯ ЛОГИКА ОБРАБОТКИ ОБНОВЛЕНИЙ
    # ============================================================
    
    async def process_update(update):
        try:
            if update.get("update_type") == "bot_started":
                user_id = update.get("user_id")
                if not user_id:
                    user_id = update.get("user", {}).get("user_id")
                if not user_id:
                    user_id = update.get("from", {}).get("user_id")
                
                user_name = update.get("user", {}).get("first_name") or update.get("from", {}).get("first_name") or "Пользователь"
                logger.info(f"🚀 Пользователь {user_name} (ID: {user_id}) активировал бота")
                await send_welcome_and_button(user_id)
                return
    
            if "callback" in update:
                try:
                    callback = update["callback"]
                    user_id = callback["user"]["user_id"]
                    user_name = callback["user"].get("first_name", "Сотрудник")
                    callback_data = callback.get("payload")
                    callback_id = callback.get("callback_id")
                    
                    # ✅ ДИАГНОСТИКА: выводим всё обновление
                    logger.info(f"📦 Полный callback update: {json.dumps(update, ensure_ascii=False, indent=2)}")
                    
                    # Пробуем получить chat_id разными способами
                    chat_id = update.get("message", {}).get("recipient", {}).get("chat_id")
                    if not chat_id:
                        chat_id = update.get("chat_id")
                    if not chat_id:
                        chat_id = callback.get("chat_id")
                    
                    logger.info(f"🔍 Определён chat_id: {chat_id}")
                    
                    await handle_callback(chat_id, user_id, user_name, callback_data, callback_id)
                    return
                except Exception as e:
                    logger.error(f"Ошибка обработки callback: {e}")
                    logger.error(f"Получено update: {update}")
    
    
            elif "message" in update:
                try:
                    message = update["message"]
                    sender = message.get("sender")
                    if not sender:
                        return
    
                    user_id = sender.get("user_id")
                    user_name = sender.get("first_name", "Сотрудник")
                    text = message.get("body", {}).get("text", "")
                    chat_id = message.get("recipient", {}).get("chat_id")
                    message_id = message.get("body", {}).get("mid")
    
                    if chat_id != CHAT_ID:
                        logger.info(f"ЛС от {user_id}: {text[:30] if text else '<фото>'} (chat_id={chat_id})")
                        
                        is_admin = await is_user_admin(user_id, CHAT_ID)
                        is_bar = await is_barista(user_id)
                        
                        if text and text.startswith("/"):
                            await handle_command(chat_id, user_id, user_name, text)
                            return
                        
                        if is_bar:
                            attachments = message.get("body", {}).get("attachments", [])
                            photo_attachments = [att for att in attachments if att.get("type") == "image"]
                            
                            if photo_attachments:
                                logger.info(f"Получено фото от баристы {user_id}, распознаём QR...")
                                photo_url = photo_attachments[0].get("payload", {}).get("url")
                                if not photo_url:
                                    await send_private_message(user_id, "❌ Не удалось получить фото")
                                    return
                                
                                try:
                                    async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                                        async with session.get(photo_url, timeout=30) as resp:
                                            if resp.status != 200:
                                                await send_private_message(user_id, "❌ Не удалось скачать фото")
                                                return
                                            image_data = await resp.read()
                                except Exception as e:
                                    await send_private_message(user_id, f"❌ Ошибка загрузки фото: {e}")
                                    return
                                
                                code = await decode_qr_from_image(image_data)
                                if not code:
                                    await send_private_message(user_id, "❌ Не удалось распознать QR-код")
                                    return
                                
                                logger.info(f"Распознан QR: {code}")
                                
                                promo = await get_code_by_text(code)
                                if not promo:
                                    await send_private_message(user_id, "❌ Промокод не найден")
                                    return
                                
                                if promo["status"] == "used":
                                    await send_private_message(
                                        user_id,
                                        f"❌ Промокод уже был использован {promo['used_at']}"
                                    )
                                    await send_private_message(
                                        promo["user_id"],
                                        f"❌ Ваш промокод {code} был использован. Запросите новый."
                                    )
                                    return
                                
                                try:
                                    await use_promocode(code, user_id)
                                    await send_private_message(
                                        user_id,
                                        f"✅ Промокод {code} активирован! Скидка предоставлена."
                                    )
                                    await send_private_message(
                                        promo["user_id"],
                                        f"✅ Ваш промокод {code} был успешно использован. Можете запросить новый."
                                    )
                                    await send_welcome_and_button(promo["user_id"])
                                except Exception as e:
                                    logger.error(f"Ошибка активации промокода: {e}")
                                    await send_private_message(
                                        user_id,
                                        "❌ Произошла ошибка при активации промокода. Попробуйте позже."
                                    )
                                return
                            
                            if text and text.strip():
                                code = text.strip()
                                promo = await get_code_by_text(code)
                                if promo:
                                    if promo["status"] == "used":
                                        await send_private_message(
                                            user_id,
                                            f"❌ Промокод уже был использован {promo['used_at']}"
                                        )
                                        await send_private_message(
                                            promo["user_id"],
                                            f"❌ Ваш промокод {code} был использован. Запросите новый."
                                        )
                                        return
                                    
                                    try:
                                        await use_promocode(code, user_id)
                                        await send_private_message(
                                            user_id,
                                            f"✅ Промокод {code} активирован! Скидка предоставлена."
                                        )
                                        await send_private_message(
                                            promo["user_id"],
                                            f"✅ Ваш промокод {code} был успешно использован. Можете запросить новый."
                                        )
                                        await send_welcome_and_button(promo["user_id"])
                                    except Exception as e:
                                        logger.error(f"Ошибка активации промокода: {e}")
                                        await send_private_message(
                                            user_id,
                                            "❌ Произошла ошибка при активации промокода. Попробуйте позже."
                                        )
                                    return
                        
                        try:
                            if is_admin:
                                await send_private_message(user_id, HELP_TEXT_ADMIN)
                            elif is_bar:
                                await send_private_message(user_id, HELP_TEXT_BARISTA)
                            else:
                                await send_private_message(user_id, HELP_TEXT_USER)
                                await send_welcome_and_button(user_id)
                        except Exception as e:
                            logger.error(f"Ошибка отправки справки: {e}")
                        return
    
                    # --- ГРУППА ---
                    if chat_id != CHAT_ID:  # если чат не ЛИГО, но всё равно обрабатываем
                        # Проверяем, зарегистрирована ли организация
                        org = await get_organization_by_chat_id(chat_id)
                        if not org:
                            logger.info(f"⚠️ Неизвестная группа {chat_id}, сообщение от {user_id}: {text}")
                            # Можно отправить уведомление админу или просто игнорировать
                            return
    
                    logger.info(f"📩 Группа от {user_id} (chat_id={chat_id}): {text}")
    
                    try:
                        is_admin = await is_user_admin(user_id, chat_id)
                        
                        if is_admin:
                            # Администратор — логируем, но не удаляем
                            logger.info(f"👑 Администратор {user_name} написал в группе {chat_id}: {text}")
                            # Если админ написал команду — обрабатываем
                            if text and text.startswith("/"):
                                await handle_command(chat_id, user_id, user_name, text)
                        else:
                            # Не-админ — удаляем сообщение
                            if message_id:
                                logger.info(f"🗑️ Удаляем сообщение от не-админа {user_id} в группе {chat_id}")
                                await delete_message(chat_id, message_id)
                    except Exception as e:
                        logger.error(f"Ошибка обработки сообщения из группы {chat_id}: {e}")
                    return
    
                except Exception as e:
                    logger.error(f"Ошибка обработки сообщения: {e}")
                    logger.error(f"Получено update: {update}")
    
            else:
                logger.info(f"Неизвестный тип обновления: {update.get('update_type')}")
    
        except Exception as e:
            logger.error(f"Критическая ошибка обработки update: {e}")
            logger.error(f"Получено update: {update}")
    
    
    async def get_user_organizations(user_id: int):
        orgs = await get_all_organizations()
        user_orgs = []
        
        for org in orgs:
            is_member = await is_user_in_chat(user_id, org['chat_id'])
            logger.info(f"🔍 Проверка членства в {org['name']} (chat_id={org['chat_id']}): {is_member}")
            if is_member:
                user_orgs.append(org)
        
        return user_orgs
    
    
    
    # ============================================================
    # ЗАПУСК БОТА
    # ============================================================
    
    async def listen():
        url = f"{MAX_API_URL}/updates"
        headers = {"Authorization": BOT_TOKEN}
        last_update_id = 0
        
        while True:
            try:
                params = {"timeout": 30, "offset": last_update_id + 1}
                async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
                    async with session.get(url, headers=headers, params=params, timeout=30) as response:
                        if response.status == 200:
                            data = await response.json()
                            for update in data.get("updates", []):
                                await process_update(update)
                                if update.get("update_id", 0) > last_update_id:
                                    last_update_id = update["update_id"]
                        else:
                            error = await response.text()
                            logger.error(f"Ошибка получения обновлений: {error}")
                            await asyncio.sleep(5)
            except asyncio.TimeoutError:
                logger.warning("Таймаут при получении обновлений, пробую снова...")
                await asyncio.sleep(5)
            except Exception as e:
                logger.error(f"Ошибка в listen: {e}")
                logger.error(traceback.format_exc())
                await asyncio.sleep(5)
    
    # ============================================================
    # ЗАПУСК БОТА
    # ============================================================
    
    async def main():
        logger.info("🚀 Бот запущен!")
        await init_db()
        
        # ✅ ДИАГНОСТИКА: выводим все организации из БД
        try:
            from database import get_all_organizations
            orgs = await get_all_organizations()
            if orgs:
                logger.info(f"📋 Найдено организаций: {len(orgs)}")
                for org in orgs:
                    logger.info(f"  🏢 {org['name']} (ID: {org['id']}, chat_id: {org['chat_id']}, префикс: {org['prefix']})")
            else:
                logger.warning("⚠️ В БД нет ни одной организации!")
        except Exception as e:
            logger.error(f"❌ Ошибка при чтении организаций: {e}")
        
        print("=" * 50)
        print("☕ Бот Скидки на Напитки от \"Кофий\"")
        print("  Бот работает с БД и QR-кодами")
        print("  Команды для админа (в группе или ЛС):")
        print("    /stats - статистика")
        print("    /add_barista ID Имя - добавить баристу")
        print("    /remove_barista ID - удалить баристу")
        print("    /list_baristas - список барист")
        print("    /restore - восстановить кнопку")
        print("    /revoke LIGO-XXXX - отозвать промокод")
        print("  Команды для баристы (в ЛС):")
        print("    /use LIGO-XXXX - погасить промокод")
        print("    Или просто отправить фото с QR-кодом")
        print("=" * 50)
        
        await listen()
    
    # ============================================================
    # ТОЧКА ВХОДА
    # ============================================================
    
    if __name__ == "__main__":
        try:
            print("🚀 Запуск бота...")
            asyncio.run(main())
        except KeyboardInterrupt:
            print("\n⏹ Бот остановлен")
        except Exception as e:
            print(f"❌ КРИТИЧЕСКАЯ ОШИБКА: {e}")
            import traceback
            traceback.print_exc()
    config.py
    ---------
    import os
    from dotenv import load_dotenv
    
    load_dotenv()
    
    BOT_TOKEN = os.getenv("BOT_TOKEN")
    CHAT_ID = int(os.getenv("CHAT_ID"))
    BOT_ID = int(os.getenv("BOT_ID"))
    IMGBB_API_KEY = os.getenv("IMGBB_API_KEY")
    IMGBB_API_URL = os.getenv("IMGBB_API_URL")
    GROUP_INVITE_LINK = os.getenv("GROUP_INVITE_LINK")
    MAX_API_URL = os.getenv("MAX_API_URL")
    LOG_RETENTION_DAYS = int(os.getenv("LOG_RETENTION_DAYS", 30))  # <-- ДОБАВИТЬ, значение по умолчанию 30
    
    # Проверяем, что все переменные загружены
    if not all([BOT_TOKEN, CHAT_ID, BOT_ID, IMGBB_API_KEY, IMGBB_API_URL, GROUP_INVITE_LINK, MAX_API_URL]):
        raise ValueError("❌ Ошибка: не все переменные окружения загружены. Проверьте файл .env")
    database.py
    -----------
    import aiosqlite
    import os
    import random
    import string
    from datetime import datetime
    
    # Путь к БД в корне проекта
    DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "promocodes.db")
    
    # --- ИНИЦИАЛИЗАЦИЯ БАЗЫ ---
    
    async def init_db():
        async with aiosqlite.connect(DB_PATH) as db:
            await db.execute('''
                CREATE TABLE IF NOT EXISTS organizations (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT NOT NULL,
                    prefix TEXT NOT NULL UNIQUE,
                    chat_id INTEGER UNIQUE NOT NULL,
                    invite_link TEXT,
                    is_active BOOLEAN DEFAULT 1,
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            await db.execute('''
                CREATE TABLE IF NOT EXISTS promocodes (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    code TEXT UNIQUE NOT NULL,
                    user_id INTEGER NOT NULL,
                    user_name TEXT,
                    organization_id INTEGER NOT NULL,
                    issued_at DATETIME NOT NULL,
                    used_at DATETIME,
                    used_by INTEGER,
                    status TEXT DEFAULT 'active',
                    qr_base64 TEXT,
                    FOREIGN KEY (organization_id) REFERENCES organizations(id)
                )
            ''')
            await db.execute('''
                CREATE TABLE IF NOT EXISTS baristas (
                    id INTEGER PRIMARY KEY,
                    name TEXT,
                    added_by INTEGER,
                    added_at DATETIME DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            await db.commit()
            print("✅ База данных инициализирована (мульти-орг)")
    
    # --- ОРГАНИЗАЦИИ ---
    
    async def get_organization_by_chat_id(chat_id: int):
        async with aiosqlite.connect(DB_PATH) as db:
            cursor = await db.execute(
                "SELECT id, name, prefix FROM organizations WHERE chat_id = ? AND is_active = 1",
                (chat_id,)
            )
            row = await cursor.fetchone()
            if row:
                return {"id": row[0], "name": row[1], "prefix": row[2]}
            return None
    
    async def add_organization(name: str, prefix: str, chat_id: int, invite_link: str = None):
        async with aiosqlite.connect(DB_PATH) as db:
            await db.execute(
                "INSERT INTO organizations (name, prefix, chat_id, invite_link) VALUES (?, ?, ?, ?)",
                (name, prefix, chat_id, invite_link)
            )
            await db.commit()
    
    async def get_all_organizations():
        async with aiosqlite.connect(DB_PATH) as db:
            cursor = await db.execute("SELECT id, name, prefix, chat_id, invite_link, is_active FROM organizations")
            rows = await cursor.fetchall()
            return [
                {
                    "id": row[0],
                    "name": row[1],
                    "prefix": row[2],
                    "chat_id": row[3],
                    "invite_link": row[4],
                    "is_active": bool(row[5])
                }
                for row in rows
            ]
    
    # --- ПРОМОКОДЫ (С ПРИВЯЗКОЙ К ОРГАНИЗАЦИИ) ---
    
    async def add_promocode(user_id: int, user_name: str, code: str, org_id: int, qr_base64: str = None):
        print(f"🔍 add_promocode: user_id={user_id}, code={code}, org_id={org_id}")
        async with aiosqlite.connect(DB_PATH) as db:
            await db.execute(
                """INSERT INTO promocodes 
                   (code, user_id, user_name, organization_id, issued_at, status, qr_base64) 
                   VALUES (?, ?, ?, ?, ?, ?, ?)""",
                (code, user_id, user_name, org_id, datetime.now(), "active", qr_base64)
            )
            await db.commit()
            print(f"✅ Промокод {code} сохранён в БД")
    
    async def get_active_code(user_id: int):
        async with aiosqlite.connect(DB_PATH) as db:
            cursor = await db.execute(
                """SELECT code, issued_at, qr_base64, organization_id 
                   FROM promocodes 
                   WHERE user_id = ? AND status = 'active' 
                   ORDER BY issued_at DESC LIMIT 1""",
                (user_id,)
            )
            row = await cursor.fetchone()
            if row:
                return {
                    "code": row[0],
                    "issued_at": row[1],
                    "qr_base64": row[2],
                    "organization_id": row[3]
                }
            return None
    
    async def get_code_by_text(code: str):
        async with aiosqlite.connect(DB_PATH) as db:
            cursor = await db.execute("SELECT * FROM promocodes WHERE code = ?", (code,))
            row = await cursor.fetchone()
            if row:
                return {
                    "id": row[0],
                    "code": row[1],
                    "user_id": row[2],
                    "user_name": row[3],
                    "organization_id": row[4],
                    "issued_at": row[5],
                    "used_at": row[6],
                    "used_by": row[7],
                    "status": row[8],
                    "qr_base64": row[9]
                }
            return None
    
    async def use_promocode(code: str, barista_id: int):
        async with aiosqlite.connect(DB_PATH) as db:
            await db.execute(
                "UPDATE promocodes SET status = 'used', used_at = ?, used_by = ? WHERE code = ?",
                (datetime.now(), barista_id, code)
            )
            await db.commit()
    
    async def get_stats_by_org(org_id: int):
        async with aiosqlite.connect(DB_PATH) as db:
            total = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE organization_id = ?", (org_id,))
            active = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE organization_id = ? AND status = 'active'", (org_id,))
            used = await db.execute_fetchall("SELECT COUNT(*) FROM promocodes WHERE organization_id = ? AND status = 'used'", (org_id,))
            return {
                "total": total[0][0] if total else 0,
                "active": active[0][0] if active else 0,
                "used": used[0][0] if used else 0
            }
    
    # --- ГЕНЕРАТОР КОДА С ПРЕФИКСОМ ---
    
    def generate_code(prefix: str) -> str:
        import sqlite3
        while True:
            part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
            code = f"{prefix}{part}"
            conn = sqlite3.connect(DB_PATH)
            cursor = conn.execute("SELECT COUNT(*) FROM promocodes WHERE code = ?", (code,))
            count = cursor.fetchone()[0]
            conn.close()
            if count == 0:
                return code
    
    # --- БАРИСТЫ ---
    
    async def is_barista(user_id: int) -> bool:
        async with aiosqlite.connect(DB_PATH) as db:
            cursor = await db.execute("SELECT id FROM baristas WHERE id = ?", (user_id,))
            row = await cursor.fetchone()
            return row is not None
    
    async def add_barista(user_id: int, name: str, added_by: int):
        async with aiosqlite.connect(DB_PATH) as db:
            await db.execute(
                "INSERT OR REPLACE INTO baristas (id, name, added_by, added_at) VALUES (?, ?, ?, ?)",
                (user_id, name, added_by, datetime.now())
            )
            await db.commit()
    

  promocodes.db
  -------------
  [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
  qr_generator.py
  ---------------
  import qrcode
  from io import BytesIO
  import base64
  
  def generate_qr_base64(code: str) -> str:
      qr = qrcode.QRCode(
          version=1,
          error_correction=qrcode.constants.ERROR_CORRECT_L,
          box_size=10,
          border=4,
      )
      qr.add_data(code)
      qr.make(fit=True)
  
      img = qr.make_image(fill_color="black", back_color="white")
  
      buffer = BytesIO()
      img.save(buffer, format="PNG")
      buffer.seek(0)
  
      img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
      return img_base64
  requirements.txt
  ----------------
  aiohttp
  qrcode[pil]
  aiosqlite
  pillow
  requests
  python-dotenv
  opencv-python-headless
  russian_trusted_root_ca.cer
  ---------------------------
  -----BEGIN CERTIFICATE-----
  MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
  PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
  ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
  Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
  VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
  YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
  dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
  qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
  XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
  zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
  YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
  Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
  U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
  4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
  G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
  BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
  ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
  OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
  BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
  BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
  AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
  tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
  W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
  /3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
  AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
  C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
  4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
  WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
  D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
  EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
  391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
  -----END CERTIFICATE-----
  russian_trusted_root_ca_gost_2025.cer
  -------------------------------------
  [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
  russian_trusted_sub_ca.cer
  --------------------------
  -----BEGIN CERTIFICATE-----
  MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
  PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
  ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
  Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
  VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
  YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
  IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
  wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
  HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
  S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
  vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
  BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
  vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
  L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
  3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
  qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
  Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
  2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
  Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
  XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
  AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
  b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
  eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
  aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
  gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
  b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
  L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
  LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
  AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
  Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
  anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
  cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
  Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
  Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
  h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
  F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
  E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
  GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
  ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
  -----END CERTIFICATE-----
  russian_trusted_sub_ca_2024.cer
  -------------------------------
  -----BEGIN CERTIFICATE-----
  MIIG6DCCBNCgAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
  PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
  ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
  Q0EwHhcNMjQwNzE1MTI1MDQxWhcNMjkwNzE5MTI1MDQxWjBvMQswCQYDVQQGEwJS
  VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
  YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
  IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1j0rkZECOt1S8o7I
  JY+4YKAxuEa5xaHKHXT2EpkuC/0krqMOjUy2oPIRNgR5g8X0Jl6jamxeGLc4Q1tf
  ju6or9oSRYThIUhRsFDQNBiBBEXoBgWxTfiKB2eyT97+pz5TBtBiRCPaLGRHYLRb
  9Jz2HkJlxbtNPjtDrF5DPHym+mZ1M1z3hIQYAqJwLpsEBnsw/VxWMlxqHoeewd0h
  uJMd71KQ5vOKlz7KrIZ6EobNNa6wItuvsfj3kYCK7O78uLHGXXFxdr8Hae9lMUmC
  8F7AFwa+bO1LRlTlqW7rE3rLf+jj70N01N8T3o22v14YBaFBWQWncAVYD2JuL3tH
  252+kdNOERf1fLbLRigJAbd+hOhWYlNf963TFDgnNPliHNIW72SygVBnI2V3JwO1
  dp1hVKpK/zt8ziGdHW4gmOLTsH50YKdR4jNqUgQv4wASlKn9OpN6zHYc5G8h86fY
  BM+zxE5ikGI+I/vIqBuI0eaDU92AWN/YjFLpu8tMu9kLRSCf1vug6FIfDPWVo7iP
  ac/SI2v8jnnpaW7ph/Pz3WkzaG7ZZJsfFs+8dploWc6LOoDtbFBhMdGMxu024msC
  0PSjZb5ODXPIaO2NsA7fMiAtZcoK6anTUJh4zOP/stA9qsJGNxdrEmiPXSmBZY/N
  Y0wkZgZ6JTDhw7038bPvctkblJkCAwEAAaOCAYswggGHMB0GA1UdDgQWBBR3Pdk5
  r0K93FvKduru/c4+YSkwXzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qws
  hzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADCBmAYIKwYBBQUH
  AQEEgYswgYgwQAYIKwYBBQUHMAKGNGh0dHA6Ly9udWMtY2RwLnZvc2tob2QucnUv
  Y2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQwRAYIKwYBBQUHMAKGOGh0dHA6Ly9u
  dWMtY2RwLmRpZ2l0YWwuZ292LnJ1L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3J0
  MIGFBgNVHR8EfjB8MDqgOKA2hjRodHRwOi8vbnVjLWNkcC52b3NraG9kLnJ1L2Nk
  cC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMD6gPKA6hjhodHRwOi8vbnVjLWNkcC5k
  aWdpdGFsLmdvdi5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG
  9w0BAQsFAAOCAgEAmsINXtQ7wwUWvIeOr80MdJS/5G4xhyZOVEmeUorThquT672y
  cCg3XCxc4fwbiZqSSbBqntQ7RtiTAKMYMvBageKoVHbzz+R4jX01tKcTx8cDePrz
  dJ73bLNUorE7RU9QsW4KyiUeRmjMDV23AUlEvuQFTwgkHXvbac1BBdPn9CrssQuF
  5EGohZKcQPFiAAc4SHbRNhlr7uAwgpc/erzI9EAcvA6BVAXcVKoeGpV01uexUgZ6
  St5RP9UmDWNA7T4yVXWJ233N0Q8bl+6AswINQ3PosPu6yQQHQjr65YS06epK+AeI
  6j+oGR4xI7EhTQhQvaobnGmX/8QQ7XDRYCP2HXYxiffnn/CfZ/BVyKLYeY1ZipjE
  nzqdQIC2+Q3WtY8jsVRQMP38WFRmtsIt5snehnPTs5bKGVIcYzj3o3Ex/K7agEz0
  zAJ0JR5ivXZOvNkT0g9x1v+S1IkU3e/nX1a+tpRquMtnHX0L2lXArNHUbaOO9EJt
  d57WaIpofV5cVhhwShOgAuBc9UMJF3/n4t4RKiPxtsK8P67gcmphMhslj7AMYrYM
  ej2NvQZY4m3ub3CPC/PrTjDONvb+8g5xrKtxBjYqC74HSB4dg9G3WimSDUuP2Su6
  G2y2TUeyJuCvCLz289VoO0vg7cNdMobE3KCqAiiNhN2VBFxHAUKmUoRcRdw=
  -----END CERTIFICATE-----

  russian_trusted_sub_ca_gost_2025.cer
  ------------------------------------
  [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
  str
  ---


  .git/
    .COMMIT_EDITMSG.swp
    -------------------
    [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
    .MERGE_MSG.swp
    --------------
    [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
    COMMIT_EDITMSG
    --------------
    Добавлена БД с организациями

    FETCH_HEAD
    ----------
    b68a10f027c4d6d2a7838f44d4230d7d7dae8be5		branch 'main' of https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo

    HEAD
    ----
    ref: refs/heads/main

    ORIG_HEAD
    ---------
    69ac457a2a0802cb016aa995e5046a8b7c4d8993

    config
    ------
    [core]
    	repositoryformatversion = 0
    	filemode = false
    	bare = false
    	logallrefupdates = true
    	symlinks = false
    	ignorecase = true
    [remote "origin"]
    	url = https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo.git
    	fetch = +refs/heads/*:refs/remotes/origin/*
    [branch "main"]
    	remote = origin
    	merge = refs/heads/main
    	vscode-merge-base = origin/main
    [branch "feature/multi-org"]
    	vscode-merge-base = origin/main

    description
    -----------
    Unnamed repository; edit this file 'description' to name the repository.

    index
    -----
    [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
    packed-refs
    -----------
    # pack-refs with: peeled fully-peeled sorted 
    d1d77b9931a61809179c4ba0ed004e5aa26bc21e refs/remotes/origin/main


    hooks/
      applypatch-msg.sample
      ---------------------
      #!/bin/sh
      #
      # An example hook script to check the commit log message taken by
      # applypatch from an e-mail message.
      #
      # The hook should exit with non-zero status after issuing an
      # appropriate message if it wants to stop the commit.  The hook is
      # allowed to edit the commit message file.
      #
      # To enable this hook, rename this file to "applypatch-msg".
      
      . git-sh-setup
      commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
      test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
      :

      commit-msg.sample
      -----------------
      #!/bin/sh
      #
      # An example hook script to check the commit log message.
      # Called by "git commit" with one argument, the name of the file
      # that has the commit message.  The hook should exit with non-zero
      # status after issuing an appropriate message if it wants to stop the
      # commit.  The hook is allowed to edit the commit message file.
      #
      # To enable this hook, rename this file to "commit-msg".
      
      # Uncomment the below to add a Signed-off-by line to the message.
      # Doing this in a hook is a bad idea in general, but the prepare-commit-msg
      # hook is more suited to it.
      #
      # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
      # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
      
      # This example catches duplicate Signed-off-by lines.
      
      test "" = "$(grep '^Signed-off-by: ' "$1" |
      	 sort | uniq -c | sed -e '/^[ 	]*1[ 	]/d')" || {
      	echo >&2 Duplicate Signed-off-by lines.
      	exit 1
      }

      fsmonitor-watchman.sample
      -------------------------
      #!/usr/bin/perl
      
      use strict;
      use warnings;
      use IPC::Open2;
      
      # An example hook script to integrate Watchman
      # (https://facebook.github.io/watchman/) with git to speed up detecting
      # new and modified files.
      #
      # The hook is passed a version (currently 2) and last update token
      # formatted as a string and outputs to stdout a new update token and
      # all files that have been modified since the update token. Paths must
      # be relative to the root of the working tree and separated by a single NUL.
      #
      # To enable this hook, rename this file to "query-watchman" and set
      # 'git config core.fsmonitor .git/hooks/query-watchman'
      #
      my ($version, $last_update_token) = @ARGV;
      
      # Uncomment for debugging
      # print STDERR "$0 $version $last_update_token\n";
      
      # Check the hook interface version
      if ($version ne 2) {
      	die "Unsupported query-fsmonitor hook version '$version'.\n" .
      	    "Falling back to scanning...\n";
      }
      
      my $git_work_tree = get_working_dir();
      
      my $retry = 1;
      
      my $json_pkg;
      eval {
      	require JSON::XS;
      	$json_pkg = "JSON::XS";
      	1;
      } or do {
      	require JSON::PP;
      	$json_pkg = "JSON::PP";
      };
      
      launch_watchman();
      
      sub launch_watchman {
      	my $o = watchman_query();
      	if (is_work_tree_watched($o)) {
      		output_result($o->{clock}, @{$o->{files}});
      	}
      }
      
      sub output_result {
      	my ($clockid, @files) = @_;
      
      	# Uncomment for debugging watchman output
      	# open (my $fh, ">", ".git/watchman-output.out");
      	# binmode $fh, ":utf8";
      	# print $fh "$clockid\n@files\n";
      	# close $fh;
      
      	binmode STDOUT, ":utf8";
      	print $clockid;
      	print "\0";
      	local $, = "\0";
      	print @files;
      }
      
      sub watchman_clock {
      	my $response = qx/watchman clock "$git_work_tree"/;
      	die "Failed to get clock id on '$git_work_tree'.\n" .
      		"Falling back to scanning...\n" if $? != 0;
      
      	return $json_pkg->new->utf8->decode($response);
      }
      
      sub watchman_query {
      	my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
      	or die "open2() failed: $!\n" .
      	"Falling back to scanning...\n";
      
      	# In the query expression below we're asking for names of files that
      	# changed since $last_update_token but not from the .git folder.
      	#
      	# To accomplish this, we're using the "since" generator to use the
      	# recency index to select candidate nodes and "fields" to limit the
      	# output to file names only. Then we're using the "expression" term to
      	# further constrain the results.
      	my $last_update_line = "";
      	if (substr($last_update_token, 0, 1) eq "c") {
      		$last_update_token = "\"$last_update_token\"";
      		$last_update_line = qq[\n"since": $last_update_token,];
      	}
      	my $query = <<"	END";
      		["query", "$git_work_tree", {$last_update_line
      			"fields": ["name"],
      			"expression": ["not", ["dirname", ".git"]]
      		}]
      	END
      
      	# Uncomment for debugging the watchman query
      	# open (my $fh, ">", ".git/watchman-query.json");
      	# print $fh $query;
      	# close $fh;
      
      	print CHLD_IN $query;
      	close CHLD_IN;
      	my $response = do {local $/; <CHLD_OUT>};
      
      	# Uncomment for debugging the watch response
      	# open ($fh, ">", ".git/watchman-response.json");
      	# print $fh $response;
      	# close $fh;
      
      	die "Watchman: command returned no output.\n" .
      	"Falling back to scanning...\n" if $response eq "";
      	die "Watchman: command returned invalid output: $response\n" .
      	"Falling back to scanning...\n" unless $response =~ /^\{/;
      
      	return $json_pkg->new->utf8->decode($response);
      }
      
      sub is_work_tree_watched {
      	my ($output) = @_;
      	my $error = $output->{error};
      	if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
      		$retry--;
      		my $response = qx/watchman watch "$git_work_tree"/;
      		die "Failed to make watchman watch '$git_work_tree'.\n" .
      		    "Falling back to scanning...\n" if $? != 0;
      		$output = $json_pkg->new->utf8->decode($response);
      		$error = $output->{error};
      		die "Watchman: $error.\n" .
      		"Falling back to scanning...\n" if $error;
      
      		# Uncomment for debugging watchman output
      		# open (my $fh, ">", ".git/watchman-output.out");
      		# close $fh;
      
      		# Watchman will always return all files on the first query so
      		# return the fast "everything is dirty" flag to git and do the
      		# Watchman query just to get it over with now so we won't pay
      		# the cost in git to look up each individual file.
      		my $o = watchman_clock();
      		$error = $output->{error};
      
      		die "Watchman: $error.\n" .
      		"Falling back to scanning...\n" if $error;
      
      		output_result($o->{clock}, ("/"));
      		$last_update_token = $o->{clock};
      
      		eval { launch_watchman() };
      		return 0;
      	}
      
      	die "Watchman: $error.\n" .
      	"Falling back to scanning...\n" if $error;
      
      	return 1;
      }
      
      sub get_working_dir {
      	my $working_dir;
      	if ($^O =~ 'msys' || $^O =~ 'cygwin') {
      		$working_dir = Win32::GetCwd();
      		$working_dir =~ tr/\\/\//;
      	} else {
      		require Cwd;
      		$working_dir = Cwd::cwd();
      	}
      
      	return $working_dir;
      }

      post-update.sample
      ------------------
      #!/bin/sh
      #
      # An example hook script to prepare a packed repository for use over
      # dumb transports.
      #
      # To enable this hook, rename this file to "post-update".
      
      exec git update-server-info

      pre-applypatch.sample
      ---------------------
      #!/bin/sh
      #
      # An example hook script to verify what is about to be committed
      # by applypatch from an e-mail message.
      #
      # The hook should exit with non-zero status after issuing an
      # appropriate message if it wants to stop the commit.
      #
      # To enable this hook, rename this file to "pre-applypatch".
      
      . git-sh-setup
      precommit="$(git rev-parse --git-path hooks/pre-commit)"
      test -x "$precommit" && exec "$precommit" ${1+"$@"}
      :

      pre-commit.sample
      -----------------
      #!/bin/sh
      #
      # An example hook script to verify what is about to be committed.
      # Called by "git commit" with no arguments.  The hook should
      # exit with non-zero status after issuing an appropriate message if
      # it wants to stop the commit.
      #
      # To enable this hook, rename this file to "pre-commit".
      
      if git rev-parse --verify HEAD >/dev/null 2>&1
      then
      	against=HEAD
      else
      	# Initial commit: diff against an empty tree object
      	against=$(git hash-object -t tree /dev/null)
      fi
      
      # If you want to allow non-ASCII filenames set this variable to true.
      allownonascii=$(git config --type=bool hooks.allownonascii)
      
      # Redirect output to stderr.
      exec 1>&2
      
      # Cross platform projects tend to avoid non-ASCII filenames; prevent
      # them from being added to the repository. We exploit the fact that the
      # printable range starts at the space character and ends with tilde.
      if [ "$allownonascii" != "true" ] &&
      	# Note that the use of brackets around a tr range is ok here, (it's
      	# even required, for portability to Solaris 10's /usr/bin/tr), since
      	# the square bracket bytes happen to fall in the designated range.
      	test $(git diff-index --cached --name-only --diff-filter=A -z $against |
      	  LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
      then
      	cat <<\EOF
      Error: Attempt to add a non-ASCII file name.
      
      This can cause problems if you want to work with people on other platforms.
      
      To be portable it is advisable to rename the file.
      
      If you know what you are doing you can disable this check using:
      
        git config hooks.allownonascii true
      EOF
      	exit 1
      fi
      
      # If there are whitespace errors, print the offending file names and fail.
      exec git diff-index --check --cached $against --

      pre-merge-commit.sample
      -----------------------
      #!/bin/sh
      #
      # An example hook script to verify what is about to be committed.
      # Called by "git merge" with no arguments.  The hook should
      # exit with non-zero status after issuing an appropriate message to
      # stderr if it wants to stop the merge commit.
      #
      # To enable this hook, rename this file to "pre-merge-commit".
      
      . git-sh-setup
      test -x "$GIT_DIR/hooks/pre-commit" &&
              exec "$GIT_DIR/hooks/pre-commit"
      :

      pre-push.sample
      ---------------
      #!/bin/sh
      
      # An example hook script to verify what is about to be pushed.  Called by "git
      # push" after it has checked the remote status, but before anything has been
      # pushed.  If this script exits with a non-zero status nothing will be pushed.
      #
      # This hook is called with the following parameters:
      #
      # $1 -- Name of the remote to which the push is being done
      # $2 -- URL to which the push is being done
      #
      # If pushing without using a named remote those arguments will be equal.
      #
      # Information about the commits which are being pushed is supplied as lines to
      # the standard input in the form:
      #
      #   <local ref> <local oid> <remote ref> <remote oid>
      #
      # This sample shows how to prevent push of commits where the log message starts
      # with "WIP" (work in progress).
      
      remote="$1"
      url="$2"
      
      zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
      
      while read local_ref local_oid remote_ref remote_oid
      do
      	if test "$local_oid" = "$zero"
      	then
      		# Handle delete
      		:
      	else
      		if test "$remote_oid" = "$zero"
      		then
      			# New branch, examine all commits
      			range="$local_oid"
      		else
      			# Update to existing branch, examine new commits
      			range="$remote_oid..$local_oid"
      		fi
      
      		# Check for WIP commit
      		commit=$(git rev-list -n 1 --grep '^WIP' "$range")
      		if test -n "$commit"
      		then
      			echo >&2 "Found WIP commit in $local_ref, not pushing"
      			exit 1
      		fi
      	fi
      done
      
      exit 0

      pre-rebase.sample
      -----------------
      #!/bin/sh
      #
      # Copyright (c) 2006, 2008 Junio C Hamano
      #
      # The "pre-rebase" hook is run just before "git rebase" starts doing
      # its job, and can prevent the command from running by exiting with
      # non-zero status.
      #
      # The hook is called with the following parameters:
      #
      # $1 -- the upstream the series was forked from.
      # $2 -- the branch being rebased (or empty when rebasing the current branch).
      #
      # This sample shows how to prevent topic branches that are already
      # merged to 'next' branch from getting rebased, because allowing it
      # would result in rebasing already published history.
      
      publish=next
      basebranch="$1"
      if test "$#" = 2
      then
      	topic="refs/heads/$2"
      else
      	topic=`git symbolic-ref HEAD` ||
      	exit 0 ;# we do not interrupt rebasing detached HEAD
      fi
      
      case "$topic" in
      refs/heads/??/*)
      	;;
      *)
      	exit 0 ;# we do not interrupt others.
      	;;
      esac
      
      # Now we are dealing with a topic branch being rebased
      # on top of master.  Is it OK to rebase it?
      
      # Does the topic really exist?
      git show-ref -q "$topic" || {
      	echo >&2 "No such branch $topic"
      	exit 1
      }
      
      # Is topic fully merged to master?
      not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
      if test -z "$not_in_master"
      then
      	echo >&2 "$topic is fully merged to master; better remove it."
      	exit 1 ;# we could allow it, but there is no point.
      fi
      
      # Is topic ever merged to next?  If so you should not be rebasing it.
      only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
      only_next_2=`git rev-list ^master           ${publish} | sort`
      if test "$only_next_1" = "$only_next_2"
      then
      	not_in_topic=`git rev-list "^$topic" master`
      	if test -z "$not_in_topic"
      	then
      		echo >&2 "$topic is already up to date with master"
      		exit 1 ;# we could allow it, but there is no point.
      	else
      		exit 0
      	fi
      else
      	not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
      	/usr/bin/perl -e '
      		my $topic = $ARGV[0];
      		my $msg = "* $topic has commits already merged to public branch:\n";
      		my (%not_in_next) = map {
      			/^([0-9a-f]+) /;
      			($1 => 1);
      		} split(/\n/, $ARGV[1]);
      		for my $elem (map {
      				/^([0-9a-f]+) (.*)$/;
      				[$1 => $2];
      			} split(/\n/, $ARGV[2])) {
      			if (!exists $not_in_next{$elem->[0]}) {
      				if ($msg) {
      					print STDERR $msg;
      					undef $msg;
      				}
      				print STDERR " $elem->[1]\n";
      			}
      		}
      	' "$topic" "$not_in_next" "$not_in_master"
      	exit 1
      fi
      
      <<\DOC_END
      
      This sample hook safeguards topic branches that have been
      published from being rewound.
      
      The workflow assumed here is:
      
       * Once a topic branch forks from "master", "master" is never
         merged into it again (either directly or indirectly).
      
       * Once a topic branch is fully cooked and merged into "master",
         it is deleted.  If you need to build on top of it to correct
         earlier mistakes, a new topic branch is created by forking at
         the tip of the "master".  This is not strictly necessary, but
         it makes it easier to keep your history simple.
      
       * Whenever you need to test or publish your changes to topic
         branches, merge them into "next" branch.
      
      The script, being an example, hardcodes the publish branch name
      to be "next", but it is trivial to make it configurable via
      $GIT_DIR/config mechanism.
      
      With this workflow, you would want to know:
      
      (1) ... if a topic branch has ever been merged to "next".  Young
          topic branches can have stupid mistakes you would rather
          clean up before publishing, and things that have not been
          merged into other branches can be easily rebased without
          affecting other people.  But once it is published, you would
          not want to rewind it.
      
      (2) ... if a topic branch has been fully merged to "master".
          Then you can delete it.  More importantly, you should not
          build on top of it -- other people may already want to
          change things related to the topic as patches against your
          "master", so if you need further changes, it is better to
          fork the topic (perhaps with the same name) afresh from the
          tip of "master".
      
      Let's look at this example:
      
      		   o---o---o---o---o---o---o---o---o---o "next"
      		  /       /           /           /
      		 /   a---a---b A     /           /
      		/   /               /           /
      	       /   /   c---c---c---c B         /
      	      /   /   /             \         /
      	     /   /   /   b---b C     \       /
      	    /   /   /   /             \     /
          ---o---o---o---o---o---o---o---o---o---o---o "master"
      
      
      A, B and C are topic branches.
      
       * A has one fix since it was merged up to "next".
      
       * B has finished.  It has been fully merged up to "master" and "next",
         and is ready to be deleted.
      
       * C has not merged to "next" at all.
      
      We would want to allow C to be rebased, refuse A, and encourage
      B to be deleted.
      
      To compute (1):
      
      	git rev-list ^master ^topic next
      	git rev-list ^master        next
      
      	if these match, topic has not merged in next at all.
      
      To compute (2):
      
      	git rev-list master..topic
      
      	if this is empty, it is fully merged to "master".
      
      DOC_END

      pre-receive.sample
      ------------------
      #!/bin/sh
      #
      # An example hook script to make use of push options.
      # The example simply echoes all push options that start with 'echoback='
      # and rejects all pushes when the "reject" push option is used.
      #
      # To enable this hook, rename this file to "pre-receive".
      
      if test -n "$GIT_PUSH_OPTION_COUNT"
      then
      	i=0
      	while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
      	do
      		eval "value=\$GIT_PUSH_OPTION_$i"
      		case "$value" in
      		echoback=*)
      			echo "echo from the pre-receive-hook: ${value#*=}" >&2
      			;;
      		reject)
      			exit 1
      		esac
      		i=$((i + 1))
      	done
      fi

      prepare-commit-msg.sample
      -------------------------
      #!/bin/sh
      #
      # An example hook script to prepare the commit log message.
      # Called by "git commit" with the name of the file that has the
      # commit message, followed by the description of the commit
      # message's source.  The hook's purpose is to edit the commit
      # message file.  If the hook fails with a non-zero status,
      # the commit is aborted.
      #
      # To enable this hook, rename this file to "prepare-commit-msg".
      
      # This hook includes three examples. The first one removes the
      # "# Please enter the commit message..." help message.
      #
      # The second includes the output of "git diff --name-status -r"
      # into the message, just before the "git status" output.  It is
      # commented because it doesn't cope with --amend or with squashed
      # commits.
      #
      # The third example adds a Signed-off-by line to the message, that can
      # still be edited.  This is rarely a good idea.
      
      COMMIT_MSG_FILE=$1
      COMMIT_SOURCE=$2
      SHA1=$3
      
      /usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
      
      # case "$COMMIT_SOURCE,$SHA1" in
      #  ,|template,)
      #    /usr/bin/perl -i.bak -pe '
      #       print "\n" . `git diff --cached --name-status -r`
      # 	 if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
      #  *) ;;
      # esac
      
      # SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
      # git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
      # if test -z "$COMMIT_SOURCE"
      # then
      #   /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
      # fi

      push-to-checkout.sample
      -----------------------
      #!/bin/sh
      
      # An example hook script to update a checked-out tree on a git push.
      #
      # This hook is invoked by git-receive-pack(1) when it reacts to git
      # push and updates reference(s) in its repository, and when the push
      # tries to update the branch that is currently checked out and the
      # receive.denyCurrentBranch configuration variable is set to
      # updateInstead.
      #
      # By default, such a push is refused if the working tree and the index
      # of the remote repository has any difference from the currently
      # checked out commit; when both the working tree and the index match
      # the current commit, they are updated to match the newly pushed tip
      # of the branch. This hook is to be used to override the default
      # behaviour; however the code below reimplements the default behaviour
      # as a starting point for convenient modification.
      #
      # The hook receives the commit with which the tip of the current
      # branch is going to be updated:
      commit=$1
      
      # It can exit with a non-zero status to refuse the push (when it does
      # so, it must not modify the index or the working tree).
      die () {
      	echo >&2 "$*"
      	exit 1
      }
      
      # Or it can make any necessary changes to the working tree and to the
      # index to bring them to the desired state when the tip of the current
      # branch is updated to the new commit, and exit with a zero status.
      #
      # For example, the hook can simply run git read-tree -u -m HEAD "$1"
      # in order to emulate git fetch that is run in the reverse direction
      # with git push, as the two-tree form of git read-tree -u -m is
      # essentially the same as git switch or git checkout that switches
      # branches while keeping the local changes in the working tree that do
      # not interfere with the difference between the branches.
      
      # The below is a more-or-less exact translation to shell of the C code
      # for the default behaviour for git's push-to-checkout hook defined in
      # the push_to_deploy() function in builtin/receive-pack.c.
      #
      # Note that the hook will be executed from the repository directory,
      # not from the working tree, so if you want to perform operations on
      # the working tree, you will have to adapt your code accordingly, e.g.
      # by adding "cd .." or using relative paths.
      
      if ! git update-index -q --ignore-submodules --refresh
      then
      	die "Up-to-date check failed"
      fi
      
      if ! git diff-files --quiet --ignore-submodules --
      then
      	die "Working directory has unstaged changes"
      fi
      
      # This is a rough translation of:
      #
      #   head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
      if git cat-file -e HEAD 2>/dev/null
      then
      	head=HEAD
      else
      	head=$(git hash-object -t tree --stdin </dev/null)
      fi
      
      if ! git diff-index --quiet --cached --ignore-submodules $head --
      then
      	die "Working directory has staged changes"
      fi
      
      if ! git read-tree -u -m "$commit"
      then
      	die "Could not update working tree to new HEAD"
      fi

      sendemail-validate.sample
      -------------------------
      #!/bin/sh
      
      # An example hook script to validate a patch (and/or patch series) before
      # sending it via email.
      #
      # The hook should exit with non-zero status after issuing an appropriate
      # message if it wants to prevent the email(s) from being sent.
      #
      # To enable this hook, rename this file to "sendemail-validate".
      #
      # By default, it will only check that the patch(es) can be applied on top of
      # the default upstream branch without conflicts in a secondary worktree. After
      # validation (successful or not) of the last patch of a series, the worktree
      # will be deleted.
      #
      # The following config variables can be set to change the default remote and
      # remote ref that are used to apply the patches against:
      #
      #   sendemail.validateRemote (default: origin)
      #   sendemail.validateRemoteRef (default: HEAD)
      #
      # Replace the TODO placeholders with appropriate checks according to your
      # needs.
      
      validate_cover_letter () {
      	file="$1"
      	# TODO: Replace with appropriate checks (e.g. spell checking).
      	true
      }
      
      validate_patch () {
      	file="$1"
      	# Ensure that the patch applies without conflicts.
      	git am -3 "$file" || return
      	# TODO: Replace with appropriate checks for this patch
      	# (e.g. checkpatch.pl).
      	true
      }
      
      validate_series () {
      	# TODO: Replace with appropriate checks for the whole series
      	# (e.g. quick build, coding style checks, etc.).
      	true
      }
      
      # main -------------------------------------------------------------------------
      
      if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
      then
      	remote=$(git config --default origin --get sendemail.validateRemote) &&
      	ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
      	worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
      	git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
      	git config --replace-all sendemail.validateWorktree "$worktree"
      else
      	worktree=$(git config --get sendemail.validateWorktree)
      fi || {
      	echo "sendemail-validate: error: failed to prepare worktree" >&2
      	exit 1
      }
      
      unset GIT_DIR GIT_WORK_TREE
      cd "$worktree" &&
      
      if grep -q "^diff --git " "$1"
      then
      	validate_patch "$1"
      else
      	validate_cover_letter "$1"
      fi &&
      
      if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
      then
      	git config --unset-all sendemail.validateWorktree &&
      	trap 'git worktree remove -ff "$worktree"' EXIT &&
      	validate_series
      fi

      update.sample
      -------------
      #!/bin/sh
      #
      # An example hook script to block unannotated tags from entering.
      # Called by "git receive-pack" with arguments: refname sha1-old sha1-new
      #
      # To enable this hook, rename this file to "update".
      #
      # Config
      # ------
      # hooks.allowunannotated
      #   This boolean sets whether unannotated tags will be allowed into the
      #   repository.  By default they won't be.
      # hooks.allowdeletetag
      #   This boolean sets whether deleting tags will be allowed in the
      #   repository.  By default they won't be.
      # hooks.allowmodifytag
      #   This boolean sets whether a tag may be modified after creation. By default
      #   it won't be.
      # hooks.allowdeletebranch
      #   This boolean sets whether deleting branches will be allowed in the
      #   repository.  By default they won't be.
      # hooks.denycreatebranch
      #   This boolean sets whether remotely creating branches will be denied
      #   in the repository.  By default this is allowed.
      #
      
      # --- Command line
      refname="$1"
      oldrev="$2"
      newrev="$3"
      
      # --- Safety check
      if [ -z "$GIT_DIR" ]; then
      	echo "Don't run this script from the command line." >&2
      	echo " (if you want, you could supply GIT_DIR then run" >&2
      	echo "  $0 <ref> <oldrev> <newrev>)" >&2
      	exit 1
      fi
      
      if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
      	echo "usage: $0 <ref> <oldrev> <newrev>" >&2
      	exit 1
      fi
      
      # --- Config
      allowunannotated=$(git config --type=bool hooks.allowunannotated)
      allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
      denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
      allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
      allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
      
      # check for no description
      projectdesc=$(sed -e '1q' "$GIT_DIR/description")
      case "$projectdesc" in
      "Unnamed repository"* | "")
      	echo "*** Project description file hasn't been set" >&2
      	exit 1
      	;;
      esac
      
      # --- Check types
      # if $newrev is 0000...0000, it's a commit to delete a ref.
      zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
      if [ "$newrev" = "$zero" ]; then
      	newrev_type=delete
      else
      	newrev_type=$(git cat-file -t $newrev)
      fi
      
      case "$refname","$newrev_type" in
      	refs/tags/*,commit)
      		# un-annotated tag
      		short_refname=${refname##refs/tags/}
      		if [ "$allowunannotated" != "true" ]; then
      			echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
      			echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
      			exit 1
      		fi
      		;;
      	refs/tags/*,delete)
      		# delete tag
      		if [ "$allowdeletetag" != "true" ]; then
      			echo "*** Deleting a tag is not allowed in this repository" >&2
      			exit 1
      		fi
      		;;
      	refs/tags/*,tag)
      		# annotated tag
      		if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
      		then
      			echo "*** Tag '$refname' already exists." >&2
      			echo "*** Modifying a tag is not allowed in this repository." >&2
      			exit 1
      		fi
      		;;
      	refs/heads/*,commit)
      		# branch
      		if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
      			echo "*** Creating a branch is not allowed in this repository" >&2
      			exit 1
      		fi
      		;;
      	refs/heads/*,delete)
      		# delete branch
      		if [ "$allowdeletebranch" != "true" ]; then
      			echo "*** Deleting a branch is not allowed in this repository" >&2
      			exit 1
      		fi
      		;;
      	refs/remotes/*,commit)
      		# tracking branch
      		;;
      	refs/remotes/*,delete)
      		# delete tracking branch
      		if [ "$allowdeletebranch" != "true" ]; then
      			echo "*** Deleting a tracking branch is not allowed in this repository" >&2
      			exit 1
      		fi
      		;;
      	*)
      		# Anything else (is there anything else?)
      		echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
      		exit 1
      		;;
      esac
      
      # --- Finished
      exit 0


    info/
      exclude
      -------
      # git ls-files --others --exclude-from=.git/info/exclude
      # Lines that start with '#' are comments.
      # For a project mostly in C, the following would be a good set of
      # exclude patterns (uncomment them if you want to use them):
      # *.[oa]
      # *~


    logs/
      HEAD
      ----
      0000000000000000000000000000000000000000 d1d77b9931a61809179c4ba0ed004e5aa26bc21e shot-love <165561097+shot-love@users.noreply.github.com> 1783075403 +1100	clone: from https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo.git
      d1d77b9931a61809179c4ba0ed004e5aa26bc21e 9c0d11ab1b5048929f1c52398114a883b4e3c34d shot-love <165561097+shot-love@users.noreply.github.com> 1783080931 +1100	commit: Добавил кнопку 'Хочу скидку' вместо текстовой команды
      9c0d11ab1b5048929f1c52398114a883b4e3c34d bc438cc3d80aadd61037c68fe5a5ed9d257e7ff0 shot-love <165561097+shot-love@users.noreply.github.com> 1783606374 +1100	commit: Убрал отладочное сохранение QR-фото, финальная версия
      bc438cc3d80aadd61037c68fe5a5ed9d257e7ff0 6166008a606354bc99430e996297f576b3dc59db shot-love <165561097+shot-love@users.noreply.github.com> 1783607573 +1100	commit: Добавлен opencv-python в зависимости
      6166008a606354bc99430e996297f576b3dc59db d7d5271c9b49fe053e849166f4197a27eb80a292 shot-love <165561097+shot-love@users.noreply.github.com> 1783607829 +1100	commit: Заменил opencv-python на opencv-python-headless для Docker
      d7d5271c9b49fe053e849166f4197a27eb80a292 9698b5964167c476293c05689d942b5b35c8b989 shot-love <165561097+shot-love@users.noreply.github.com> 1783608060 +1100	commit: Удалён pyzbar, используется только OpenCV
      9698b5964167c476293c05689d942b5b35c8b989 46c0e14abf48c42476c03d2fdc77002bfafa1c61 shot-love <165561097+shot-love@users.noreply.github.com> 1783742257 +1100	commit: Добавлен Dockerfile с сертификатами Минцифры
      46c0e14abf48c42476c03d2fdc77002bfafa1c61 46c0e14abf48c42476c03d2fdc77002bfafa1c61 shot-love <165561097+shot-love@users.noreply.github.com> 1783742894 +1100	reset: moving to HEAD
      46c0e14abf48c42476c03d2fdc77002bfafa1c61 b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 shot-love <165561097+shot-love@users.noreply.github.com> 1783742909 +1100	pull --rebase origin main (start): checkout b68a10f027c4d6d2a7838f44d4230d7d7dae8be5
      b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 shot-love <165561097+shot-love@users.noreply.github.com> 1783742909 +1100	pull --rebase origin main (finish): returning to refs/heads/main
      b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 ebc9fda237794836b1eba254b3af1ef5c3122cd0 shot-love <165561097+shot-love@users.noreply.github.com> 1783743047 +1100	commit: Добавлен Dockerfile
      ebc9fda237794836b1eba254b3af1ef5c3122cd0 d51adb8820abdab6138ca254be175cbe743a1623 shot-love <165561097+shot-love@users.noreply.github.com> 1783743558 +1100	commit: Отключен SSL для aiohttp
      d51adb8820abdab6138ca254be175cbe743a1623 69ac457a2a0802cb016aa995e5046a8b7c4d8993 shot-love <165561097+shot-love@users.noreply.github.com> 1783743894 +1100	commit: SSL отключен для всех запросов
      69ac457a2a0802cb016aa995e5046a8b7c4d8993 69ac457a2a0802cb016aa995e5046a8b7c4d8993 shot-love <165561097+shot-love@users.noreply.github.com> 1783748229 +1100	checkout: moving from main to main
      69ac457a2a0802cb016aa995e5046a8b7c4d8993 69ac457a2a0802cb016aa995e5046a8b7c4d8993 shot-love <165561097+shot-love@users.noreply.github.com> 1783748242 +1100	checkout: moving from main to feature/multi-org
      69ac457a2a0802cb016aa995e5046a8b7c4d8993 b062a2531892b2e6a69dfd02e37a0a3a10be36ea shot-love <165561097+shot-love@users.noreply.github.com> 1783752035 +1100	commit: Новая мульти-организационная версия database.py
      b062a2531892b2e6a69dfd02e37a0a3a10be36ea abea51fe704e0b22c640f008682cf272b430c80d shot-love <165561097+shot-love@users.noreply.github.com> 1783753677 +1100	commit: Переименован проект в Бот_Скидки_на_Напитки_от_Кофий
      abea51fe704e0b22c640f008682cf272b430c80d db38e10f585b49e7b337fe4d529df7cf47f9b7f8 shot-love <165561097+shot-love@users.noreply.github.com> 1783753836 +1100	commit: Обновлено название проекта: Бот Скидки на Напитки от Кофий
      db38e10f585b49e7b337fe4d529df7cf47f9b7f8 3590938857c6b778f38bd78d1feb29db1a164087 shot-love <165561097+shot-love@users.noreply.github.com> 1783763995 +1100	commit: Заменена handle_callback на мультиорганизационную версию
      3590938857c6b778f38bd78d1feb29db1a164087 a8319494cff25ea08270e63eadc6d1476c77f91a shot-love <165561097+shot-love@users.noreply.github.com> 1783947603 +1100	commit: Финальные правки мультиорганизационной архитектуры
      a8319494cff25ea08270e63eadc6d1476c77f91a 69ac457a2a0802cb016aa995e5046a8b7c4d8993 shot-love <165561097+shot-love@users.noreply.github.com> 1783947624 +1100	checkout: moving from feature/multi-org to main
      69ac457a2a0802cb016aa995e5046a8b7c4d8993 a8319494cff25ea08270e63eadc6d1476c77f91a shot-love <165561097+shot-love@users.noreply.github.com> 1783947624 +1100	merge feature/multi-org: Fast-forward
      a8319494cff25ea08270e63eadc6d1476c77f91a 225096932575be5681e7c0a18cb11574bbce4481 shot-love <165561097+shot-love@users.noreply.github.com> 1783951122 +1100	commit: Добавлена БД с организациями


      refs/

        heads/
          main
          ----
          0000000000000000000000000000000000000000 d1d77b9931a61809179c4ba0ed004e5aa26bc21e shot-love <165561097+shot-love@users.noreply.github.com> 1783075403 +1100	clone: from https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo.git
          d1d77b9931a61809179c4ba0ed004e5aa26bc21e 9c0d11ab1b5048929f1c52398114a883b4e3c34d shot-love <165561097+shot-love@users.noreply.github.com> 1783080931 +1100	commit: Добавил кнопку 'Хочу скидку' вместо текстовой команды
          9c0d11ab1b5048929f1c52398114a883b4e3c34d bc438cc3d80aadd61037c68fe5a5ed9d257e7ff0 shot-love <165561097+shot-love@users.noreply.github.com> 1783606374 +1100	commit: Убрал отладочное сохранение QR-фото, финальная версия
          bc438cc3d80aadd61037c68fe5a5ed9d257e7ff0 6166008a606354bc99430e996297f576b3dc59db shot-love <165561097+shot-love@users.noreply.github.com> 1783607573 +1100	commit: Добавлен opencv-python в зависимости
          6166008a606354bc99430e996297f576b3dc59db d7d5271c9b49fe053e849166f4197a27eb80a292 shot-love <165561097+shot-love@users.noreply.github.com> 1783607829 +1100	commit: Заменил opencv-python на opencv-python-headless для Docker
          d7d5271c9b49fe053e849166f4197a27eb80a292 9698b5964167c476293c05689d942b5b35c8b989 shot-love <165561097+shot-love@users.noreply.github.com> 1783608060 +1100	commit: Удалён pyzbar, используется только OpenCV
          9698b5964167c476293c05689d942b5b35c8b989 46c0e14abf48c42476c03d2fdc77002bfafa1c61 shot-love <165561097+shot-love@users.noreply.github.com> 1783742257 +1100	commit: Добавлен Dockerfile с сертификатами Минцифры
          46c0e14abf48c42476c03d2fdc77002bfafa1c61 b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 shot-love <165561097+shot-love@users.noreply.github.com> 1783742909 +1100	pull --rebase origin main (finish): refs/heads/main onto b68a10f027c4d6d2a7838f44d4230d7d7dae8be5
          b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 ebc9fda237794836b1eba254b3af1ef5c3122cd0 shot-love <165561097+shot-love@users.noreply.github.com> 1783743047 +1100	commit: Добавлен Dockerfile
          ebc9fda237794836b1eba254b3af1ef5c3122cd0 d51adb8820abdab6138ca254be175cbe743a1623 shot-love <165561097+shot-love@users.noreply.github.com> 1783743558 +1100	commit: Отключен SSL для aiohttp
          d51adb8820abdab6138ca254be175cbe743a1623 69ac457a2a0802cb016aa995e5046a8b7c4d8993 shot-love <165561097+shot-love@users.noreply.github.com> 1783743894 +1100	commit: SSL отключен для всех запросов
          69ac457a2a0802cb016aa995e5046a8b7c4d8993 a8319494cff25ea08270e63eadc6d1476c77f91a shot-love <165561097+shot-love@users.noreply.github.com> 1783947624 +1100	merge feature/multi-org: Fast-forward
          a8319494cff25ea08270e63eadc6d1476c77f91a 225096932575be5681e7c0a18cb11574bbce4481 shot-love <165561097+shot-love@users.noreply.github.com> 1783951122 +1100	commit: Добавлена БД с организациями


          feature/
            multi-org
            ---------
            0000000000000000000000000000000000000000 69ac457a2a0802cb016aa995e5046a8b7c4d8993 shot-love <165561097+shot-love@users.noreply.github.com> 1783748242 +1100	branch: Created from HEAD
            69ac457a2a0802cb016aa995e5046a8b7c4d8993 b062a2531892b2e6a69dfd02e37a0a3a10be36ea shot-love <165561097+shot-love@users.noreply.github.com> 1783752035 +1100	commit: Новая мульти-организационная версия database.py
            b062a2531892b2e6a69dfd02e37a0a3a10be36ea abea51fe704e0b22c640f008682cf272b430c80d shot-love <165561097+shot-love@users.noreply.github.com> 1783753677 +1100	commit: Переименован проект в Бот_Скидки_на_Напитки_от_Кофий
            abea51fe704e0b22c640f008682cf272b430c80d db38e10f585b49e7b337fe4d529df7cf47f9b7f8 shot-love <165561097+shot-love@users.noreply.github.com> 1783753836 +1100	commit: Обновлено название проекта: Бот Скидки на Напитки от Кофий
            db38e10f585b49e7b337fe4d529df7cf47f9b7f8 3590938857c6b778f38bd78d1feb29db1a164087 shot-love <165561097+shot-love@users.noreply.github.com> 1783763995 +1100	commit: Заменена handle_callback на мультиорганизационную версию
            3590938857c6b778f38bd78d1feb29db1a164087 a8319494cff25ea08270e63eadc6d1476c77f91a shot-love <165561097+shot-love@users.noreply.github.com> 1783947603 +1100	commit: Финальные правки мультиорганизационной архитектуры


        remotes/

          origin/
            HEAD
            ----
            0000000000000000000000000000000000000000 d1d77b9931a61809179c4ba0ed004e5aa26bc21e shot-love <165561097+shot-love@users.noreply.github.com> 1783075403 +1100	clone: from https://hub.mos.ru/alexey.v.shatalov/warmhomeforligo.git

            main
            ----
            d1d77b9931a61809179c4ba0ed004e5aa26bc21e 9c0d11ab1b5048929f1c52398114a883b4e3c34d shot-love <165561097+shot-love@users.noreply.github.com> 1783080958 +1100	update by push
            9c0d11ab1b5048929f1c52398114a883b4e3c34d bc438cc3d80aadd61037c68fe5a5ed9d257e7ff0 shot-love <165561097+shot-love@users.noreply.github.com> 1783606448 +1100	update by push
            bc438cc3d80aadd61037c68fe5a5ed9d257e7ff0 6166008a606354bc99430e996297f576b3dc59db shot-love <165561097+shot-love@users.noreply.github.com> 1783607579 +1100	update by push
            6166008a606354bc99430e996297f576b3dc59db d7d5271c9b49fe053e849166f4197a27eb80a292 shot-love <165561097+shot-love@users.noreply.github.com> 1783607834 +1100	update by push
            d7d5271c9b49fe053e849166f4197a27eb80a292 9698b5964167c476293c05689d942b5b35c8b989 shot-love <165561097+shot-love@users.noreply.github.com> 1783608064 +1100	update by push
            9698b5964167c476293c05689d942b5b35c8b989 b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 shot-love <165561097+shot-love@users.noreply.github.com> 1783742299 +1100	pull: fast-forward
            b68a10f027c4d6d2a7838f44d4230d7d7dae8be5 ebc9fda237794836b1eba254b3af1ef5c3122cd0 shot-love <165561097+shot-love@users.noreply.github.com> 1783743051 +1100	update by push
            ebc9fda237794836b1eba254b3af1ef5c3122cd0 d51adb8820abdab6138ca254be175cbe743a1623 shot-love <165561097+shot-love@users.noreply.github.com> 1783743561 +1100	update by push
            d51adb8820abdab6138ca254be175cbe743a1623 69ac457a2a0802cb016aa995e5046a8b7c4d8993 shot-love <165561097+shot-love@users.noreply.github.com> 1783743897 +1100	update by push
            69ac457a2a0802cb016aa995e5046a8b7c4d8993 a8319494cff25ea08270e63eadc6d1476c77f91a shot-love <165561097+shot-love@users.noreply.github.com> 1783947630 +1100	update by push
            a8319494cff25ea08270e63eadc6d1476c77f91a 225096932575be5681e7c0a18cb11574bbce4481 shot-love <165561097+shot-love@users.noreply.github.com> 1783951141 +1100	update by push


            feature/
              multi-org
              ---------
              0000000000000000000000000000000000000000 b062a2531892b2e6a69dfd02e37a0a3a10be36ea shot-love <165561097+shot-love@users.noreply.github.com> 1783752039 +1100	update by push
              b062a2531892b2e6a69dfd02e37a0a3a10be36ea abea51fe704e0b22c640f008682cf272b430c80d shot-love <165561097+shot-love@users.noreply.github.com> 1783753682 +1100	update by push
              abea51fe704e0b22c640f008682cf272b430c80d db38e10f585b49e7b337fe4d529df7cf47f9b7f8 shot-love <165561097+shot-love@users.noreply.github.com> 1783753840 +1100	update by push
              db38e10f585b49e7b337fe4d529df7cf47f9b7f8 3590938857c6b778f38bd78d1feb29db1a164087 shot-love <165561097+shot-love@users.noreply.github.com> 1783764005 +1100	update by push


    objects/

      00/
        b1c21b2b5931ef94bf3bcfa432b21587a4f54c
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      01/
        c7a01ae7eeb22e53947f6fa618fd316809c4c7
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        edb41e6cabc19b2af64d038e2c3c1d9b30d5a8
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      03/
        8fc1c902c8b59a5f52bcf9b3742ba58d614b08
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      06/
        bdf9e15bba16e92a30eeea089682f6d79d9451
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      0f/
        29f053ed4f04d0d36f712604fa20780a5d52b4
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      17/
        516d8e69b7ac2500bd064952bd47af583a0eca
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      1e/
        95f27f002761ff286e44afacadcfd4b3109738
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      1f/
        bc4a47ea472291a62e656059c62f1cc98254f4
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      22/
        5096932575be5681e7c0a18cb11574bbce4481
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      29/
        84bc372d4c6304468c3e1c22df1fdb2c173d23
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      2a/
        0a41114cdc033632fb03e6e1ef87f4e9e983c4
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      2e/
        3f8285de488a1fb6bf2a4713e3fd0686e6d627
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        82e4cda4a025c3dc1e4e30558646227c4c94a3
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      31/
        6faa72bb9e9e27567cf5aa614c08a361b79e00
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      32/
        ccfe2a7dad16987d69b00a3b9727ab4b99fcb3
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      35/
        90938857c6b778f38bd78d1feb29db1a164087
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      36/
        6d49128ce8145f68a64b85ffdf385c08193d76
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      37/
        1265c233a2c424595efacd832eb63e6ebeb7f4
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        8ee7fe28b6546818f68294a83012aa1f578d7f
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      3b/
        5ae9f934558a22f05a5e38ff0abee34bc95eed
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      3c/
        477d5f2ea1c9e41c731727da106ed5905ed8a5
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      41/
        efb341b673d606c8eaeb1c516ede08534eb488
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      45/
        1349dffe7d8e5b1300217f114d999ad83354a4
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        31ca86e0c7ff67d9322f62e9124fdf5e2cb742
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      46/
        b4c119b3e695e5465ba22ec2daf872e8ca7b7d
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        c0e14abf48c42476c03d2fdc77002bfafa1c61
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      4b/
        01105be9fda16a4470ac85f2520865e4bbc91a
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      4c/
        0a6a81515531037ed97be33e380131b7ae02e9
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        143a21f7d4145ecec56e1748a2934932003e4c
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      4d/
        aa3afc27a354c44ad5dc2fc3b2582f4fed2606
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      61/
        66008a606354bc99430e996297f576b3dc59db
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        6977b0f0e0c583c13a0dbc5a1bd709397e35ea
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      66/
        1c0e4860246a033e20f33b7518630a1a6320c1
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        a49c6ed5b9747e0b889bfb86d47a25009ecfa5
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      68/
        3e5f978bb3758c69bf5c833b60e6508577fdcd
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        f2fb70a07bc658d58626e20dc61ac1176bd870
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      69/
        ac457a2a0802cb016aa995e5046a8b7c4d8993
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      6f/
        6b2a3d1c4c7610588385f3e6c42fc7b650e104
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      74/
        1cc40c7785568e17ef856b25799a6e316a3ed8
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      75/
        ed686480d1c82998422e4a8d7e2d7711fea43b
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        f8c80b2c50b109fd550424ac7315bca46272c5
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      7f/
        e532cd9ae601382577d53fe9b4e8144c7e16cc
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      85/
        027dc21ad8ae86238c60544ba0ae42a4018cd1
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      87/
        3460eff54c3c3b230b595754affaf98c1eb3d9
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      88/
        2a9cacf93ca9b7f30516f4ea5e7f34620b4828
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      8e/
        9205bf920991ddb8c3111e044f69657d8b6097
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      91/
        b18a869a3d7b4e4a2b9b1d78811932c5d2cbaf
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      92/
        0bfb3b4ef07dac66cc37d67d57de9460ca2b36
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      93/
        17371b66c7047a32c36dd81157ebdc183b83c0
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      96/
        98b5964167c476293c05689d942b5b35c8b989
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      99/
        bd0c0c8e6711446b5c10624a56c79973394cd3
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      9b/
        228e8eb11b2d916dc97ea64a5d6e1bfe41df39
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      9c/
        01c12f77b9397b0ca57180d918cf7c40cbefb5
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        0d11ab1b5048929f1c52398114a883b4e3c34d
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      9e/
        d53a0212835590f102e1335756c49f733e099f
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      a5/
        0c5dac24b55c9436f9dcb44343ec4c9d0313da
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      a8/
        319494cff25ea08270e63eadc6d1476c77f91a
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      ab/
        ea51fe704e0b22c640f008682cf272b430c80d
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      b0/
        62a2531892b2e6a69dfd02e37a0a3a10be36ea
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      b1/
        bb750f10ce228dc9496651686cc167659f1c30
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      b5/
        0efec507c78a23521861fda317d7f83f74733e
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      b6/
        8a10f027c4d6d2a7838f44d4230d7d7dae8be5
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      bc/
        438cc3d80aadd61037c68fe5a5ed9d257e7ff0
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      c0/
        ff5d24b3b37c431af40a2003dd6523440e1127
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      c1/
        05fa01853ba6888d14419acb93d9de1ab49375
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      cc/
        28184cab7c6e782e47dcfc580f6b2d2cbfcc26
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        8aeee6a646ed9460000298c591834bd2ed5608
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        bf45de352a209870f6678a693410404b8ea928
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      cf/
        2b9eb1c6b3ca3a090c83b9dd8682360345b43c
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      d5/
        1adb8820abdab6138ca254be175cbe743a1623
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      d7/
        d5271c9b49fe053e849166f4197a27eb80a292
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      d8/
        b99a198e0e21ff4b28067d563110bb94d14121
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      d9/
        009265310dc6ba3fc4d6d466f0415e4a732be4
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      db/
        38e10f585b49e7b337fe4d529df7cf47f9b7f8
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      dd/
        d40311495e0eaa800c384c4509247c83069b83
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      df/
        df918bd910fbad40d7185857dd27d214919024
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      e3/
        faef65abdb5610c648be444fed21fb2f0bae94
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      e6/
        718b9c0f36238d07b4c72a3b5650b0994618cc
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        82f465248d734d134aed498a84e6b30bf3c920
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        9de29bb2d1d6434b8b29ae775ad8c2e48c5391
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      eb/
        c9fda237794836b1eba254b3af1ef5c3122cd0
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      ed/
        7c02505f2611704ad7a9ae378f733524ca3a4c
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      f3/
        5ff9c86ccc2d3f6a5c45ebe32936da9378dcaf
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      f5/
        50f18752146f66fb05f9158775b3b19d0d6db5
        --------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

      info/

      pack/
        pack-146c9d209a215716049ebdfce1aa3d1dccc16fd7.idx
        -------------------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        pack-146c9d209a215716049ebdfce1aa3d1dccc16fd7.pack
        --------------------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
        pack-146c9d209a215716049ebdfce1aa3d1dccc16fd7.rev
        -------------------------------------------------
        [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

    refs/

      heads/
        main
        ----
        225096932575be5681e7c0a18cb11574bbce4481


        feature/
          multi-org
          ---------
          a8319494cff25ea08270e63eadc6d1476c77f91a


      remotes/

        origin/
          HEAD
          ----
          ref: refs/remotes/origin/main

          main
          ----
          225096932575be5681e7c0a18cb11574bbce4481


          feature/
            multi-org
            ---------
            3590938857c6b778f38bd78d1feb29db1a164087


      tags/

  web/
    app.py
    ------
    from flask import Flask, render_template, request, redirect, url_for
    import sys
    import os
    
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    
    from database import (
        init_db, get_all_organizations, add_organization, get_stats_by_org,
        get_all_baristas, get_all_active_codes, get_code_by_text, use_promocode,
        DB_PATH, get_all_codes
    )
    
    print(f"📂 Веб-кабинет использует БД: {DB_PATH}")
    
    app = Flask(__name__)
    
    # ============================================================
    # ВСПОМОГАТЕЛЬНАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА КОРОТИН
    # ============================================================
    
    def run_async(coro):
        """Запускает асинхронную функцию в синхронном контексте"""
        import asyncio
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
        return loop.run_until_complete(coro)
    
    # ============================================================
    # ГЛАВНАЯ СТРАНИЦА (дашборд)
    # ============================================================
    
    @app.route('/')
    def index():
        orgs = run_async(get_all_organizations())
        stats = []
        for org in orgs:
            stat = run_async(get_stats_by_org(org['id']))
            stats.append({
                'name': org['name'],
                'prefix': org['prefix'],
                'total': stat['total'],
                'active': stat['active'],
                'used': stat['used']
            })
        
        baristas = run_async(get_all_baristas())
        
        return render_template('index.html', stats=stats, baristas=baristas)
    
    # ============================================================
    # ОРГАНИЗАЦИИ
    # ============================================================
    
    @app.route('/organizations')
    def organizations():
        orgs = run_async(get_all_organizations())
        return render_template('organizations.html', orgs=orgs)
    
    @app.route('/add_organization', methods=['GET', 'POST'])
    def add_organization_view():
        if request.method == 'POST':
            name = request.form['name']
            prefix = request.form['prefix']
            chat_id = int(request.form['chat_id'])
            invite_link = request.form.get('invite_link', '')
            
            try:
                run_async(add_organization(name, prefix, chat_id, invite_link))
                return redirect(url_for('organizations'))
            except Exception as e:
                return f"❌ Ошибка: {e}"
        
        return render_template('add_organization.html')
    
    # ============================================================
    # ПРОМОКОДЫ
    # ============================================================
    
    @app.route('/promocodes')
    def promocodes():
        org_id = request.args.get('org_id', type=int)
        status = request.args.get('status', 'all')
        
        if status == 'all':
            codes = run_async(get_all_codes(None))
        else:
            codes = run_async(get_all_codes(status))
        
        orgs = run_async(get_all_organizations())
        
        if org_id:
            codes = [c for c in codes if c.get('organization_id') == org_id]  # <-- теперь работает
        
        return render_template('promocodes.html', codes=codes, orgs=orgs, selected_org=org_id, status=status)
    
    @app.route('/baristas', methods=['GET', 'POST'])
    def baristas():
        if request.method == 'POST':
            user_id = request.form.get('user_id')
            name = request.form.get('name')
            if user_id and name:
                try:
                    run_async(add_barista(int(user_id), name, user_id))  # added_by = сам админ
                    return redirect(url_for('baristas'))
                except Exception as e:
                    return f"❌ Ошибка: {e}"
            else:
                return "❌ Все поля обязательны"
        
        baristas = run_async(get_all_baristas())
        return render_template('baristas.html', baristas=baristas)
    
    @app.route('/remove_barista/<int:user_id>')
    def remove_barista_view(user_id):
        try:
            run_async(remove_barista(user_id))
            return redirect(url_for('baristas'))
        except Exception as e:
            return f"❌ Ошибка: {e}"
    
    # ============================================================
    # ЗАПУСК
    # ============================================================
    
    if __name__ == '__main__':
        run_async(init_db())
        print("✅ База данных инициализирована")
        app.run(debug=True, host='0.0.0.0', port=5000)

    templates/
      add_organization.html
      ---------------------
      {% extends "base.html" %}
      
      {% block content %}
      <h2>➕ Добавить организацию</h2>
      <form method="POST">
          <div>
              <label>Название организации:</label><br>
              <input type="text" name="name" required>
          </div>
          <div style="margin-top: 10px;">
              <label>Префикс промокода (например, LIGO-):</label><br>
              <input type="text" name="prefix" required placeholder="LIGO-">
          </div>
          <div style="margin-top: 10px;">
              <label>Chat ID группы:</label><br>
              <input type="number" name="chat_id" required>
          </div>
          <div style="margin-top: 10px;">
              <label>Ссылка-приглашение (опционально):</label><br>
              <input type="url" name="invite_link" placeholder="https://max.ru/join/...">
          </div>
          <button type="submit" class="btn" style="margin-top: 15px;">✅ Добавить</button>
      </form>
      {% endblock %}
      baristas.html
      -------------
      {% extends "base.html" %}
      
      {% block content %}
      <h2>🧑‍🍳 Управление баристами</h2>
      
      <h3>➕ Добавить баристу</h3>
      <form method="POST">
          <div>
              <label>ID пользователя в MAX:</label><br>
              <input type="number" name="user_id" required placeholder="123456789">
          </div>
          <div style="margin-top: 10px;">
              <label>Имя баристы:</label><br>
              <input type="text" name="name" required placeholder="Анна">
          </div>
          <button type="submit" class="btn" style="margin-top: 15px;">✅ Добавить</button>
      </form>
      
      <hr>
      
      <h3>📋 Список барист</h3>
      <table>
          <thead>
              <tr>
                  <th>ID</th>
                  <th>Имя</th>
                  <th>Действие</th>
              </tr>
          </thead>
          <tbody>
              {% for b in baristas %}
              <tr>
                  <td>{{ b.user_id }}</td>
                  <td>{{ b.name }}</td>
                  <td>
                      <a href="/remove_barista/{{ b.user_id }}" class="btn btn-danger" 
                         onclick="return confirm('Удалить баристу {{ b.name }}?')">🗑️ Удалить</a>
                  </td>
              </tr>
              {% else %}
              <tr><td colspan="3">Нет добавленных барист</td></tr>
              {% endfor %}
          </tbody>
      </table>
      {% endblock %}
      base.html
      ---------
      <!DOCTYPE html>
      <html lang="ru">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Бот Скидки на Напитки от "Кофий" — Админ-панель</title>
          <style>
              body { font-family: Arial, sans-serif; margin: 40px; background: #f4f4f4; }
              h1 { color: #2c3e50; }
              nav a { margin-right: 15px; text-decoration: none; color: #3498db; font-weight: bold; }
              nav a:hover { text-decoration: underline; }
              table { width: 100%; border-collapse: collapse; background: white; }
              th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
              th { background: #2c3e50; color: white; }
              .btn { background: #3498db; color: white; padding: 8px 16px; text-decoration: none; border-radius: 4px; display: inline-block; }
              .btn:hover { opacity: 0.8; }
              .btn-danger { background: #e74c3c; }
              .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
              .stats { display: flex; gap: 20px; flex-wrap: wrap; }
              .stat-card { background: #ecf0f1; padding: 15px; border-radius: 8px; flex: 1; min-width: 150px; }
              .stat-card h3 { margin: 0; color: #2c3e50; }
              .stat-card .number { font-size: 28px; font-weight: bold; color: #2980b9; }
              .flash { padding: 10px; background: #2ecc71; color: white; border-radius: 4px; margin-bottom: 20px; }
              .flash.error { background: #e74c3c; }
              input, select { padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; }
              button { cursor: pointer; }
              hr { margin: 20px 0; border: none; border-top: 1px solid #ddd; }
          </style>
      </head>
      <body>
          <div class="container">
              <h1>☕ Бот Скидки на Напитки от "Кофий" — Админ-панель</h1>
              <nav>
                  <a href="/">📊 Главная</a>
                  <a href="/organizations">🏢 Организации</a>
                  <a href="/promocodes">📋 Промокоды</a>
                  <a href="/baristas">🧑‍🍳 Баристы</a>
              </nav>
              <hr>
              {% block content %}{% endblock %}
          </div>
      </body>
      </html>
      index.html
      ----------
      {% extends "base.html" %}
      
      {% block content %}
      <h2>📊 Статистика</h2>
      
      <div class="stats">
          {% for stat in stats %}
          <div class="stat-card">
              <h3>{{ stat.name }}</h3>
              <div class="number">{{ stat.total }}</div>
              <div>Всего выдано</div>
              <div style="margin-top: 5px;">
                  <span style="color: #27ae60;">{{ stat.active }} активных</span> |
                  <span style="color: #7f8c8d;">{{ stat.used }} использовано</span>
              </div>
              <div style="font-size: 12px; color: #95a5a6; margin-top: 5px;">Префикс: {{ stat.prefix }}</div>
          </div>
          {% endfor %}
      </div>
      
      <h3>🧑‍🍳 Баристы</h3>
      <ul>
          {% for b in baristas %}
          <li>{{ b.name }} (ID: {{ b.user_id }})</li>
          {% else %}
          <li>Нет добавленных барист</li>
          {% endfor %}
      </ul>
      {% endblock %}
      organizations.html
      ------------------
      {% extends "base.html" %}
      
      {% block content %}
      <h2>🏢 Организации</h2>
      <a href="/add_organization" class="btn">➕ Добавить организацию</a>
      <table>
          <thead>
              <tr>
                  <th>ID</th>
                  <th>Название</th>
                  <th>Префикс</th>
                  <th>Chat ID</th>
                  <th>Ссылка</th>
                  <th>Активна</th>
              </tr>
          </thead>
          <tbody>
              {% for org in orgs %}
              <tr>
                  <td>{{ org.id }}</td>
                  <td>{{ org.name }}</td>
                  <td><code>{{ org.prefix }}</code></td>
                  <td><code>{{ org.chat_id }}</code></td>
                  <td>{% if org.invite_link %}<a href="{{ org.invite_link }}" target="_blank">ссылка</a>{% endif %}</td>
                  <td>{{ "✅" if org.is_active else "❌" }}</td>
              </tr>
              {% else %}
              <tr><td colspan="6">Нет организаций</td></tr>
              {% endfor %}
          </tbody>
      </table>
      {% endblock %}
      promocodes.html
      ---------------
      {% extends "base.html" %}
      
      {% block content %}
      <h2>📋 Промокоды</h2>
      <form method="GET" style="margin-bottom: 15px;">
          <label>Фильтр по организации:</label>
          <select name="org_id" onchange="this.form.submit()">
              <option value="">Все</option>
              {% for org in orgs %}
              <option value="{{ org.id }}" {% if org.id == selected_org %}selected{% endif %}>{{ org.name }}</option>
              {% endfor %}
          </select>
          
          <label style="margin-left: 15px;">Статус:</label>
          <select name="status" onchange="this.form.submit()">
              <option value="all" {% if status == 'all' %}selected{% endif %}>Все</option>
              <option value="active" {% if status == 'active' %}selected{% endif %}>Активные</option>
              <option value="used" {% if status == 'used' %}selected{% endif %}>Погашенные</option>
          </select>
      </form>
      <table>
          <thead>
              <tr>
                  <th>Код</th>
                  <th>User ID</th>
                  <th>Пользователь</th>
                  <th>Организация</th>
                  <th>Выдан</th>
                  <th>Использован</th>
                  <th>Статус</th>
              </tr>
          </thead>
          <tbody>
              {% for code in codes %}
              <tr>
                  <td><code>{{ code.code }}</code></td>
                  <td>{{ code.user_id }}</td>
                  <td>{{ code.user_name }}</td>
                  <td>{{ code.org_name or '—' }}</td>
                  <td>{{ code.issued_at[:16] if code.issued_at else '' }}</td>
                  <td>{{ code.used_at[:16] if code.used_at else '—' }}</td>
                  <td>
                      {% if code.status == 'active' %}
                          <span style="color: green;">✅ Активен</span>
                      {% else %}
                          <span style="color: red;">❌ Погашен</span>
                      {% endif %}
                  </td>
              </tr>
              {% else %}
              <tr><td colspan="7">Нет промокодов</td></tr>
              {% endfor %}
          </tbody>
      </table>
      
      {% endblock %}

  __pycache__/
    config.cpython-310.pyc
    ----------------------
    [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
    database.cpython-310.pyc
    ------------------------
    [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]
    qr_generator.cpython-310.pyc
    ----------------------------
    [Не удалось прочитать содержимое (бинарный файл или ошибка доступа)]

