- CRM_USE_STUB: local runs no longer reach the live CRM. With only the Checkbox stub, a stub receipt would still move the real order to PACKED. Refused in production, same as CHECKBOX_USE_STUB. - scripts/deploy.sh: backup, fast-forward main, build, health check and code rollback on failure. scripts/backup.sh: pg_dump with verification and 14-day rotation (used by cron and deploy.sh). - Gitea Actions CI: ruff + pytest, oxlint + build. - DEPLOY.md runbook; CLAUDE.md rules for safe local development. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.4 KiB
Python
78 lines
3.4 KiB
Python
"""ARQ worker: опрос Nova Poshta и ЕТТН-чеки Checkbox.
|
|
|
|
Запускается отдельным процессом: `arq app.worker.WorkerSettings`.
|
|
- `create_ettn_receipt` — задача, которую ставит API после запроса кассира;
|
|
- `poll_np_statuses` — раз в минуту статусы ТТН по ключам НП касс и привязка заказов к кассам;
|
|
- `poll_receipts` — раз в минуту повтор зависших `pending`, статусы `created`-чеков
|
|
и повтор смены статуса заказа в CRM (PACKED), если CRM была недоступна.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from arq import Retry, cron, func
|
|
from arq.connections import RedisSettings
|
|
|
|
from app.core.config import settings
|
|
from app.core.logging import configure_logging, get_logger
|
|
from app.db.session import SessionFactory
|
|
from app.services import receipts as receipts_service
|
|
from app.services.checkbox.client import CheckboxRateLimitedError, get_checkbox_client
|
|
from app.services.crm.client import get_crm_client
|
|
from app.services.nova_poshta.np_client import NpTrackingClient
|
|
from app.services.orders import sync_np_statuses
|
|
|
|
log = get_logger(__name__)
|
|
|
|
# Повторы `create_ettn_receipt` при лимите частоты: пауза retry_after + 5 с × номер попытки.
|
|
_RATE_LIMIT_BACKOFF = 5
|
|
_CREATE_ETTN_MAX_TRIES = 6
|
|
|
|
|
|
async def startup(ctx: dict[str, Any]) -> None:
|
|
configure_logging()
|
|
ctx["np_client"] = NpTrackingClient()
|
|
ctx["checkbox_client"] = get_checkbox_client()
|
|
ctx["crm_client"] = get_crm_client()
|
|
log.info("worker_starting", environment=settings.environment)
|
|
|
|
|
|
async def poll_np_statuses(ctx: dict[str, Any]) -> None:
|
|
async with SessionFactory() as session:
|
|
await sync_np_statuses(session, ctx["np_client"])
|
|
log.info("np_statuses_polled")
|
|
|
|
|
|
async def create_ettn_receipt(ctx: dict[str, Any], receipt_id: str) -> None:
|
|
async with SessionFactory() as session:
|
|
try:
|
|
await receipts_service.create_ettn_for_receipt(
|
|
session, ctx["checkbox_client"], uuid.UUID(receipt_id)
|
|
)
|
|
except CheckboxRateLimitedError as exc:
|
|
# Лимит частоты Новой Почты: повтор с нарастающей паузой. Когда попытки
|
|
# кончатся, `pending`-чек подберёт cron `poll_receipts`.
|
|
raise Retry(defer=exc.retry_after + _RATE_LIMIT_BACKOFF * ctx["job_try"]) from exc
|
|
# Чек принят Checkbox — сразу переводим заказ в CRM в PACKED.
|
|
await receipts_service.sync_crm_statuses(session, ctx["crm_client"])
|
|
|
|
|
|
async def poll_receipts(ctx: dict[str, Any]) -> None:
|
|
async with SessionFactory() as session:
|
|
await receipts_service.retry_pending_receipts(session, ctx["checkbox_client"])
|
|
await receipts_service.sync_ettn_statuses(session, ctx["checkbox_client"])
|
|
await receipts_service.sync_crm_statuses(session, ctx["crm_client"])
|
|
log.info("receipts_polled")
|
|
|
|
|
|
class WorkerSettings:
|
|
redis_settings = RedisSettings.from_dsn(settings.redis_url)
|
|
on_startup = startup
|
|
functions = [func(create_ettn_receipt, max_tries=_CREATE_ETTN_MAX_TRIES)]
|
|
cron_jobs = [
|
|
cron(poll_np_statuses, minute=set(range(60)), run_at_startup=True),
|
|
cron(poll_receipts, minute=set(range(60)), second=30),
|
|
]
|