This commit is contained in:
Yuriy Yuriev
2026-07-21 00:05:25 +07:00
parent 4bd13ad041
commit 9aa072792d
+30 -2
View File
@@ -6,7 +6,7 @@ import asyncio
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime, timedelta
from typing import Dict, Optional from typing import Dict, Optional
from managers.task_manager import TaskParams from managers.task_manager import TaskParams
@@ -309,6 +309,9 @@ class RubleBalanceStorage(_IntBalanceStorage):
class PaymentStorage: class PaymentStorage:
"""Хранилище ожидающих платежей.""" """Хранилище ожидающих платежей."""
# Сколько держать обработанные платежи ради защиты от дублей вебхука
PROCESSED_TTL_DAYS = 7
def __init__(self, file_path: str = "data/payments.json"): def __init__(self, file_path: str = "data/payments.json"):
self.file_path = Path(file_path) self.file_path = Path(file_path)
self.file_path.parent.mkdir(parents=True, exist_ok=True) 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: with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2) 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 def save(self, payment_id: str, data: dict) -> None:
async with self._lock: async with self._lock:
payments = await asyncio.to_thread(self._load_sync) payments = await asyncio.to_thread(self._load_sync)
payments = self._prune_processed(payments)
payments[payment_id] = data payments[payment_id] = data
await asyncio.to_thread(self._save_sync, payments) await asyncio.to_thread(self._save_sync, payments)
@@ -357,10 +380,15 @@ class PaymentStorage:
return data return data
async def get_by_user(self, user_id: int) -> Optional[dict]: async def get_by_user(self, user_id: int) -> Optional[dict]:
"""Возвращает незакрытый счёт пользователя.
Обработанные платежи остаются в файле как защита от повторного
начисления по дублю вебхука, но открытым счётом уже не считаются.
"""
async with self._lock: async with self._lock:
payments = await asyncio.to_thread(self._load_sync) payments = await asyncio.to_thread(self._load_sync)
for pid, data in payments.items(): 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 {**data, "payment_id": pid}
return None return None