diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -7,12 +7,13 @@ import hashlib import json from urllib.parse import unquote, parse_qs, quote import time -from datetime import datetime, timezone, timedelta +from datetime import datetime import logging import threading import random from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError +import pytz BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4") HOST = '0.0.0.0' @@ -24,6 +25,8 @@ HF_DATA_FILE_PATH = "data.json" HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") +BISHKEK_TZ = pytz.timezone('Asia/Bishkek') + app = Flask(__name__) logging.basicConfig(level=logging.INFO) app.secret_key = os.urandom(24) @@ -31,15 +34,25 @@ app.secret_key = os.urandom(24) _data_lock = threading.Lock() visitor_data_cache = {} -def get_bishkek_time(): - return datetime.now(timezone(timedelta(hours=6))) +DEFAULT_ORGANIZATION_INFO = { + "name": "Название вашей организации", + "phones": ["+996000123456"], + "address": "г. Бишкек, ул. Примерная, д.1", + "links": [{"label": "Наш сайт", "url": "https://example.com"}] +} -def generate_unique_id(all_data): +def generate_unique_id(all_data_keys): while True: new_id = str(random.randint(10000, 99999)) - if new_id not in all_data: + if new_id not in all_data_keys: return new_id +def generate_invoice_id(): + return f"INV-{int(time.time() * 1000)}-{random.randint(100, 999)}" + +def get_current_time_bishkek(): + return datetime.now(BISHKEK_TZ) + def download_data_from_hf(): global visitor_data_cache if not HF_TOKEN_READ: @@ -73,35 +86,38 @@ def download_data_from_hf(): logging.error(f"Error downloading data from Hugging Face: {e}") return False -def load_visitor_data(): +def load_data(): global visitor_data_cache with _data_lock: if not visitor_data_cache: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: visitor_data_cache = json.load(f) - logging.info("Visitor data loaded from local JSON.") + logging.info("Data loaded from local JSON.") except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") - visitor_data_cache = {} + logging.warning(f"{DATA_FILE} not found locally. Initializing with default structure.") + visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} except json.JSONDecodeError: - logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") - visitor_data_cache = {} + logging.error(f"Error decoding {DATA_FILE}. Initializing with default structure.") + visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} except Exception as e: - logging.error(f"Unexpected error loading visitor data: {e}") - visitor_data_cache = {} + logging.error(f"Unexpected error loading data: {e}") + visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} + + if 'organization_info' not in visitor_data_cache: + visitor_data_cache['organization_info'] = DEFAULT_ORGANIZATION_INFO + return visitor_data_cache -def save_visitor_data(data): +def save_data_sync(): with _data_lock: try: - visitor_data_cache.update(data) with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4) - logging.info(f"Visitor data successfully saved to {DATA_FILE}.") + logging.info(f"Data successfully saved to {DATA_FILE}.") upload_data_to_hf_async() except Exception as e: - logging.error(f"Error saving visitor data: {e}") + logging.error(f"Error saving data: {e}") def upload_data_to_hf(): if not HF_TOKEN_WRITE: @@ -126,9 +142,9 @@ def upload_data_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Update bonus data {get_bishkek_time().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Update data {get_current_time_bishkek().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("Bonus data successfully uploaded to Hugging Face.") + logging.info("Data successfully uploaded to Hugging Face.") except Exception as e: logging.error(f"Error uploading data to Hugging Face: {e}") @@ -164,7 +180,7 @@ def verify_telegram_data(init_data_str): if calculated_hash == received_hash: auth_date = int(parsed_data.get('auth_date', [0])[0]) current_time = int(time.time()) - if current_time - auth_date > 86400: + if current_time - auth_date > 86400: # 24 hours logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).") return parsed_data, True else: @@ -214,152 +230,60 @@ TEMPLATE = """ visibility: hidden; min-height: 100vh; } - .container { - max-width: 600px; - margin: 0 auto; - display: flex; - flex-direction: column; - gap: var(--padding-m); - } - .header { - text-align: left; - padding: var(--padding-m) 0; - } - .logo { - font-size: 2.5em; - font-weight: 800; - color: var(--text-color); - letter-spacing: -1px; - } + .container { max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--padding-m); } + .header { text-align: left; padding: var(--padding-m) 0; } + .logo { font-size: 2.5em; font-weight: 800; color: var(--text-color); letter-spacing: -1px; } .logo span { color: var(--brand-yellow); } - .welcome-text { - font-size: 1em; - color: var(--text-secondary-color); - margin-top: 4px; - } - .card-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--padding-m); - } - .bonus-card, .debt-card { - background: linear-gradient(145deg, #2a2a2a, #1c1c1c); - border-radius: calc(var(--border-radius) + 8px); - padding: var(--padding-l); - text-align: center; - position: relative; - overflow: hidden; - } - .bonus-card { - box-shadow: var(--shadow-glow); - border: 1px solid rgba(255, 193, 7, 0.2); - } - .debt-card { - box-shadow: var(--shadow-glow-red); - border: 1px solid rgba(244, 67, 54, 0.2); - } - .card-label { - font-size: 1.1em; - font-weight: 500; - color: var(--text-secondary-color); - margin-bottom: 12px; - } - .bonus-amount { - font-size: 3em; - font-weight: 800; - color: var(--brand-yellow); - letter-spacing: -2px; - line-height: 1; - } - .debt-amount { - font-size: 3em; - font-weight: 800; - color: var(--brand-red); - letter-spacing: -2px; - line-height: 1; - } - .client-id-card { - background-color: var(--card-bg); - border-radius: var(--border-radius); - padding: var(--padding-m); - display: flex; - justify-content: space-between; - align-items: center; - } - .client-id-label { - font-weight: 500; - color: var(--text-secondary-color); - } - .client-id-value { - font-size: 1.3em; - font-weight: 700; - color: var(--brand-yellow); - letter-spacing: 2px; - background-color: rgba(255,193,7,0.1); - padding: 4px 10px; - border-radius: 8px; - } - .history-section { - background-color: var(--card-bg); - border-radius: var(--border-radius); - padding: var(--padding-l); - } - .history-title { - font-size: 1.4em; - font-weight: 700; - margin-bottom: var(--padding-m); - padding-bottom: var(--padding-m); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - } - .history-list { - list-style: none; - padding: 0; - margin: 0; - max-height: 35vh; - overflow-y: auto; - } - .history-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 14px 4px; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - } - .history-item:last-child { border-bottom: none; } - .history-details { display: flex; flex-direction: column; } - .history-description { font-size: 1em; font-weight: 500; } - .history-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } - .history-amount { font-size: 1.1em; font-weight: 700; } - .history-amount.accrual { color: #4CAF50; } - .history-amount.deduction { color: #F44336; } - .no-history { - text-align: center; - color: var(--text-secondary-color); - padding: 2rem 0; - } - .action-buttons { - display: flex; - gap: var(--padding-m); - margin-top: var(--padding-m); - } - .btn-action { - background-color: var(--brand-yellow); - color: var(--brand-black); - padding: 12px 20px; - border: none; - border-radius: 12px; - font-size: 1.1em; - font-weight: 700; - cursor: pointer; - flex: 1; - text-align: center; - text-decoration: none; - transition: background-color 0.2s, transform 0.2s; - } - .btn-action:hover { - background-color: #e0a800; - transform: translateY(-2px); - } + .welcome-text { font-size: 1em; color: var(--text-secondary-color); margin-top: 4px; } + .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); } + .bonus-card, .debt-card { background: linear-gradient(145deg, #2a2a2a, #1c1c1c); border-radius: calc(var(--border-radius) + 8px); padding: var(--padding-l); text-align: center; position: relative; overflow: hidden; } + .bonus-card { box-shadow: var(--shadow-glow); border: 1px solid rgba(255, 193, 7, 0.2); } + .debt-card { box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); } + .card-label { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; } + .bonus-amount, .debt-amount { font-size: 3em; font-weight: 800; letter-spacing: -2px; line-height: 1; } + .bonus-amount { color: var(--brand-yellow); } + .debt-amount { color: var(--brand-red); } + .client-id-card { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-m); display: flex; justify-content: space-between; align-items: center; } + .client-id-label { font-weight: 500; color: var(--text-secondary-color); } + .client-id-value { font-size: 1.3em; font-weight: 700; color: var(--brand-yellow); letter-spacing: 2px; background-color: rgba(255,193,7,0.1); padding: 4px 10px; border-radius: 8px; } + + .navigation-tabs { display: flex; gap: 8px; margin-bottom: var(--padding-m); } + .navigation-tabs button { + flex-grow: 1; padding: 12px 10px; font-size: 0.9em; font-weight: 600; color: var(--text-secondary-color); + background-color: var(--card-bg); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; cursor: pointer; transition: all 0.2s ease; + } + .navigation-tabs button.active { color: var(--brand-black); background-color: var(--brand-yellow); border-color: var(--brand-yellow); } + .content-pane { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-l); display: none; } + .content-pane.active { display: block; } + .pane-title { font-size: 1.4em; font-weight: 700; margin-bottom: var(--padding-m); padding-bottom: var(--padding-m); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } + .list { list-style: none; padding: 0; margin: 0; max-height: 40vh; overflow-y: auto; } + .list-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); } + .list-item:last-child { border-bottom: none; } + .list-item-details { display: flex; flex-direction: column; } + .list-item-description { font-size: 1em; font-weight: 500; } + .list-item-subtext { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } + .list-item-amount { font-size: 1.1em; font-weight: 700; } + .amount-accrual { color: #4CAF50; } + .amount-deduction { color: #F44336; } + .no-items { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; } + + .biz-card-item { margin-bottom: 12px; } + .biz-card-label { font-size: 0.9em; color: var(--text-secondary-color); margin-bottom: 4px; } + .biz-card-value { font-size: 1.1em; font-weight: 500; } + .biz-card-phone-actions a { margin-right: 10px; color: var(--brand-yellow); text-decoration: none; font-size: 0.9em;} + .biz-card-link a { color: var(--brand-yellow); text-decoration: none; } + + .invoice-item-details { cursor: pointer; } + .invoice-details-modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(5px); } + .invoice-modal-content { background-color: var(--card-bg); margin: 15% auto; padding: var(--padding-l); border-radius: var(--border-radius); width: 90%; max-width: 500px; box-shadow: 0 5px 25px rgba(0,0,0,0.3); } + .invoice-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: var(--padding-m); margin-bottom: var(--padding-m); } + .invoice-modal-title { font-size: 1.3em; font-weight: 700; } + .invoice-modal-close { font-size: 1.8em; font-weight: bold; cursor: pointer; color: var(--text-secondary-color); } + .invoice-modal-body table { width: 100%; border-collapse: collapse; } + .invoice-modal-body th, .invoice-modal-body td { text-align: left; padding: 8px 4px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.95em; } + .invoice-modal-body th { font-weight: 600; color: var(--text-secondary-color); } + .invoice-modal-body .item-qty, .invoice-modal-body .item-price, .invoice-modal-body .item-total { text-align: right; } + .invoice-grand-total { text-align: right; font-size: 1.1em; font-weight: 700; margin-top: var(--padding-m); padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.1); } @@ -385,27 +309,28 @@ TEMPLATE = """

{{ user.id }}

-
- Мои накладные - Визитка + -
-

История операций

+
+

История операций

{% if user.combined_history %} -
    +
      {% for item in user.combined_history %} -
    • -
      - {{ item.description }} - {{ item.date_str }} +
    • +
      + {{ item.description }} + {{ item.date_str }}
      {% if item.transaction_type == 'bonus' %} - + {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }} {% elif item.transaction_type == 'debt' %} - + {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }} {% endif %} @@ -413,13 +338,75 @@ TEMPLATE = """ {% endfor %}
    {% else %} -

    Операций пока не было.

    +

    Операций пока не было.

    + {% endif %} +
+ +
+

Мои накладные

+ {% if user.invoices %} +
    + {% for invoice in user.invoices|sort(attribute='date', reverse=true) %} +
  • +
    + Накладная #{{ invoice.id.split('-')[-1] }} + {{ invoice.date_str }} +
    + {{ "%.2f"|format(invoice.grand_total|float) }} +
  • + {% endfor %} +
+ {% else %} +

У вас пока нет накладных.

+ {% endif %} +
+ +
+

Визитка организации

+
+

Название:

+

{{ organization.name }}

+
+
+

Адрес:

+

{{ organization.address }}

+
+
+

Телефоны:

+ {% for phone in organization.phones %} +

{{ phone }}

+

+ Позвонить + WhatsApp + Telegram +

+ {% endfor %} +
+ {% if organization.links %} +
+

Ссылки:

+ {% for link in organization.links %} + + {% endfor %} +
{% endif %}
+
+
+
+

Детали накладной

+ × +
+
+
+
+
+ """ -COMPANY_INFO_TEMPLATE = """ +ADMIN_TEMPLATE = """ - - Визитка Компании - + + Bonus Admin - + -
-
- -

{{ company_info.name }}

-
- -
-
- - Телефон - {{ company_info.phone1 }} +

Панель администратора

+
+ + +
+ +
+

Управление клиентами

+
+
{{ summary.total_users }}
Всего клиентов
+
{{ "%.2f"|format(summary.total_bonuses|float) }}
Всего бонусов
+
{{ "%.2f"|format(summary.total_debts|float) }}
Всего долгов
+
{{ summary.users_with_debt }}
Клиенты с долгом
- {% if company_info.phone2 %} -
- - Телефон - {{ company_info.phone2 }} +
+ +
- {% endif %} - {% if company_info.whatsapp %} -
- - WhatsApp - {{ company_info.whatsapp }} + {% if users %} +
+ {% for user in users|sort(attribute='visited_at', reverse=true) %} +
+ +
+
Бонусы
{{ "%.2f"|format(user.bonuses|float) }}
+
Долг
{{ "%.2f"|format(user.debts|float if user.debts else 0) }}
+
+ +
+ {% endfor %} +
+ {% else %}

Пользователей пока нет.

{% endif %} +
+ +
+

Настройки организации

+
+
+ + +
+
+ + +
+
+ +
+ {% for phone in organization.phones %} +
+ {% endfor %} +
+ +
+
+ + + +
+ +
- {% endif %} - {% if company_info.telegram %} - +
+ +
- - - - Назад -
+

Долги

+
+
+
+
+
+
Текущий долг: 0.00
+
Будет добавлено: +0.00
+
Будет погашено: -0.00

+
Итоговый долг: 0.00
+
+
+

Общая история операций

+ + + + + + - - -""" - -INVOICE_LIST_TEMPLATE = """ - - - - - - Мои накладные - - - - - - - -
-
- -

Мои накладные

-
- - {% if invoices %} - - {% else %} -

Накладных пока нет.

- {% endif %} - - Назад -
- - - - -""" - -ADMIN_TEMPLATE = """ - - - - - - Bonus Admin - - - - - - -
-

Панель администратора Bonus

-
-
-
{{ summary.total_users }}
-
Всего клиентов
-
-
-
{{ "%.2f"|format(summary.total_bonuses|float) }}
-
Всего бонусов
-
-
-
{{ "%.2f"|format(summary.total_debts|float) }}
-
Всего долгов
-
-
-
{{ summary.users_with_debt }}
-
Клиенты с долгом
-
-
- -
- - -
- -
-

Информация о компании

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- -
-

Создание накладной

-
-
-
- - - - -
-
- - -
-
- - -
-
- - -
-
-
-
- -
-
-
Всего товаров: 0
-
Общая сумма: 0.00
-
- -
- - {% if users %} -
- {% for user in users|sort(attribute='visited_at', reverse=true) %} -
- -
-
-
Бонусы
-
{{ "%.2f"|format(user.bonuses|float) }}
-
-
-
Долг
-
{{ "%.2f"|format(user.debts|float if user.debts else 0) }}
-
-
- -
- {% endfor %} -
- {% else %} -

Пользователей пока нет.

- {% endif %} -
- - - - - - @@ -1706,83 +1019,29 @@ ADMIN_TEMPLATE = """ @app.route('/') def index(): user_id_str = request.args.get('user_id_for_test') - - current_data = load_visitor_data() + all_data = load_data() user_data = {} + organization_info = all_data.get('organization_info', DEFAULT_ORGANIZATION_INFO) - if user_id_str and user_id_str in current_data: - user_data = current_data[user_id_str] + if user_id_str and user_id_str in all_data: + user_data = all_data[user_id_str] user_data['id'] = user_id_str bonus_history = user_data.get('history', []) - for item in bonus_history: - item['transaction_type'] = 'bonus' - + for item in bonus_history: item['transaction_type'] = 'bonus' debt_history = user_data.get('debt_history', []) - for item in debt_history: - item['transaction_type'] = 'debt' - - combined_history = sorted( - bonus_history + debt_history, - key=lambda x: x['date'], - reverse=True - ) + for item in debt_history: item['transaction_type'] = 'debt' + + combined_history = sorted(bonus_history + debt_history, key=lambda x: x['date'], reverse=True) user_data['combined_history'] = combined_history + user_data['invoices'] = user_data.get('invoices', []) else: - user_data = { - "id": "N/A", - "bonuses": 0, - "debts": 0, - "history": [], - "debt_history": [], - "combined_history": [] - } - - return render_template_string(TEMPLATE, user=user_data, user_id=user_id_str) - -@app.route('/company_info') -def view_company_info(): - user_id_str = request.args.get('user_id_for_test') - current_data = load_visitor_data() - company_info = current_data.get('company_info', {}) - - user_data_for_template = {} - if user_id_str and user_id_str in current_data: - user_data_for_template = {"id": user_id_str} - else: - user_data_for_template = {"id": None} - - return render_template_string(COMPANY_INFO_TEMPLATE, company_info=company_info, user_id=user_id_str) - -@app.route('/invoices/') -def view_invoice(user_id): - current_data = load_visitor_data() - user_data = current_data.get(user_id, {}) - invoices = user_data.get('invoices', []) - - for invoice in invoices: - invoice_date = invoice.get('date') - if invoice_date: - try: - dt_object = datetime.fromisoformat(invoice_date) - invoice['created_at_str'] = dt_object.strftime('%Y-%m-%d %H:%M:%S') - except ValueError: - invoice['created_at_str'] = invoice_date - - items = invoice.get('items', []) - for item in items: - item_price = item.get('unit_price', 0) - item_quantity = item.get('quantity', 0) - item['total_price'] = item_price * item_quantity - - total_invoice_amount = sum(item['total_price'] for item in items) - invoice['total_amount'] = total_invoice_amount - - return render_template_string(INVOICE_LIST_TEMPLATE, invoices=invoices, user_id=user_id) + user_data = { "id": "N/A", "bonuses": 0, "debts": 0, "history": [], "debt_history": [], "combined_history": [], "invoices": [] } + return render_template_string(TEMPLATE, user=user_data, organization=organization_info) @app.route('/verify', methods=['POST']) -def verify_data(): +def verify_data_endpoint(): try: req_data = request.get_json() init_data_str = req_data.get('initData') @@ -1790,7 +1049,6 @@ def verify_data(): return jsonify({"status": "error", "message": "Missing initData"}), 400 user_data_parsed, is_valid = verify_telegram_data(init_data_str) - user_info_dict = {} if user_data_parsed and 'user' in user_data_parsed: try: @@ -1798,211 +1056,71 @@ def verify_data(): user_info_dict = json.loads(user_json_str) except Exception as e: logging.error(f"Could not parse user JSON: {e}") - user_info_dict = {} - + if is_valid: tg_user_id = user_info_dict.get('id') if tg_user_id: - now = get_bishkek_time() - all_data = load_visitor_data() + now_dt = get_current_time_bishkek() + now_ts = now_dt.timestamp() + now_str = now_dt.strftime('%Y-%m-%d %H:%M:%S') - existing_user_key = None - for key, user_data_item in all_data.items(): - if str(user_data_item.get('telegram_id')) == str(tg_user_id): - existing_user_key = key - break - - if existing_user_key: - user_entry = all_data[existing_user_key] - user_entry.update({ - 'first_name': user_info_dict.get('first_name'), - 'last_name': user_info_dict.get('last_name'), - 'username': user_info_dict.get('username'), - 'photo_url': user_info_dict.get('photo_url'), - 'language_code': user_info_dict.get('language_code'), - 'visited_at': now.timestamp(), - 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S') - }) + with _data_lock: + all_users_data = {k: v for k, v in visitor_data_cache.items() if k != 'organization_info'} + existing_user_key = None + for key, user_data_item in all_users_data.items(): + if str(user_data_item.get('telegram_id')) == str(tg_user_id): + existing_user_key = key + break + user_id_to_save = existing_user_key - else: - new_user_id = generate_unique_id(all_data) - user_entry = { - 'id': new_user_id, - 'telegram_id': tg_user_id, - 'first_name': user_info_dict.get('first_name'), - 'last_name': user_info_dict.get('last_name'), - 'username': user_info_dict.get('username'), - 'photo_url': user_info_dict.get('photo_url'), - 'language_code': user_info_dict.get('language_code'), - 'is_premium': user_info_dict.get('is_premium', False), - 'phone_number': None, - 'visited_at': now.timestamp(), - 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'), - 'bonuses': 0, - 'history': [], - 'debts': 0, - 'debt_history': [], - 'invoices': [] - } - user_id_to_save = new_user_id - - save_visitor_data({user_id_to_save: user_entry}) - + if existing_user_key: + user_entry = visitor_data_cache[existing_user_key] + user_entry.update({ + 'first_name': user_info_dict.get('first_name'), 'last_name': user_info_dict.get('last_name'), + 'username': user_info_dict.get('username'), 'photo_url': user_info_dict.get('photo_url'), + 'language_code': user_info_dict.get('language_code'), + 'visited_at': now_ts, 'visited_at_str': now_str + }) + else: + new_user_id = generate_unique_id(all_users_data.keys()) + user_entry = { + 'id': new_user_id, 'telegram_id': tg_user_id, + 'first_name': user_info_dict.get('first_name'), 'last_name': user_info_dict.get('last_name'), + 'username': user_info_dict.get('username'), 'photo_url': user_info_dict.get('photo_url'), + 'language_code': user_info_dict.get('language_code'), 'is_premium': user_info_dict.get('is_premium', False), + 'phone_number': None, 'visited_at': now_ts, 'visited_at_str': now_str, + 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [] + } + visitor_data_cache[new_user_id] = user_entry + user_id_to_save = new_user_id + save_data_sync() return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save}) else: - return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400 + return jsonify({"status": "error", "verified": True, "message": "User ID not found"}), 400 else: logging.warning(f"Verification failed for user: {user_info_dict.get('id')}") return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403 - except Exception as e: logging.exception("Error in /verify endpoint") return jsonify({"status": "error", "message": "Internal server error"}), 500 @app.route('/admin') def admin_panel(): - current_data = load_visitor_data() + all_data = load_data() users_list = [] - for user_id, user_data in current_data.items(): - if user_id == 'company_info': continue - user_data['id'] = user_id - users_list.append(user_data) - - total_users = len(users_list) - total_bonuses = sum(u.get('bonuses', 0) for u in users_list) - total_debts = sum(u.get('debts', 0) for u in users_list) - users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0) - - company_info = current_data.get('company_info', {}) + for user_id, user_data_item in all_data.items(): + if user_id == 'organization_info': continue + user_data_item['id'] = user_id + users_list.append(user_data_item) summary_stats = { - "total_users": total_users, - "total_bonuses": total_bonuses, - "total_debts": total_debts, - "users_with_debt": users_with_debt + "total_users": len(users_list), + "total_bonuses": sum(u.get('bonuses', 0) for u in users_list), + "total_debts": sum(u.get('debts', 0) for u in users_list), + "users_with_debt": sum(1 for u in users_list if u.get('debts', 0) > 0) } - - return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats, company_info=company_info) - -@app.route('/admin/save_company_info', methods=['POST']) -def save_company_info(): - try: - data = request.get_json() - - company_info = { - 'name': data.get('name'), - 'phone1': data.get('phone1'), - 'phone2': data.get('phone2'), - 'whatsapp': data.get('whatsapp'), - 'telegram': data.get('telegram'), - 'address': data.get('address'), - 'website': data.get('website'), - 'facebook': data.get('facebook'), - 'instagram': data.get('instagram'), - 'tiktok': data.get('tiktok') - } - - all_data = load_visitor_data() - all_data['company_info'] = company_info - save_visitor_data({}) - - return jsonify({"status": "ok", "message": "Company info saved successfully"}), 200 - - except Exception as e: - logging.exception("Error in /admin/save_company_info endpoint") - return jsonify({"status": "error", "message": str(e)}), 500 - -@app.route('/admin/lookup_client', methods=['GET']) -def lookup_client(): - query = request.args.get('q', '').strip() - if not query: - return jsonify({"users": []}) - - all_data = load_visitor_data() - results = [] - for user_id, user_data in all_data.items(): - if user_id == 'company_info': continue - - match = False - if query.lower() in str(user_id).lower(): match = True - if user_data.get('first_name') and query.lower() in user_data['first_name'].lower(): match = True - if user_data.get('last_name') and query.lower() in user_data['last_name'].lower(): match = True - if user_data.get('username') and query.lower() in user_data['username'].lower(): match = True - if user_data.get('phone_number') and query in user_data['phone_number']: match = True - - if match: - results.append({ - "id": user_id, - "first_name": user_data.get('first_name'), - "last_name": user_data.get('last_name'), - "username": user_data.get('username'), - "phone_number": user_data.get('phone_number') - }) - if len(results) >= 5: break - - return jsonify({"users": results}) - -@app.route('/admin/create_invoice', methods=['POST']) -def create_invoice(): - try: - data = request.get_json() - items_data = data.get('items', []) - - if not items_data: - return jsonify({"status": "error", "message": "Накладная не может быть пустой."}), 400 - - all_data = load_visitor_data() - invoice_id_counter = all_data.get('invoice_id_counter', 0) - new_invoice_id = invoice_id_counter + 1 - - processed_items = [] - total_amount = 0 - - now = get_bishkek_time() - now_iso = now.isoformat() - now_str = now.strftime('%Y-%m-%d %H:%M:%S') - - for item_data in items_data: - client_id = item_data.get('client_id') - product_name = item_data.get('product_name') - quantity = float(item_data.get('quantity', 0)) - unit_price = float(item_data.get('unit_price', 0)) - - if not client_id or not product_name or quantity <= 0 or unit_price < 0: - return jsonify({"status": "error", "message": f"Некорректные данные для товара: {product_name or 'Неизвестно'}"}), 400 - - if client_id not in all_data or client_id == 'company_info': - return jsonify({"status": "error", "message": f"Клиент с ID '{client_id}' не найден."}), 404 - - item_total_price = quantity * unit_price - processed_items.append({ - "product_name": product_name, - "quantity": quantity, - "unit_price": unit_price, - "total_price": item_total_price - }) - total_amount += item_total_price - - new_invoice = { - "id": str(new_invoice_id), - "date": now_iso, - "items": processed_items, - "total_amount": total_amount - } - - client_invoices = all_data[client_id].setdefault('invoices', []) - client_invoices.append(new_invoice) - - all_data['invoice_id_counter'] = new_invoice_id - - save_visitor_data({}) - - return jsonify({"status": "ok", "message": "Накладная успешно создана.", "invoice_id": new_invoice_id}), 201 - - except Exception as e: - logging.exception("Error in /admin/create_invoice endpoint") - return jsonify({"status": "error", "message": str(e)}), 500 - + organization_info = all_data.get('organization_info', DEFAULT_ORGANIZATION_INFO) + return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats, organization=organization_info) @app.route('/admin/add_client', methods=['POST']) def add_client(): @@ -2010,163 +1128,150 @@ def add_client(): data = request.get_json() phone_number = data.get('phone_number') first_name = data.get('first_name') - if not phone_number or not first_name: return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400 - all_data = load_visitor_data() - - for user in all_data.values(): - if user_id == 'company_info': continue - if user.get('phone_number') == phone_number: - return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409 - - now = get_bishkek_time() - new_id = generate_unique_id(all_data) - - new_client = { - 'id': new_id, - 'telegram_id': None, - 'first_name': first_name, - 'last_name': None, - 'username': None, - 'photo_url': None, - 'language_code': 'ru', - 'is_premium': False, - 'phone_number': phone_number, - 'visited_at': now.timestamp(), - 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'), - 'bonuses': 0, - 'history': [], - 'debts': 0, - 'debt_history': [], - 'invoices': [] - } - - save_visitor_data({new_id: new_client}) - + with _data_lock: + all_users_data = {k: v for k, v in visitor_data_cache.items() if k != 'organization_info'} + for user in all_users_data.values(): + if user.get('phone_number') == phone_number: + return jsonify({"status": "error", "message": "Клиент с таким номером уже существует."}), 409 + + now_dt = get_current_time_bishkek() + new_id = generate_unique_id(all_users_data.keys()) + new_client = { + 'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None, 'username': None, + 'photo_url': None, 'language_code': 'ru', 'is_premium': False, 'phone_number': phone_number, + 'visited_at': now_dt.timestamp(), 'visited_at_str': now_dt.strftime('%Y-%m-%d %H:%M:%S'), + 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [] + } + visitor_data_cache[new_id] = new_client + save_data_sync() return jsonify({"status": "ok", "message": "Client added successfully"}), 201 - except Exception as e: - logging.exception("Error in /admin/add_client endpoint") + logging.exception("Error in /admin/add_client") return jsonify({"status": "error", "message": str(e)}), 500 - @app.route('/admin/add_transaction', methods=['POST']) def add_transaction(): try: data = request.get_json() - user_id = data.get('user_id') + user_id = str(data.get('user_id')) purchase_amount = float(data.get('purchase_amount', 0)) deduct_amount = float(data.get('deduct_amount', 0)) add_debt_amount = float(data.get('add_debt_amount', 0)) repay_debt_amount = float(data.get('repay_debt_amount', 0)) - if not user_id: - return jsonify({"status": "error", "message": "User ID is required"}), 400 - - user_id_str = str(user_id) - all_data = load_visitor_data() - - if user_id_str not in all_data or user_id_str == 'company_info': - return jsonify({"status": "error", "message": "User not found"}), 404 - - user = all_data[user_id_str] - now = get_bishkek_time() - now_iso = now.isoformat() - now_str = now.strftime('%Y-%m-%d %H:%M:%S') - - if deduct_amount > user.get('bonuses', 0): - return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400 + if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400 - if repay_debt_amount > user.get('debts', 0): - return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400 - - accrual_amount = purchase_amount * 0.02 - user['bonuses'] = user.get('bonuses', 0) + accrual_amount - deduct_amount - if 'history' not in user or not isinstance(user['history'], list): - user['history'] = [] - - if accrual_amount > 0: - user['history'].append({ - "type": "accrual", "amount": accrual_amount, - "description": f"Начисление с покупки {purchase_amount:.2f}", - "date": now_iso, "date_str": now_str - }) - if deduct_amount > 0: - user['history'].append({ - "type": "deduction", "amount": deduct_amount, - "description": "Списание бонусов", - "date": now_iso, "date_str": now_str - }) - - user['debts'] = user.get('debts', 0) + add_debt_amount - repay_debt_amount - if 'debt_history' not in user or not isinstance(user['debt_history'], list): - user['debt_history'] = [] - - if add_debt_amount > 0: - user['debt_history'].append({ - "type": "accrual", "amount": add_debt_amount, - "description": "Добавление долга", - "date": now_iso, "date_str": now_str - }) - if repay_debt_amount > 0: - user['debt_history'].append({ - "type": "payment", "amount": repay_debt_amount, - "description": "Погашение долга", - "date": now_iso, "date_str": now_str - }) + with _data_lock: + if user_id not in visitor_data_cache: + return jsonify({"status": "error", "message": "User not found"}), 404 + user = visitor_data_cache[user_id] + now_dt = get_current_time_bishkek() + now_iso = now_dt.isoformat() + now_str = now_dt.strftime('%Y-%m-%d %H:%M:%S') + + if deduct_amount > user.get('bonuses', 0): + return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400 + if repay_debt_amount > user.get('debts', 0): + return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400 + + accrual_amount = purchase_amount * 0.02 + user['bonuses'] = user.get('bonuses', 0) + accrual_amount - deduct_amount + user['history'] = user.get('history', []) + if accrual_amount > 0: + user['history'].append({"type": "accrual", "amount": accrual_amount, "description": f"Начисление с покупки {purchase_amount}", "date": now_iso, "date_str": now_str}) + if deduct_amount > 0: + user['history'].append({"type": "deduction", "amount": deduct_amount, "description": "Списание бонусов", "date": now_iso, "date_str": now_str}) + + user['debts'] = user.get('debts', 0) + add_debt_amount - repay_debt_amount + user['debt_history'] = user.get('debt_history', []) + if add_debt_amount > 0: + user['debt_history'].append({"type": "accrual", "amount": add_debt_amount, "description": "Добавление долга", "date": now_iso, "date_str": now_str}) + if repay_debt_amount > 0: + user['debt_history'].append({"type": "payment", "amount": repay_debt_amount, "description": "Погашение долга", "date": now_iso, "date_str": now_str}) - save_visitor_data({user_id_str: user}) - - return jsonify({ - "status": "ok", "message": "Transaction successful", - "new_balance": user['bonuses'], "new_debt": user['debts'] - }), 200 - + save_data_sync() + return jsonify({"status": "ok", "message": "Transaction successful", "new_balance": user['bonuses'], "new_debt": user['debts']}), 200 except Exception as e: - logging.exception("Error in /admin/add_transaction endpoint") + logging.exception("Error in /admin/add_transaction") return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/admin/delete_client', methods=['POST']) -def delete_client(): +def delete_client_endpoint(): try: data = request.get_json() - user_id = data.get('user_id') - - if not user_id: - return jsonify({"status": "error", "message": "User ID is required"}), 400 - - user_id_str = str(user_id) - load_visitor_data() + user_id = str(data.get('user_id')) + if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400 with _data_lock: - if user_id_str not in visitor_data_cache or user_id_str == 'company_info': + if user_id not in visitor_data_cache or user_id == 'organization_info': return jsonify({"status": "error", "message": "User not found"}), 404 - - user_to_delete = visitor_data_cache[user_id_str] + user_to_delete = visitor_data_cache[user_id] if user_to_delete.get('telegram_id') is not None: return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403 + del visitor_data_cache[user_id] + + save_data_sync() + return jsonify({"status": "ok", "message": "Client deleted successfully"}), 200 + except Exception as e: + logging.exception("Error in /admin/delete_client") + return jsonify({"status": "error", "message": str(e)}), 500 - del visitor_data_cache[user_id_str] +@app.route('/admin/save_organization_info', methods=['POST']) +def save_organization_info_endpoint(): + try: + data = request.get_json() + with _data_lock: + visitor_data_cache['organization_info'] = { + "name": data.get("name", DEFAULT_ORGANIZATION_INFO["name"]), + "address": data.get("address", DEFAULT_ORGANIZATION_INFO["address"]), + "phones": data.get("phones", DEFAULT_ORGANIZATION_INFO["phones"]), + "links": data.get("links", DEFAULT_ORGANIZATION_INFO["links"]), + } + save_data_sync() + return jsonify({"status": "ok", "message": "Organization info saved"}), 200 + except Exception as e: + logging.exception("Error saving organization info") + return jsonify({"status": "error", "message": str(e)}), 500 - try: - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4) - logging.info(f"User {user_id_str} deleted. Data saved to {DATA_FILE}.") - upload_data_to_hf_async() - except Exception as e: - logging.error(f"Error saving data after deletion: {e}") - return jsonify({"status": "error", "message": "Failed to save data after deletion"}), 500 +@app.route('/admin/add_invoice', methods=['POST']) +def add_invoice_endpoint(): + try: + data = request.get_json() + user_id = str(data.get('user_id')) + items = data.get('items', []) + grand_total = float(data.get('grand_total', 0)) - return jsonify({"status": "ok", "message": "Client deleted successfully"}), 200 + if not user_id: return jsonify({"status": "error", "message": "User ID required"}), 400 + if not items: return jsonify({"status": "error", "message": "Invoice must have items"}), 400 + with _data_lock: + if user_id not in visitor_data_cache or user_id == 'organization_info': + return jsonify({"status": "error", "message": "User not found"}), 404 + + user = visitor_data_cache[user_id] + if 'invoices' not in user or not isinstance(user['invoices'], list): + user['invoices'] = [] + + now_dt = get_current_time_bishkek() + new_invoice = { + "id": generate_invoice_id(), + "date": now_dt.isoformat(), + "date_str": now_dt.strftime('%Y-%m-%d %H:%M:%S'), + "items": items, + "grand_total": grand_total + } + user['invoices'].append(new_invoice) + + save_data_sync() + return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": new_invoice["id"]}), 201 except Exception as e: - logging.exception("Error in /admin/delete_client endpoint") + logging.exception("Error adding invoice") return jsonify({"status": "error", "message": str(e)}), 500 if __name__ == '__main__': - print("--- BONUS SYSTEM SERVER ---") print(f"Server starting on http://{HOST}:{PORT}") if not HF_TOKEN_READ or not HF_TOKEN_WRITE: print("WARNING: Hugging Face token(s) not set. Backup/restore functionality will be limited.") @@ -2174,8 +1279,7 @@ if __name__ == '__main__': print("Attempting initial data download from Hugging Face...") download_data_from_hf() - load_visitor_data() - + load_data() print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.") if HF_TOKEN_WRITE: