457 lines
16 KiB
Python
457 lines
16 KiB
Python
"""Запросы к таблицам 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
|
||
|
||
|
||
# ── Tasks by local org_id ─────────────────────────────────────────────────────
|
||
|
||
_BOT_PAGE_SIZE = 8
|
||
|
||
|
||
async def get_tasks_for_local_org(
|
||
db: aiosqlite.Connection,
|
||
local_org_id: int,
|
||
page: int = 0,
|
||
page_size: int = _BOT_PAGE_SIZE,
|
||
) -> list[dict]:
|
||
"""
|
||
Вернуть intradesk_tasks для локальной организации (через organizations.intradesk_id).
|
||
"""
|
||
async with db.execute(
|
||
"""
|
||
SELECT it.*
|
||
FROM intradesk_tasks it
|
||
JOIN organizations o ON o.intradesk_id = it.client_id
|
||
WHERE o.id = ?
|
||
ORDER BY it.updated_at DESC
|
||
LIMIT ? OFFSET ?
|
||
""",
|
||
(local_org_id, page_size, page * page_size),
|
||
) as cur:
|
||
rows = await cur.fetchall()
|
||
return [dict(r) for r in rows]
|
||
|
||
|
||
async def count_tasks_for_local_org(
|
||
db: aiosqlite.Connection, local_org_id: int
|
||
) -> int:
|
||
async with db.execute(
|
||
"""
|
||
SELECT COUNT(*)
|
||
FROM intradesk_tasks it
|
||
JOIN organizations o ON o.intradesk_id = it.client_id
|
||
WHERE o.id = ?
|
||
""",
|
||
(local_org_id,),
|
||
) as cur:
|
||
row = await cur.fetchone()
|
||
return row[0] if row else 0
|
||
|
||
|
||
async def get_task_for_local_org(
|
||
db: aiosqlite.Connection, task_id: int, local_org_id: int
|
||
) -> dict | None:
|
||
"""Получить одну задачу, убедившись что она принадлежит организации пользователя."""
|
||
async with db.execute(
|
||
"""
|
||
SELECT it.*
|
||
FROM intradesk_tasks it
|
||
JOIN organizations o ON o.intradesk_id = it.client_id
|
||
WHERE it.id = ? AND o.id = ?
|
||
""",
|
||
(task_id, local_org_id),
|
||
) as cur:
|
||
row = await cur.fetchone()
|
||
return dict(row) if row else None
|
||
|
||
|
||
# ── Contact person lookup ─────────────────────────────────────────────────────
|
||
|
||
async def get_intradesk_contact_id_for_phone(
|
||
db: aiosqlite.Connection, client_intradesk_id: int, phone: str
|
||
) -> int | None:
|
||
"""
|
||
Найти id контактного лица в Intradesk по клиенту + нормализованному телефону.
|
||
phone — только цифры (как в bot_users.phone).
|
||
"""
|
||
async with db.execute(
|
||
"SELECT id, phone FROM intradesk_users WHERE client_id = ?",
|
||
(client_intradesk_id,),
|
||
) as cur:
|
||
rows = await cur.fetchall()
|
||
for row in rows:
|
||
if _normalize_phone(row[1] or "") == phone:
|
||
return row[0]
|
||
return 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
|