"""Синхронизация локальной очереди заказов с CRM и работа с ней.""" from __future__ import annotations from datetime import UTC, datetime from decimal import Decimal from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models.order import Order from app.services.crm.client import CrmClient from app.services.nova_poshta.client import NovaPoshtaClient _CRM_STATUS = "APPROVED" # NP отклоняет запросы с более чем 100 накладными за раз (см. np_client.py). _NP_BATCH_SIZE = 100 def _to_kopecks(amount: str) -> int: return int((Decimal(amount) * 100).to_integral_value()) def _parse_crm_datetime(value: str) -> datetime: return datetime.strptime(value, "%Y-%m-%d %H:%M:%S") async def sync_orders_from_crm(session: AsyncSession, crm: CrmClient) -> None: """Подтягивает заказы CRM (статус APPROVED) в локальную таблицу. Уже скрытые (`is_deleted`) заказы не восстанавливаются и не перезаписываются — иначе кнопка «Удалить» переставала бы работать при следующем открытии дашборда, т.к. CRM продолжает возвращать эти заказы как есть. """ crm_orders = await crm.get_orders(status=_CRM_STATUS) if not crm_orders: return existing = await session.scalars( select(Order).where(Order.id.in_(order.id for order in crm_orders)) ) existing_by_id = {order.id: order for order in existing} for crm_order in crm_orders: local = existing_by_id.get(crm_order.id) if local is not None: if local.is_deleted: continue else: local = Order(id=crm_order.id) session.add(local) local.create_date_time = _parse_crm_datetime(crm_order.create_date_time) local.recipient_name = crm_order.recipient_name local.recipient_phone = crm_order.recipient_phone local.recipient_email = crm_order.recipient_email local.waybill_number = crm_order.waybill_number local.notes = crm_order.notes local.total_amount_kopecks = _to_kopecks(crm_order.total.amount) local.goods = [good.model_dump(by_alias=False) for good in crm_order.goods] await session.commit() async def list_orders(session: AsyncSession, *, has_receipt: bool) -> list[Order]: receipt_filter = ( Order.receipt_created_at.is_not(None) if has_receipt else Order.receipt_created_at.is_(None) ) result = await session.scalars( select(Order) .where(Order.is_deleted.is_(False)) .where(receipt_filter) .order_by(Order.create_date_time.desc()) ) return list(result) async def sync_np_statuses(session: AsyncSession, np: NovaPoshtaClient) -> None: """Обновляет статус ТТН и сумму наложенного платежа для заказов без чека. Вызывается ARQ worker'ом раз в минуту (см. `app/worker.py`), а не из HTTP-запроса: опрос статусов не должен зависеть от того, открыт ли сейчас дашборд. """ orders = await session.scalars( select(Order) .where(Order.is_deleted.is_(False)) .where(Order.receipt_created_at.is_(None)) .where(Order.waybill_number.is_not(None)) ) orders_by_waybill: dict[str, Order] = {order.waybill_number: order for order in orders} if not orders_by_waybill: return waybill_numbers = list(orders_by_waybill) for i in range(0, len(waybill_numbers), _NP_BATCH_SIZE): batch = waybill_numbers[i : i + _NP_BATCH_SIZE] statuses = await np.get_statuses(waybill_numbers=batch) for tracking_status in statuses: order = orders_by_waybill.get(tracking_status.number) if order is None: continue order.np_status = tracking_status.status order.np_status_code = tracking_status.status_code order.np_cod_amount_kopecks = ( _to_kopecks(tracking_status.cod_amount) if tracking_status.cod_amount else None ) order.np_payment_status = tracking_status.payment_status await session.commit() async def delete_order(session: AsyncSession, order_id: str) -> Order | None: order = await session.get(Order, order_id) if order is None or order.is_deleted: return None order.is_deleted = True order.deleted_at = datetime.now(UTC) return order