diff --git a/managers/storage.py b/managers/storage.py index 7e959fb..15b8522 100644 --- a/managers/storage.py +++ b/managers/storage.py @@ -6,7 +6,7 @@ import asyncio import json import logging from pathlib import Path -from datetime import datetime +from datetime import datetime, timedelta from typing import Dict, Optional from managers.task_manager import TaskParams @@ -309,6 +309,9 @@ class RubleBalanceStorage(_IntBalanceStorage): class PaymentStorage: """Хранилище ожидающих платежей.""" + # Сколько держать обработанные платежи ради защиты от дублей вебхука + PROCESSED_TTL_DAYS = 7 + def __init__(self, file_path: str = "data/payments.json"): self.file_path = Path(file_path) self.file_path.parent.mkdir(parents=True, exist_ok=True) @@ -327,9 +330,29 @@ class PaymentStorage: with open(self.file_path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) + def _prune_processed(self, payments: dict) -> dict: + """Удаляет обработанные платежи старше PROCESSED_TTL_DAYS. + + Без этого файл рос бы бесконечно, а его читают при каждой + отрисовке кабинета. + """ + cutoff = datetime.now() - timedelta(days=self.PROCESSED_TTL_DAYS) + kept = {} + for pid, data in payments.items(): + processed_at = data.get("processed_at") + if data.get("processed") and processed_at: + try: + if datetime.fromisoformat(processed_at) < cutoff: + continue + except ValueError: + pass # некорректная дата — запись оставляем + kept[pid] = data + return kept + async def save(self, payment_id: str, data: dict) -> None: async with self._lock: payments = await asyncio.to_thread(self._load_sync) + payments = self._prune_processed(payments) payments[payment_id] = data await asyncio.to_thread(self._save_sync, payments) @@ -357,10 +380,15 @@ class PaymentStorage: return data async def get_by_user(self, user_id: int) -> Optional[dict]: + """Возвращает незакрытый счёт пользователя. + + Обработанные платежи остаются в файле как защита от повторного + начисления по дублю вебхука, но открытым счётом уже не считаются. + """ async with self._lock: payments = await asyncio.to_thread(self._load_sync) for pid, data in payments.items(): - if data.get("user_id") == user_id: + if data.get("user_id") == user_id and not data.get("processed"): return {**data, "payment_id": pid} return None