supersam/scripts/sms_manual_campaign.py

218 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
SuperSam — Manual Campaign (State Machine)
Заменяет n8n workflow "Ручное управление"
Логика:
Если после второй SMS (notification_status = 'second_sms_sent')
прошло auto_manual_after_hours и клиент не согласовал
(delivery_status = 'pending_confirmation'):
→ notification_status = 'manual_required'
→ delivery_status = 'manual_confirmation_required'
→ next_notification_check_at = +3 мин
→ Telegram-уведомление
Расписание: 9-21, Пн-Сб (настраивается из админки)
Скрипт НЕ отправляет SMS — только перевод в ручное управление.
"""
import os
import sys
import logging
from datetime import datetime, timezone, timedelta
import requests
import psycopg2
from psycopg2.extras import RealDictCursor
# ─── Конфигурация ────────────────────────────────────────────────────────────
DB_HOST = os.environ.get("DB_HOST", "10.0.4.12")
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")
LOG_FILE = "/var/log/supersam-sms-manual.log"
CAMPAIGN_TYPE = "manual"
# ─── Логирование ─────────────────────────────────────────────────────────────
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_manual")
# ─── БД ──────────────────────────────────────────────────────────────────────
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):
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT * FROM sms_campaign_settings WHERE campaign_type = %s", (CAMPAIGN_TYPE,))
row = cur.fetchone()
if not row:
return {"enabled": True, "auto_manual_after_hours": 3, "telegram_chat_id": TELEGRAM_CHAT_ID}
return dict(row)
# ─── Telegram ────────────────────────────────────────────────────────────────
def send_telegram(message, chat_id):
if not TELEGRAM_BOT_TOKEN:
log.warning("TELEGRAM_BOT_TOKEN not set, skipping Telegram")
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 failed: {e}")
# ─── Проверка рабочего времени ────────────────────────────────────────────────
def is_within_work_hours(settings):
now_msk = datetime.now(timezone(timedelta(hours=3)))
today_num = now_msk.weekday() + 1
allowed_days = set()
work_days_str = settings.get("work_days", "1,2,3,4,5,6")
for part in str(work_days_str).split(","):
part = part.strip()
if part.isdigit():
allowed_days.add(int(part))
if today_num not in allowed_days:
return False
hour = now_msk.hour
start_h = settings.get("work_hours_start", 9)
end_h = settings.get("work_hours_end", 21)
return start_h <= hour < end_h
# ─── State Machine ───────────────────────────────────────────────────────────
def get_groups_to_manual(conn):
"""Группы, где вторая SMS отправлена, но клиент не согласовал,
и пришло время перехода к ручному управлению.
"""
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("""
SELECT og.id, og.group_key, og.customer_name, og.customer_phone,
og.notification_status, og.delivery_link,
og.second_sms_sent_at, og.next_notification_check_at
FROM order_groups og
WHERE og.delivery_status = 'pending_confirmation'
AND COALESCE(og.notification_status, '') = 'second_sms_sent'
AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW())
ORDER BY og.created_at ASC
""")
return [dict(r) for r in cur.fetchall()]
def update_order_group(conn, group_id, fields):
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 step_move_to_manual(conn, settings):
"""Перевод групп в ручное управление"""
tg_chat = settings.get("telegram_chat_id", TELEGRAM_CHAT_ID)
delay_minutes = settings.get("auto_manual_after_hours", 3)
groups = get_groups_to_manual(conn)
log.info(f"Step 1: {len(groups)} groups to move to manual")
moved = 0
for group in groups:
group_id = str(group["id"])
name = group.get("customer_name") or group.get("group_key", "")
phone = group.get("customer_phone", "")
delivery_link = group.get("delivery_link", "")
log.info(f"Group {group_id}: moving to manual_required ({name})")
update_order_group(conn, group_id, {
"notification_status": "manual_required",
"delivery_status": "manual_confirmation_required",
"sms_attempts": 2,
"last_sms_error": None,
"next_notification_check_at": f"NOW() + INTERVAL '{int(delay_minutes)} minutes'",
"status": "manual_required",
})
send_telegram(
f"🔧 <b>Ручное управление</b>\n{name} ({phone})\n"
f"Клиент не согласовал доставку после двух SMS\n"
f"Ссылка: {delivery_link}",
tg_chat,
)
moved += 1
return moved
# ─── Main ────────────────────────────────────────────────────────────────────
def main():
log.info("=" * 60)
log.info("Manual Campaign — START")
conn = get_db_conn()
try:
settings = load_settings(conn)
log.info(f"Settings: work_hours={settings.get('work_hours_start')}-{settings.get('work_hours_end')}, "
f"work_days={settings.get('work_days')}")
if not settings.get("enabled", True):
log.info("Campaign disabled, exiting")
return
# Только в рабочие часы
if not is_within_work_hours(settings):
log.info("Outside work hours, skipping")
return
moved = step_move_to_manual(conn, settings)
log.info(f"Run summary: moved_to_manual={moved}")
if moved > 0:
send_telegram(
f"📊 <b>Ручное управление</b>\nПереведено в ручное: {moved}",
settings.get("telegram_chat_id", TELEGRAM_CHAT_ID),
)
except Exception as e:
log.error(f"Fatal error: {e}", exc_info=True)
finally:
conn.close()
log.info("Manual Campaign — END")
log.info("=" * 60)
if __name__ == "__main__":
main()