Refactor code structure for improved readability and maintainability

This commit is contained in:
k1nq
2026-09-16 13:41:55 +05:00
commit 7d0762ef17
27 changed files with 2718 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
import asyncio
import logging
import random
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import async_sessionmaker
from app.config import Settings
from app.models import Payment, PaymentStatus
from app.schemas import PaymentCreatedEvent, PaymentWebhook
logger = logging.getLogger(__name__)
class PaymentNotFoundError(Exception):
pass
class PaymentGateway:
def __init__(self, settings: Settings) -> None:
self._min_seconds = settings.processing_min_seconds
self._max_seconds = settings.processing_max_seconds
self._success_rate = settings.processing_success_rate
async def charge(self) -> PaymentStatus:
await asyncio.sleep(random.uniform(self._min_seconds, self._max_seconds))
if random.random() < self._success_rate:
return PaymentStatus.SUCCEEDED
return PaymentStatus.FAILED
class WebhookClient:
def __init__(self, timeout: float) -> None:
self._timeout = timeout
async def send(self, url: str, payload: PaymentWebhook) -> None:
async with httpx.AsyncClient(timeout=self._timeout) as client:
response = await client.post(url, json=payload.model_dump(mode="json"))
response.raise_for_status()
async def retry_async[T](
operation: Callable[[], Awaitable[T]],
*,
attempts: int,
base_delay: float,
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
) -> T:
for attempt in range(attempts):
try:
return await operation()
except Exception:
if attempt == attempts - 1:
raise
delay = base_delay * (2**attempt)
logger.warning(
"Payment handling attempt %d/%d failed; retrying in %.2fs",
attempt + 1,
attempts,
delay,
exc_info=True,
)
await sleep(delay)
raise RuntimeError("Retry loop completed without a result")
class PaymentProcessor:
def __init__(
self,
session_factory: async_sessionmaker,
gateway: PaymentGateway,
webhook_client: WebhookClient,
settings: Settings,
) -> None:
self._session_factory = session_factory
self._gateway = gateway
self._webhook_client = webhook_client
self._attempts = settings.processing_max_attempts
self._base_delay = settings.retry_base_delay
async def process(self, event: PaymentCreatedEvent) -> None:
await retry_async(
lambda: self._process_once(event),
attempts=self._attempts,
base_delay=self._base_delay,
)
async def _process_once(self, event: PaymentCreatedEvent) -> None:
async with self._session_factory() as session:
payment = await session.scalar(
select(Payment).where(Payment.id == event.payment_id).with_for_update()
)
if payment is None:
raise PaymentNotFoundError(str(event.payment_id))
if payment.status is PaymentStatus.PENDING:
payment.status = await self._gateway.charge()
payment.processed_at = datetime.now(UTC)
await session.commit()
if payment.webhook_delivered_at is not None:
return
if payment.processed_at is None:
raise RuntimeError("A terminal payment has no processing timestamp")
webhook_url = payment.webhook_url
webhook = PaymentWebhook(
payment_id=payment.id,
amount=payment.amount,
currency=payment.currency,
status=payment.status,
processed_at=payment.processed_at,
metadata=payment.metadata_,
)
await self._webhook_client.send(webhook_url, webhook)
async with self._session_factory() as session:
payment = await session.scalar(
select(Payment).where(Payment.id == event.payment_id).with_for_update()
)
if payment is None:
raise PaymentNotFoundError(str(event.payment_id))
if payment.webhook_delivered_at is None:
payment.webhook_delivered_at = datetime.now(UTC)
await session.commit()