diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 2353d78..c383373 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -593,8 +593,11 @@ def process_queue(conn, settings): # Заказ уже согласован (клиент выбрал дату или менеджер поставил её сам) — # роботу звонить не о чем, иначе он предложит дни по заказу, где всё решено. # Заявку НЕ закрываем (правило владельца) — просто не обрабатываем. - agreed = (str(q.get("notification_status") or "") in ("confirmed", "manual_required", "opt_out") - or q.get("delivery_date") is not None) + # ВАЖНО: 'manual_required' («нужен менеджер») сюда не относится — это как раз + # заказ, где клиент дату НЕ выбрал, звонок робота по нему допустим. + agreed = (str(q.get("notification_status") or "") == "confirmed" + or q.get("delivery_date") is not None + or q.get("autocall_opt_out") is True) if agreed and not is_test: log.info( f"Queue item {q['queue_id']} (group {group_id}): заказ уже согласован " diff --git a/src/components/orders/AutocallStatusCard.jsx b/src/components/orders/AutocallStatusCard.jsx index 7d2016d..fa2dd57 100644 --- a/src/components/orders/AutocallStatusCard.jsx +++ b/src/components/orders/AutocallStatusCard.jsx @@ -7,7 +7,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Badge } from "../UI/Badge"; import { supabase } from "../../supabaseClient"; import { resolveAutocallStatus, AUTOCALL_STATUS_META } from "../../services/orderGroupViews"; -import { describeCallProcess } from "../../services/autocallProcess"; +import { describeCallProcess, isOrderAgreed } from "../../services/autocallProcess"; import { isAutocallPossible } from "../../services/autocallAvailability"; const OUTCOME_META = { @@ -161,11 +161,10 @@ export const AutocallStatusCard = ({ order, userRole }) => { }, [order?.id, order?.delivery_type, order?.created_at]); const autocallBlocked = availability ? !availability.possible : false; - // Заказ уже согласован — дату выбрал клиент (в том числе в звонке) или поставил менеджер - const orderAgreed = Boolean(order?.delivery_date || order?.deliveryDate) - || ["confirmed", "manual_required", "opt_out"].includes( - String(order?.notification_status || order?.notificationStatus || ""), - ); + // Заказ уже согласован — дату выбрал клиент (в том числе в звонке) или поставил менеджер. + // 'manual_required' («нужен менеджер») сюда НЕ входит: там дату как раз не выбрали, + // и звонок робота допустим (см. isOrderAgreed и её тесты). + const orderAgreed = isOrderAgreed(order); const process = useMemo( () => describeCallProcess({ diff --git a/src/services/autocallProcess.js b/src/services/autocallProcess.js index ec1d3da..3769c46 100644 --- a/src/services/autocallProcess.js +++ b/src/services/autocallProcess.js @@ -185,3 +185,19 @@ export const describeCallProcess = ({ queueItem, callAfter, settings = {}, now = hint: `Робот звонит только в рабочее время: ${padHour(startHour)}–${padHour(endHour)}, дни — ${workDays.join(", ")} — ${requested}`, }; }; + +/** + * Согласован ли уже заказ (дату выбрал клиент или поставил менеджер). + * + * Нужно карточке, чтобы не предлагать автодозвон по заказу, где всё решено. + * ВАЖНО: `manual_required` («нужен менеджер») — это НЕ согласованный заказ: + * дату клиент как раз не выбрал, и звонок робота по нему допустим. + * Отдельно стоит флаг «не звонить» (`autocall_opt_out`) — он про запрет звонков, + * а не про согласование, и обрабатывается своим состоянием. + */ +export const isOrderAgreed = (order) => { + if (!order) return false; + const date = order.delivery_date ?? order.deliveryDate; + const status = String(order.notification_status ?? order.notificationStatus ?? ""); + return Boolean(date) || status === "confirmed"; +}; diff --git a/src/services/autocallProcess.test.js b/src/services/autocallProcess.test.js index 280e0c8..0518bdd 100644 --- a/src/services/autocallProcess.test.js +++ b/src/services/autocallProcess.test.js @@ -6,6 +6,7 @@ import { describe, it, expect } from "vitest"; import { describeCallProcess, + isOrderAgreed, isCallTimeNow, moscowNow, nextCallDayLabel, @@ -160,3 +161,32 @@ describe("describeCallProcess — что видит логист", () => { expect(res.title).not.toContain("уже согласован"); }); }); + +describe("isOrderAgreed — когда автодозвон предлагать нельзя", () => { + it("дата выбрана — согласован", () => { + expect(isOrderAgreed({ delivery_date: "2026-09-18", notification_status: "pickup" })).toBe(true); + }); + + it("статус «согласовано» — согласован", () => { + expect(isOrderAgreed({ notification_status: "confirmed" })).toBe(true); + expect(isOrderAgreed({ notificationStatus: "confirmed" })).toBe(true); + }); + + it("«ручное управление» — НЕ согласован: клиент дату не выбрал", () => { + expect(isOrderAgreed({ notification_status: "manual_required", delivery_date: null })).toBe(false); + }); + + it("«первое приглашение отправлено» — НЕ согласован", () => { + expect(isOrderAgreed({ notification_status: "first_sms_sent" })).toBe(false); + }); + + it("«не звонить» само по себе не делает заказ согласованным", () => { + expect(isOrderAgreed({ notification_status: "first_sms_sent", autocall_opt_out: true })).toBe(false); + }); + + it("пустой заказ — не согласован", () => { + expect(isOrderAgreed(null)).toBe(false); + expect(isOrderAgreed(undefined)).toBe(false); + expect(isOrderAgreed({})).toBe(false); + }); +});