intra_max_chatbot/database/queries/intradesk.py

375 lines
13 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.

"""Запросы к таблицам intradesk_* (синхронизированные справочники)."""
from __future__ import annotations
import re
import aiosqlite
# ── Clients ───────────────────────────────────────────────────────────────────
async def upsert_client(db: aiosqlite.Connection, item: dict) -> None:
await db.execute(
"""
INSERT INTO intradesk_clients
(id, name, client_type, email, address, description, updated_at, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
client_type = excluded.client_type,
email = excluded.email,
address = excluded.address,
description = excluded.description,
updated_at = excluded.updated_at,
synced_at = excluded.synced_at
""",
(
item["id"],
item.get("name") or "",
item.get("clientType"),
item.get("email"),
item.get("address"),
item.get("description"),
item.get("updatedAt"),
),
)
async def get_all_clients(db: aiosqlite.Connection, search: str = "") -> list[dict]:
q = "SELECT * FROM intradesk_clients"
params: list = []
if search:
q += " WHERE name LIKE ?"
params.append(f"%{search}%")
q += " ORDER BY name"
async with db.execute(q, params) as cur:
rows = await cur.fetchall()
return [dict(r) for r in rows]
async def get_client(db: aiosqlite.Connection, client_id: int) -> dict | None:
async with db.execute(
"SELECT * FROM intradesk_clients WHERE id = ?", (client_id,)
) as cur:
row = await cur.fetchone()
return dict(row) if row else None
# ── Users ─────────────────────────────────────────────────────────────────────
async def upsert_user(db: aiosqlite.Connection, item: dict) -> None:
# Основная группа — первая в списке groups
client_id: int | None = None
groups = item.get("groups") or []
if groups:
client_id = groups[0].get("userGroupId")
# Телефон — первый из списка
phones = item.get("phoneNumbers") or []
phone = phones[0].get("phoneNumber") if phones else item.get("phoneNumber")
await db.execute(
"""
INSERT INTO intradesk_users
(id, name, first_name, last_name, middle_name, email, phone, client_id, is_archived, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
first_name = excluded.first_name,
last_name = excluded.last_name,
middle_name = excluded.middle_name,
email = excluded.email,
phone = excluded.phone,
client_id = excluded.client_id,
is_archived = excluded.is_archived,
synced_at = excluded.synced_at
""",
(
item["id"],
item.get("name") or "",
item.get("firstName") or "",
item.get("lastName") or "",
item.get("middleName") or "",
item.get("email") or "",
phone,
client_id,
1 if item.get("isArchived") else 0,
),
)
async def get_users_for_client(
db: aiosqlite.Connection, client_id: int
) -> list[dict]:
async with db.execute(
"SELECT * FROM intradesk_users WHERE client_id = ? ORDER BY name",
(client_id,),
) as cur:
rows = await cur.fetchall()
return [dict(r) for r in rows]
# ── Tasks ─────────────────────────────────────────────────────────────────────
async def upsert_task(
db: aiosqlite.Connection, item: dict, dicts: dict[int, str] | None = None
) -> None:
dicts = dicts or {}
status_id = item.get("status")
status_name = dicts.get(status_id, "") if status_id else ""
priority_id = item.get("priority")
priority_name = dicts.get(priority_id, "") if priority_id else ""
executor_id = item.get("executor")
executor_name = dicts.get(executor_id, "") if executor_id else ""
initiator_id = item.get("initiator")
initiator_name = dicts.get(initiator_id, "") if initiator_id else ""
await db.execute(
"""
INSERT INTO intradesk_tasks (
id, task_number, name, description,
status_id, status_name,
priority_id, priority_name,
client_id, initiator_id, initiator_name,
executor_id, executor_name, executor_group,
service_name, service_full_name,
created_at, updated_at, closed_at, created_by, updated_by,
synced_at
)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
ON CONFLICT(id) DO UPDATE SET
task_number = excluded.task_number,
name = excluded.name,
description = excluded.description,
status_id = excluded.status_id,
status_name = excluded.status_name,
priority_id = excluded.priority_id,
priority_name = excluded.priority_name,
client_id = excluded.client_id,
initiator_id = excluded.initiator_id,
initiator_name = excluded.initiator_name,
executor_id = excluded.executor_id,
executor_name = excluded.executor_name,
executor_group = excluded.executor_group,
service_name = excluded.service_name,
service_full_name = excluded.service_full_name,
created_at = excluded.created_at,
updated_at = excluded.updated_at,
closed_at = excluded.closed_at,
created_by = excluded.created_by,
updated_by = excluded.updated_by,
synced_at = excluded.synced_at
""",
(
item["id"],
item.get("tasknumber"),
item.get("name") or "",
item.get("description") or "",
status_id,
status_name,
priority_id,
priority_name,
item.get("clientid"),
initiator_id,
initiator_name,
executor_id,
executor_name,
item.get("executorgroup"),
item.get("servicename") or "",
item.get("servicefullname") or "",
item.get("createdat"),
item.get("updatedat"),
item.get("closedat"),
item.get("createdby") or "",
item.get("updatedby") or "",
),
)
async def get_tasks_for_client(
db: aiosqlite.Connection,
client_id: int,
status_filter: str = "",
page: int = 0,
page_size: int = 30,
) -> list[dict]:
q = "SELECT * FROM intradesk_tasks WHERE client_id = ?"
params: list = [client_id]
if status_filter:
q += " AND status_name = ?"
params.append(status_filter)
q += " ORDER BY updated_at DESC LIMIT ? OFFSET ?"
params += [page_size, page * page_size]
async with db.execute(q, params) as cur:
rows = await cur.fetchall()
return [dict(r) for r in rows]
async def count_tasks_for_client(
db: aiosqlite.Connection, client_id: int, status_filter: str = ""
) -> int:
q = "SELECT COUNT(*) FROM intradesk_tasks WHERE client_id = ?"
params: list = [client_id]
if status_filter:
q += " AND status_name = ?"
params.append(status_filter)
async with db.execute(q, params) as cur:
row = await cur.fetchone()
return row[0] if row else 0
async def get_distinct_statuses(db: aiosqlite.Connection) -> list[str]:
async with db.execute(
"SELECT DISTINCT status_name FROM intradesk_tasks WHERE status_name != '' ORDER BY status_name"
) as cur:
rows = await cur.fetchall()
return [r[0] for r in rows]
# ── Sync log ──────────────────────────────────────────────────────────────────
async def start_sync_log(db: aiosqlite.Connection, sync_type: str) -> int:
async with db.execute(
"INSERT INTO intradesk_sync_log (sync_type, status) VALUES (?, 'running')",
(sync_type,),
) as cur:
log_id = cur.lastrowid
await db.commit()
return log_id # type: ignore[return-value]
async def finish_sync_log(
db: aiosqlite.Connection,
log_id: int,
status: str,
records_synced: int,
error_message: str = "",
) -> None:
await db.execute(
"""
UPDATE intradesk_sync_log
SET status = ?, records_synced = ?, error_message = ?,
finished_at = datetime('now')
WHERE id = ?
""",
(status, records_synced, error_message or None, log_id),
)
await db.commit()
async def get_recent_sync_logs(
db: aiosqlite.Connection, limit: int = 10
) -> list[dict]:
async with db.execute(
"SELECT * FROM intradesk_sync_log ORDER BY id DESC LIMIT ?", (limit,)
) as cur:
rows = await cur.fetchall()
return [dict(r) for r in rows]
async def get_last_sync(
db: aiosqlite.Connection, sync_type: str
) -> dict | None:
async with db.execute(
"""
SELECT * FROM intradesk_sync_log
WHERE sync_type = ? AND status = 'success'
ORDER BY id DESC LIMIT 1
""",
(sync_type,),
) as cur:
row = await cur.fetchone()
return dict(row) if row else None
# ── Whitelist sync ────────────────────────────────────────────────────────────
def _normalize_phone(raw: str) -> str:
"""Убираем всё кроме цифр, заменяем ведущую 8 → 7 (как в боте)."""
digits = re.sub(r"\D", "", raw or "")
if digits.startswith("8") and len(digits) == 11:
digits = "7" + digits[1:]
return digits
async def sync_whitelist_from_intradesk(db: aiosqlite.Connection) -> int:
"""
Наполнить таблицы organizations и org_users данными из intradesk_clients /
intradesk_users. Вызывается автоматически после синхронизации.
Возвращает количество затронутых строк в org_users.
"""
# 1. Загружаем существующий маппинг intradesk_id → local org_id
org_map: dict[int, int] = {}
async with db.execute(
"SELECT id, intradesk_id FROM organizations WHERE intradesk_id IS NOT NULL"
) as cur:
for row in await cur.fetchall():
org_map[row[1]] = row[0]
# 2. Для каждого intradesk_client создаём или обновляем organizations
async with db.execute("SELECT id, name FROM intradesk_clients") as cur:
clients = await cur.fetchall()
for c in clients:
iid, cname = c[0], c[1]
if iid in org_map:
# Обновляем имя на случай переименования в Intradesk
await db.execute(
"UPDATE organizations SET name = ? WHERE intradesk_id = ?",
(cname, iid),
)
else:
# Создаём новую организацию
async with db.execute(
"INSERT INTO organizations (name, intradesk_id) VALUES (?, ?)",
(cname, iid),
) as cur:
org_map[iid] = cur.lastrowid # type: ignore[assignment]
await db.commit()
# 3. Для каждого активного intradesk_user с телефоном — upsert в org_users
async with db.execute(
"""
SELECT id, phone, client_id, name, first_name, last_name
FROM intradesk_users
WHERE phone IS NOT NULL AND phone != '' AND is_archived = 0
"""
) as cur:
users = await cur.fetchall()
count = 0
for u in users:
raw_phone = u[1] or ""
phone = _normalize_phone(raw_phone)
if len(phone) < 7:
continue # явно некорректный номер
client_id = u[2]
org_id = org_map.get(client_id) if client_id else None
if org_id is None:
continue # клиент ещё не синхронизирован
# Формируем отображаемое имя
display = (u[3] or "").strip() or (
f"{u[5] or ''} {u[4] or ''}".strip() or None
)
await db.execute(
"""
INSERT INTO org_users (org_id, phone, display_name)
VALUES (?, ?, ?)
ON CONFLICT(phone) DO UPDATE SET
org_id = excluded.org_id,
display_name = COALESCE(excluded.display_name, org_users.display_name)
""",
(org_id, phone, display),
)
count += 1
await db.commit()
return count