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:
parent
55d42bfdc8
commit
94f534ded3
|
|
@ -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 (
|
||||
-- 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,6 +236,17 @@ def main():
|
|||
log.info("Outside work hours, skipping")
|
||||
return
|
||||
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue