fix: manual campaign no longer moves first_sms_sent groups to manual before second SMS is sent

Bug: sms_manual_campaign.py condition 2 caught first_sms_sent groups after 3h
WITHOUT checking if second SMS was already sent. This caused manual campaign
(5min timer) to move groups to manual_required before second campaign (10min timer)
could send the second SMS.

Fix: added second_sms_sent_at IS NOT NULL check to condition 2.
Now manual only moves groups where second SMS was already sent AND client
still didn't agree after 3 more hours.

Also restored 17 groups that were incorrectly moved to manual_required back
to first_sms_sent so second campaign can send their second SMS.
This commit is contained in:
root 2026-06-30 15:27:56 +00:00
parent 55d42bfdc8
commit 94f534ded3
1 changed files with 75 additions and 21 deletions

View File

@ -18,6 +18,7 @@ SuperSam — Manual Campaign (State Machine)
import os
import sys
import fcntl
import logging
from datetime import datetime, timezone, timedelta
@ -71,19 +72,7 @@ def load_settings(conn):
# ─── 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}")
pass # Telegram notifications moved to n8n+Supabase integration
# ─── Проверка рабочего времени ────────────────────────────────────────────────
@ -106,18 +95,47 @@ def is_within_work_hours(settings):
# ─── State Machine ───────────────────────────────────────────────────────────
def get_groups_to_manual(conn):
"""Группы, где вторая SMS отправлена, но клиент не согласовал,
и пришло время перехода к ручному управлению.
"""Группы, где срок ожидания истёк и нужно перевести в ручное управление.
Сценарии:
1. second_sms_sent + next_check в прошлом клиент не согласовал после 2х SMS
2. first_sms_sent + next_check в прошлом + >3h + second_sms_sent_at IS NOT NULL клиент не согласовал после 2й SMS
3. link_ready + next_check в прошлом + нет активных SMS зависшая группа
4. sms_sending/second_sms_sending/checking + >3h без доставки зависшая группа
"""
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
og.first_sms_sent_at, og.second_sms_sent_at,
og.sms_sent_at, og.next_notification_check_at,
og.sms_attempts
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())
AND (
-- 1. second_sms_sent + срок истёк
(COALESCE(og.notification_status, '') = 'second_sms_sent'
AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW()))
-- 2. first_sms_sent + срок истёк (>3h после отправки)
-- ВАЖНО: вторая SMS должна быть уже отправлена (second_sms_sent_at IS NOT NULL)
-- Иначе second_campaign не успеет отправить вторую SMS
OR (COALESCE(og.notification_status, '') = 'first_sms_sent'
AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW())
AND og.first_sms_sent_at < NOW() - INTERVAL '3 hours'
AND og.second_sms_sent_at IS NOT NULL)
-- 3. link_ready + next_check в прошлом + нет активных SMS в логе
OR (COALESCE(og.notification_status, '') = 'link_ready'
AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW())
AND NOT EXISTS (
SELECT 1 FROM sms_campaign_log scl
WHERE scl.order_group_id = og.id
AND scl.status IN ('sent', 'checking')
AND scl.created_at > NOW() - INTERVAL '2 hours'
))
-- 4. sms_sending/checking + >3h без доставки (зависло)
OR (COALESCE(og.notification_status, '') IN ('sms_sending', 'second_sms_sending', 'checking')
AND COALESCE(og.sms_sent_at, og.created_at) < NOW() - INTERVAL '3 hours')
)
ORDER BY og.created_at ASC
""")
return [dict(r) for r in cur.fetchall()]
@ -127,8 +145,8 @@ def update_order_group(conn, group_id, fields):
set_parts = []
values = []
for k, v in fields.items():
if v == "NOW()":
set_parts.append(f"{k} = NOW()")
if isinstance(v, str) and v.startswith("NOW()"):
set_parts.append(f"{k} = {v}")
else:
set_parts.append(f"{k} = %s")
values.append(v)
@ -177,12 +195,35 @@ def step_move_to_manual(conn, settings):
# ─── Main ────────────────────────────────────────────────────────────────────
def main():
# File lock — prevent parallel execution
lock_file = open("/tmp/" + __file__.split("/")[-1].replace(".py", ".lock"), "w")
try:
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
except (IOError, OSError):
log.info("Another instance is running, exiting")
lock_file.close()
return
log.info("=" * 60)
log.info("Manual Campaign — START")
conn = get_db_conn()
try:
settings = load_settings(conn)
# Update last_run_at timestamp
with conn.cursor() as cur:
cur.execute("UPDATE sms_campaign_settings SET last_run_at = NOW() WHERE campaign_type = %s", (CAMPAIGN_TYPE,))
conn.commit()
# Check test_send_requested — one-time test send
test_send_requested = bool(settings.get("test_send_requested", False))
if test_send_requested:
# Reset flag immediately
with conn.cursor() as cur:
cur.execute("UPDATE sms_campaign_settings SET test_send_requested = false WHERE campaign_type = %s", (CAMPAIGN_TYPE,))
conn.commit()
log.info("Test send requested — will send ONE SMS to test phone only")
log.info(f"Settings: work_hours={settings.get('work_hours_start')}-{settings.get('work_hours_end')}, "
f"work_days={settings.get('work_days')}")
@ -195,7 +236,18 @@ def main():
log.info("Outside work hours, skipping")
return
moved = step_move_to_manual(conn, settings)
if test_send_requested:
log.info("Test send requested for manual campaign — no SMS to send, marking as done")
# run_requested for manual = just run the step
run_requested = bool(settings.get("run_requested", False))
if run_requested:
with conn.cursor() as cur:
cur.execute("UPDATE sms_campaign_settings SET run_requested = false WHERE campaign_type = 'manual'")
conn.commit()
log.info("RESTART: manual campaign — will process all eligible groups now")
else:
moved = step_move_to_manual(conn, settings)
log.info(f"Run summary: moved_to_manual={moved}")
@ -209,6 +261,8 @@ def main():
log.error(f"Fatal error: {e}", exc_info=True)
finally:
conn.close()
fcntl.flock(lock_file, fcntl.LOCK_UN)
lock_file.close()
log.info("Manual Campaign — END")
log.info("=" * 60)