"""Стаб Checkbox для тестов и локальной разработки. ЕТТН-чеки на тестовой кассе Checkbox не работают, поэтому полный цикл без боевой кассы прогоняется только через стаб. Состояние — в памяти процесса. `auto_complete_after=N`: чек переходит в `DONE` на N-м вызове `get_ettn` — имитирует получение посылки клиентом. """ from __future__ import annotations import uuid from typing import Any from app.schemas.checkbox import EttnOut, EttnStatus from app.services.checkbox.client import CheckboxCredentials, CheckboxError class StubCheckboxClient: def __init__(self, *, auto_complete_after: int | None = None) -> None: self.auto_complete_after = auto_complete_after self.orders: dict[str, EttnOut] = {} self.bodies: dict[str, dict[str, Any]] = {} self._polls: dict[str, int] = {} def set_status(self, ettn_id: str, status: str, *, raw_error: str | None = None) -> None: ettn = self.orders[ettn_id] receipt_id = str(uuid.uuid4()) if status.startswith(EttnStatus.DONE) else ettn.receipt_id self.orders[ettn_id] = ettn.model_copy( update={"status": status, "receipt_id": receipt_id, "raw_error": raw_error} ) async def sign_in(self, creds: CheckboxCredentials) -> None: if not creds.license_key or not creds.pin_code: raise CheckboxError("Вход кассира не удался: пустой ключ или PIN") async def create_ettn(self, creds: CheckboxCredentials, body: dict[str, Any]) -> EttnOut: waybill = body["receipt_body"]["payments"][0]["ettn"] if await self.find_ettn(creds, waybill) is not None: raise CheckboxError(f"ЕТТН {waybill} уже привязана к чеку") ettn = EttnOut( id=str(uuid.uuid4()), status=EttnStatus.CREATED, ettn_number=waybill, total_sum=body["receipt_body"]["payments"][0]["value"], ) self.orders[ettn.id] = ettn self.bodies[ettn.id] = body return ettn async def get_ettn(self, creds: CheckboxCredentials, ettn_id: str) -> EttnOut: if ettn_id not in self.orders: raise CheckboxError(f"ЕТТН-чек {ettn_id} не найден") self._polls[ettn_id] = self._polls.get(ettn_id, 0) + 1 if ( self.auto_complete_after is not None and self.orders[ettn_id].status == EttnStatus.CREATED and self._polls[ettn_id] >= self.auto_complete_after ): self.set_status(ettn_id, EttnStatus.DONE) return self.orders[ettn_id] async def find_ettn(self, creds: CheckboxCredentials, waybill_number: str) -> EttnOut | None: for ettn in self.orders.values(): if ettn.ettn_number == waybill_number and ettn.status != EttnStatus.CANCELLED: return ettn return None async def delete_ettn(self, creds: CheckboxCredentials, ettn_id: str) -> None: # API и worker — разные процессы с разными стабами: неизвестный id # считаем уже удалённым, иначе локально отмена не работала бы. if ettn_id in self.orders: self.set_status(ettn_id, EttnStatus.CANCELLED)