""" 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]