#!/usr/bin/env python3 """ SuperSam — Первая отправка согласования доставки (SMS) Заменяет n8n workflow "Первая отправка согласования" (JnfZwOnpjEazseW5) Логика: 1. SELECT order_groups WHERE status='ready_to_launch' AND delivery_status='pending_confirmation' 2. Для каждой группы с delivery_link и notification_status='link_ready': a. Отправка SMS через sms.ru/sms/send b. Ждёт wait_between_checks_seconds (25 сек по умолчанию) c. Проверка статуса через sms.ru/sms/status d. Если код 103 (доставлено) → UPDATE order_groups + лог e. Если код 102 (в пути) → повторная проверка каждые wait_between_checks f. Если в течение max_check_duration_minutes (90 мин) не доставлено → повторная отправка SMS g. Если повторная отправка тоже не доставлена за max_check_duration → manual_required 3. Telegram-уведомление после каждой отправки 4. Полный лог в sms_campaign_log (видно в админке) """ import os import sys import time import json import logging import requests from datetime import datetime, timezone, timedelta from decimal import Decimal import psycopg2 from psycopg2.extras import RealDictCursor # ─── Конфигурация ──────────────────────────────────────────────────────────── DB_HOST = os.environ.get("DB_HOST", "10.0.1.3") DB_PORT = os.environ.get("DB_PORT", "5432") DB_NAME = os.environ.get("DB_NAME", "postgres") DB_USER = os.environ.get("DB_USER", "supabase_admin") DB_PASS = os.environ.get("DB_PASS", "4fe80bb21c7c3d17a8d8b226adf7a479") TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "25164483") SMS_API_ID = os.environ.get("SMS_API_ID", "C92063B3-95ED-8559-157B-1946EB5A2486") SMS_SEND_URL = "https://sms.ru/sms/send" SMS_STATUS_URL = "https://sms.ru/sms/status" LOG_FILE = "/var/log/supersam-sms-first.log" # ─── Логирование ───────────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler(LOG_FILE), logging.StreamHandler(sys.stdout), ], ) log = logging.getLogger("sms_first") # ─── БД ────────────────────────────────────────────────────────────────────── def get_db_conn(): return psycopg2.connect( host=DB_HOST, port=DB_PORT, dbname=DB_NAME, user=DB_USER, password=DB_PASS, ) def load_settings(conn): """Загружает настройки из sms_campaign_settings (управляются мегаадмином)""" with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT * FROM sms_campaign_settings WHERE campaign_type = 'first_sms'") row = cur.fetchone() if not row: return { "wait_between_checks_seconds": 25, "max_check_duration_minutes": 90, "max_attempts": 2, "enabled": True, "telegram_chat_id": "25164483", } return dict(row) def get_pending_groups(conn): """Получает группы для отправки первой SMS""" with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(""" SELECT id, group_key, customer_name, customer_phone, customer_phone_normalized, delivery_link, notification_status, status, delivery_status FROM order_groups WHERE status = 'ready_to_launch' AND delivery_status = 'pending_confirmation' ORDER BY created_at ASC """) return [dict(row) for row in cur.fetchall()] def update_order_group(conn, group_id, fields): """Обновляет order_groups""" with conn.cursor() as cur: set_parts = [] values = [] for k, v in fields.items(): if v == "NOW()": set_parts.append(f"{k} = NOW()") else: set_parts.append(f"{k} = %s") values.append(v) values.append(group_id) cur.execute(f"UPDATE order_groups SET {', '.join(set_parts)} WHERE id = %s", values) conn.commit() def insert_sms_log(conn, **kwargs): """Записывает лог в sms_campaign_log""" with conn.cursor() as cur: cols = ", ".join(kwargs.keys()) placeholders = ", ".join(["%s"] * len(kwargs)) cur.execute(f"INSERT INTO sms_campaign_log ({cols}) VALUES ({placeholders})", list(kwargs.values())) conn.commit() def update_sms_log(conn, log_id, **kwargs): """Обновляет запись лога""" with conn.cursor() as cur: set_parts = [] values = [] for k, v in kwargs.items(): set_parts.append(f"{k} = %s") values.append(v) values.append(log_id) cur.execute(f"UPDATE sms_campaign_log SET {', '.join(set_parts)}, updated_at = NOW() WHERE id = %s", values) conn.commit() # ─── SMS API ───────────────────────────────────────────────────────────────── def send_sms(phone, message, api_id): """Отправляет SMS через sms.ru, возвращает (sms_id, raw_response)""" try: # Нормализуем телефон: убираем всё кроме цифр, добавляем 7 clean_phone = "".join(c for c in phone if c.isdigit()) if clean_phone.startswith("8"): clean_phone = "7" + clean_phone[1:] elif not clean_phone.startswith("7"): clean_phone = "7" + clean_phone resp = requests.post(SMS_SEND_URL, params={ "api_id": api_id, "to": clean_phone, }, data={"msg": message}, timeout=30) text = resp.text log.info(f"SMS send response: {text[:200]}") # Парсим sms_id: sms.ru возвращает строки, вторая строка = sms_id lines = text.strip().split("\n") if len(lines) >= 2 and lines[0].strip() == "100": sms_id = lines[1].strip() return sms_id, text elif lines and lines[0].strip() == "100": sms_id = lines[1].strip() if len(lines) > 1 else "" return sms_id, text else: return None, text except Exception as e: log.error(f"SMS send error: {e}") return None, str(e) def check_sms_status(sms_id, api_id): """Проверяет статус SMS, возвращает код (102=в пути, 103=доставлено)""" try: resp = requests.post(SMS_STATUS_URL, params={ "api_id": api_id, "sms_id": sms_id, }, timeout=30) text = resp.text log.info(f"SMS status response for {sms_id}: {text[:200]}") lines = text.strip().split("\n") if len(lines) >= 2: code = lines[1].strip() return code, text return None, text except Exception as e: log.error(f"SMS status check error: {e}") return None, str(e) # ─── Telegram ──────────────────────────────────────────────────────────────── def send_telegram(message, chat_id): """Отправляет сообщение в Telegram""" if not TELEGRAM_BOT_TOKEN: log.warning("TELEGRAM_BOT_TOKEN not set, skipping Telegram notification") return try: resp = requests.post( f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage", json={"chat_id": chat_id, "text": message, "parse_mode": "HTML"}, timeout=10, ) if resp.status_code != 200: log.warning(f"Telegram error: {resp.text[:200]}") except Exception as e: log.warning(f"Telegram send failed: {e}") # ─── Основная логика ───────────────────────────────────────────────────────── def process_group(conn, group, settings): """Обрабатывает одну группу: отправка SMS → проверка доставки → UPDATE""" group_id = group["id"] customer_name = group.get("customer_name") or group.get("group_key", "—") phone = group.get("customer_phone_normalized") or group.get("customer_phone", "") delivery_link = group.get("delivery_link", "") if not delivery_link: log.warning(f"Group {group_id}: no delivery_link, skipping") return False if group.get("notification_status") != "link_ready": log.info(f"Group {group_id}: notification_status={group.get('notification_status')}, skipping") return False api_id = settings.get("sms_api_id", SMS_API_ID) wait_sec = settings.get("wait_between_checks_seconds", 25) max_duration_min = settings.get("max_check_duration_minutes", 90) max_attempts = settings.get("max_attempts", 2) tg_chat = settings.get("telegram_chat_id", TELEGRAM_CHAT_ID) sms_text = f"Ваш заказ готов. Согласуйте дату доставки по ссылке: {delivery_link}" log.info(f"=== Group {group_id} ({customer_name}, phone={phone}) ===") for attempt in range(1, max_attempts + 1): log.info(f"Group {group_id}: attempt {attempt}/{max_attempts}") # Запись лога отправки log_id = None with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(""" INSERT INTO sms_campaign_log (campaign_type, order_group_id, customer_phone, sms_text, status, attempts, created_at) VALUES ('first_sms', %s, %s, %s, 'sending', %s, NOW()) RETURNING id """, (str(group_id), phone, sms_text, attempt)) log_id = cur.fetchone()["id"] conn.commit() # Отправка SMS sms_id, raw = send_sms(phone, sms_text, api_id) if not sms_id: error = raw[:500] if raw else "No sms_id in response" log.error(f"Group {group_id}: SMS send failed: {error}") update_sms_log(conn, log_id, status="send_failed", sms_code=None, error_message=error) if attempt < max_attempts: log.info(f"Group {group_id}: retrying in {wait_sec}s...") time.sleep(wait_sec) continue else: update_order_group(conn, group_id, { "notification_status": "send_failed", "last_sms_error": error[:200], "status": "ready_to_launch", # остаётся для повторной попытки }) send_telegram(f"❌ SMS не отправлена: {customer_name} ({phone})\nОшибка: {error[:200]}", tg_chat) return False # SMS отправлена успешно update_sms_log(conn, log_id, status="sent", sms_id=sms_id, sms_code="100") log.info(f"Group {group_id}: SMS sent, sms_id={sms_id}") # Цикл проверки доставки (до max_check_duration_minutes) start_time = datetime.now(timezone.utc) max_duration = timedelta(minutes=max_duration_min) check_interval = wait_sec while datetime.now(timezone.utc) - start_time < max_duration: log.info(f"Group {group_id}: waiting {check_interval}s before status check...") time.sleep(check_interval) code, raw_status = check_sms_status(sms_id, api_id) update_sms_log(conn, log_id, status="checking", sms_code=code) if code == "103": # Доставлено! log.info(f"Group {group_id}: SMS delivered (code=103)!") update_sms_log(conn, log_id, status="delivered", sms_code="103") now_iso = datetime.now(timezone.utc).isoformat() update_order_group(conn, group_id, { "notification_status": "first_sms_sent", "sms_attempts": attempt, "first_sms_sent_at": "NOW()", "last_sms_error": None, "sms_sent_at": "NOW()", "next_notification_check_at": (datetime.now(timezone.utc) + timedelta(hours=settings.get("second_sms_delay_hours", 3))).isoformat(), "status": "first_sms_sent", }) send_telegram( f"✅ SMS доставлена: {customer_name} ({phone})\nПопытка {attempt}\nsms_id: {sms_id}", tg_chat, ) return True elif code == "102": # В пути — продолжаем ждать log.info(f"Group {group_id}: SMS in transit (code=102), continuing to check...") continue elif code in ("104", "105", "106", "107", "108"): # Ошибка доставки error_msg = f"SMS error code: {code}" log.error(f"Group {group_id}: {error_msg}") update_sms_log(conn, log_id, status="error", sms_code=code, error_message=error_msg) # Прерываем внутренний цикл, переходим к следующей попытке break else: # Неизвестный код — продолжаем проверять log.warning(f"Group {group_id}: unknown SMS code: {code}") continue # Время проверки истекло или ошибка — повторная отправка elapsed = (datetime.now(timezone.utc) - start_time).total_seconds() / 60 log.warning(f"Group {group_id}: check duration exceeded ({elapsed:.0f} min) or error, attempt {attempt}") update_sms_log(conn, log_id, status="timeout", error_message=f"Not delivered in {max_duration_min} min") if attempt < max_attempts: log.info(f"Group {group_id}: will retry SMS send...") continue else: # Все попытки исчерпаны update_order_group(conn, group_id, { "notification_status": "send_failed", "last_sms_error": f"Not delivered after {max_attempts} attempts", "status": "ready_to_launch", }) send_telegram( f"⚠️ SMS не доставлена после {max_attempts} попыток: {customer_name} ({phone})\nТребуется ручное управление", tg_chat, ) return False return False def main(): log.info("=" * 60) log.info("SMS First Campaign — START") conn = get_db_conn() try: settings = load_settings(conn) log.info(f"Settings: wait={settings.get('wait_between_checks_seconds')}s, " f"max_duration={settings.get('max_check_duration_minutes')}min, " f"max_attempts={settings.get('max_attempts')}") if not settings.get("enabled", True): log.info("Campaign disabled, exiting") return groups = get_pending_groups(conn) log.info(f"Found {len(groups)} groups with status=ready_to_launch, delivery_status=pending_confirmation") if not groups: log.info("No groups to process, exiting") return tg_chat = settings.get("telegram_chat_id", TELEGRAM_CHAT_ID) sent = 0 delivered = 0 failed = 0 for group in groups: try: result = process_group(conn, group, settings) if result: delivered += 1 else: failed += 1 sent += 1 except Exception as e: log.error(f"Error processing group {group.get('id')}: {e}", exc_info=True) failed += 1 # Сводка в Telegram summary = (f"📊 Первая отправка — сводка\n\n" f"Групп найдено: {len(groups)}\n" f"Отправлено: {sent}\n" f"Доставлено: {delivered}\n" f"Ошибок: {failed}") send_telegram(summary, tg_chat) log.info(f"Summary: sent={sent}, delivered={delivered}, failed={failed}") finally: conn.close() log.info("SMS First Campaign — END") log.info("=" * 60) if __name__ == "__main__": main()