mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-20 03:11:47 +00:00
Сохранение времени оплаты:
- Добавлен параметр operation_time в create_receipt()
- Чеки из очереди создаются с оригинальным временем платежа
- Парсинг created_at из Redis очереди
Защита от дублей (3 уровня):
- Проверка transaction.receipt_uuid перед созданием
- Redis ключ nalogo:created:{payment_id} с TTL 30 дней
- Сохранение receipt_uuid в транзакцию после создания
Бесконечные повторы:
- Убрано удаление чеков после 10 попыток
- Чеки остаются в очереди до успешной отправки
Обработка ошибок:
- Добавлена обработка 500 и "внутренняя ошибка" как временной недоступности
Сверка чеков:
- Заменена API сверка на сверку по логам (logs/current/payments.log)
- Кнопка "Без чеков" → "Сверка чеков" с прямым показом сверки
- Исправлена навигация кнопок "Назад"
240 lines
7.5 KiB
Python
240 lines
7.5 KiB
Python
"""
|
||
Income API implementation.
|
||
Based on PHP library's Api\\Income class.
|
||
"""
|
||
|
||
from datetime import datetime, date
|
||
from decimal import Decimal
|
||
from typing import Any, Optional
|
||
|
||
from ._http import AsyncHTTPClient
|
||
from .dto.income import (
|
||
AtomDateTime,
|
||
CancelCommentType,
|
||
CancelRequest,
|
||
IncomeClient,
|
||
IncomeRequest,
|
||
IncomeServiceItem,
|
||
IncomeType,
|
||
PaymentType,
|
||
)
|
||
|
||
|
||
class IncomeAPI:
|
||
"""
|
||
Income API for creating and managing receipts.
|
||
|
||
Provides async methods for:
|
||
- Creating income receipts (single or multiple items)
|
||
- Cancelling income receipts
|
||
|
||
Maps to PHP Api\\Income functionality.
|
||
"""
|
||
|
||
def __init__(self, http_client: AsyncHTTPClient):
|
||
self.http = http_client
|
||
|
||
async def create(
|
||
self,
|
||
name: str,
|
||
amount: Decimal | float | int | str,
|
||
quantity: Decimal | float | int | str = 1,
|
||
operation_time: datetime | None = None,
|
||
client: IncomeClient | None = None,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
Create income receipt with single service item.
|
||
|
||
Maps to PHP Income::create() method.
|
||
|
||
Args:
|
||
name: Service name/description
|
||
amount: Service amount (converted to Decimal)
|
||
quantity: Service quantity (converted to Decimal, default: 1)
|
||
operation_time: Operation datetime (default: now)
|
||
client: Client information (default: individual client)
|
||
|
||
Returns:
|
||
Dictionary with response data including approvedReceiptUuid
|
||
|
||
Raises:
|
||
ValidationException: For validation errors
|
||
DomainException: For other API errors
|
||
"""
|
||
# Convert to IncomeServiceItem
|
||
service_item = IncomeServiceItem(
|
||
name=name,
|
||
amount=Decimal(str(amount)),
|
||
quantity=Decimal(str(quantity)),
|
||
)
|
||
|
||
return await self.create_multiple_items([service_item], operation_time, client)
|
||
|
||
async def create_multiple_items(
|
||
self,
|
||
services: list[IncomeServiceItem],
|
||
operation_time: datetime | None = None,
|
||
client: IncomeClient | None = None,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
Create income receipt with multiple service items.
|
||
|
||
Maps to PHP Income::createMultipleItems() method.
|
||
|
||
Args:
|
||
services: List of service items
|
||
operation_time: Operation datetime (default: now)
|
||
client: Client information (default: individual client)
|
||
|
||
Returns:
|
||
Dictionary with response data including approvedReceiptUuid
|
||
|
||
Raises:
|
||
ValidationException: For validation errors (empty items, invalid amounts, etc.)
|
||
DomainException: For other API errors
|
||
"""
|
||
if not services:
|
||
raise ValueError("Services cannot be empty")
|
||
|
||
# Validate client for legal entity (mirrors PHP validation)
|
||
if client and client.income_type == IncomeType.FROM_LEGAL_ENTITY:
|
||
if not client.inn:
|
||
raise ValueError("Client INN cannot be empty for legal entity")
|
||
if not client.display_name:
|
||
raise ValueError("Client DisplayName cannot be empty for legal entity")
|
||
|
||
# Calculate total amount (mirrors PHP BigDecimal logic)
|
||
total_amount = sum(item.get_total_amount() for item in services)
|
||
|
||
# Create request object
|
||
request = IncomeRequest(
|
||
operation_time=(
|
||
AtomDateTime.from_datetime(operation_time)
|
||
if operation_time
|
||
else AtomDateTime.now()
|
||
),
|
||
request_time=AtomDateTime.now(),
|
||
services=services,
|
||
total_amount=str(total_amount),
|
||
client=client or IncomeClient(),
|
||
payment_type=PaymentType.CASH,
|
||
ignore_max_total_income_restriction=False,
|
||
)
|
||
|
||
# Make API request
|
||
response = await self.http.post("/income", json_data=request.model_dump())
|
||
return response.json() # type: ignore[no-any-return]
|
||
|
||
async def cancel(
|
||
self,
|
||
receipt_uuid: str,
|
||
comment: CancelCommentType | str,
|
||
operation_time: datetime | None = None,
|
||
request_time: datetime | None = None,
|
||
partner_code: str | None = None,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
Cancel income receipt.
|
||
|
||
Maps to PHP Income::cancel() method.
|
||
|
||
Args:
|
||
receipt_uuid: Receipt UUID to cancel
|
||
comment: Cancellation reason (enum or string)
|
||
operation_time: Operation datetime (default: now)
|
||
request_time: Request datetime (default: now)
|
||
partner_code: Partner code (optional)
|
||
|
||
Returns:
|
||
Dictionary with cancellation response data
|
||
|
||
Raises:
|
||
ValidationException: For validation errors (empty UUID, invalid comment)
|
||
DomainException: For other API errors
|
||
"""
|
||
# Validate receipt UUID
|
||
if not receipt_uuid.strip():
|
||
raise ValueError("Receipt UUID cannot be empty")
|
||
|
||
# Convert comment to enum if string
|
||
if isinstance(comment, str):
|
||
# Try to find matching enum value
|
||
comment_enum = None
|
||
for enum_val in CancelCommentType:
|
||
if enum_val.value == comment:
|
||
comment_enum = enum_val
|
||
break
|
||
|
||
if comment_enum is None:
|
||
valid_comments = [e.value for e in CancelCommentType]
|
||
raise ValueError(
|
||
f"Comment is invalid. Must be one of: {valid_comments}"
|
||
)
|
||
|
||
comment = comment_enum
|
||
|
||
# Create request object
|
||
request = CancelRequest(
|
||
operation_time=(
|
||
AtomDateTime.from_datetime(operation_time)
|
||
if operation_time
|
||
else AtomDateTime.now()
|
||
),
|
||
request_time=(
|
||
AtomDateTime.from_datetime(request_time)
|
||
if request_time
|
||
else AtomDateTime.now()
|
||
),
|
||
comment=comment,
|
||
receipt_uuid=receipt_uuid.strip(),
|
||
partner_code=partner_code,
|
||
)
|
||
|
||
# Make API request
|
||
response = await self.http.post("/cancel", json_data=request.model_dump())
|
||
return response.json() # type: ignore[no-any-return]
|
||
|
||
async def get_list(
|
||
self,
|
||
from_date: Optional[date] = None,
|
||
to_date: Optional[date] = None,
|
||
limit: int = 100,
|
||
offset: int = 0,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
Get list of income records for a period.
|
||
|
||
Args:
|
||
from_date: Start date (default: 30 days ago)
|
||
to_date: End date (default: today)
|
||
limit: Maximum number of records (default: 100)
|
||
offset: Offset for pagination (default: 0)
|
||
|
||
Returns:
|
||
Dictionary with income records list
|
||
|
||
Raises:
|
||
DomainException: For API errors
|
||
"""
|
||
from datetime import timedelta
|
||
|
||
if from_date is None:
|
||
from_date = date.today() - timedelta(days=30)
|
||
if to_date is None:
|
||
to_date = date.today()
|
||
|
||
# API использует GET с query параметрами
|
||
params = {
|
||
"from": from_date.isoformat(),
|
||
"to": to_date.isoformat(),
|
||
"limit": str(limit),
|
||
"offset": str(offset),
|
||
"sortBy": "OPERATION_TIME",
|
||
"sortOrder": "DESC",
|
||
}
|
||
|
||
# Формируем query string
|
||
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||
response = await self.http.get(f"/incomes?{query}")
|
||
return response.json() # type: ignore[no-any-return]
|