Repository: python-telegram-bot/python-telegram-bot
Pull request: Refactor Module Structure and Tests for Star Payments Classes
<!--
Hey! You're PRing? Cool!
Please be sure to check out our contribution guide (https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.github/CONTRIBUTING.rst).
Especially, please have a look at the check list for PRs (https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.github/CONTRIBUTING.rst#check-list-for-prs). Feel free to copy (parts of) the checklist to the PR description to remind you or the maintainers of open points or if you have questions on anything.
-->

closes #4593

Problem statement:
Refactor `test_stars`
`test_stars` currently tries to avoid test duplication by abstracting a lot away in fixtures and parametrizing the tests. The intention behind this certainly was good!
Unfortunately, it leads to the tests being very convoluted, hard to navigate and it being easy to forget updating everything on changes.

I strongly suggest that we instead use the boilerplate approach. It's more manuel, sure, but GitHub Copilot will take care of most of that and the result will be that we now again what we're testing :D

Hints from issue discussion:

Commit message: Remove superfluous if-clause

Patch to review:
diff --git a/telegram/__init__.py b/telegram/__init__.py
index a827670d66b..df722a0980e 100644
--- a/telegram/__init__.py
+++ b/telegram/__init__.py
@@ -271,6 +271,17 @@
     "warnings",
 )
 
+from telegram._payment.stars.startransactions import StarTransaction, StarTransactions
+from telegram._payment.stars.transactionpartner import (
+    TransactionPartner,
+    TransactionPartnerAffiliateProgram,
+    TransactionPartnerFragment,
+    TransactionPartnerOther,
+    TransactionPartnerTelegramAds,
+    TransactionPartnerTelegramApi,
+    TransactionPartnerUser,
+)
+
 from . import _version, constants, error, helpers, request, warnings
 from ._birthdate import Birthdate
 from ._bot import Bot
@@ -470,21 +481,12 @@
 from ._payment.shippingaddress import ShippingAddress
 from ._payment.shippingoption import ShippingOption
 from ._payment.shippingquery import ShippingQuery
-from ._payment.stars import (
-    AffiliateInfo,
+from ._payment.stars.affiliateinfo import AffiliateInfo
+from ._payment.stars.revenuewithdrawelstate import (
     RevenueWithdrawalState,
     RevenueWithdrawalStateFailed,
     RevenueWithdrawalStatePending,
     RevenueWithdrawalStateSucceeded,
-    StarTransaction,
-    StarTransactions,
-    TransactionPartner,
-    TransactionPartnerAffiliateProgram,
-    TransactionPartnerFragment,
-    TransactionPartnerOther,
-    TransactionPartnerTelegramAds,
-    TransactionPartnerTelegramApi,
-    TransactionPartnerUser,
 )
 from ._payment.successfulpayment import SuccessfulPayment
 from ._poll import InputPollOption, Poll, PollAnswer, PollOption
diff --git a/telegram/_bot.py b/telegram/_bot.py
index dc1ef7ff43f..7b5b4b04f3d 100644
--- a/telegram/_bot.py
+++ b/telegram/_bot.py
@@ -81,7 +81,7 @@
 from telegram._menubutton import MenuButton
 from telegram._message import Message
 from telegram._messageid import MessageId
-from telegram._payment.stars import StarTransactions
+from telegram._payment.stars.startransactions import StarTransactions
 from telegram._poll import InputPollOption, Poll
 from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
 from telegram._reply import ReplyParameters
diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py
deleted file mode 100644
index 7ce4cf2de86..00000000000
--- a/telegram/_payment/stars.py
+++ /dev/null
@@ -1,795 +0,0 @@
-#!/usr/bin/env python
-#
-# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
-# Leandro Toledo de Souza <devs@python-telegram-bot.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Lesser Public License for more details.
-#
-# You should have received a copy of the GNU Lesser Public License
-# along with this program. If not, see [http://www.gnu.org/licenses/].
-# pylint: disable=redefined-builtin
-"""This module contains the classes for Telegram Stars transactions."""
-
-import datetime as dtm
-from collections.abc import Sequence
-from typing import TYPE_CHECKING, Final, Optional
-
-from telegram import constants
-from telegram._chat import Chat
-from telegram._gifts import Gift
-from telegram._paidmedia import PaidMedia
-from telegram._telegramobject import TelegramObject
-from telegram._user import User
-from telegram._utils import enum
-from telegram._utils.argumentparsing import parse_sequence_arg
-from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
-from telegram._utils.types import JSONDict
-
-if TYPE_CHECKING:
-    from telegram import Bot
-
-
-class RevenueWithdrawalState(TelegramObject):
-    """This object escribes the state of a revenue withdrawal operation. Currently, it can be one
-    of:
-
-    * :class:`telegram.RevenueWithdrawalStatePending`
-    * :class:`telegram.RevenueWithdrawalStateSucceeded`
-    * :class:`telegram.RevenueWithdrawalStateFailed`
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`type` is equal.
-
-    .. versionadded:: 21.4
-
-    Args:
-        type (:obj:`str`): The type of the state.
-
-    Attributes:
-        type (:obj:`str`): The type of the state.
-    """
-
-    __slots__ = ("type",)
-
-    PENDING: Final[str] = constants.RevenueWithdrawalStateType.PENDING
-    """:const:`telegram.constants.RevenueWithdrawalStateType.PENDING`"""
-    SUCCEEDED: Final[str] = constants.RevenueWithdrawalStateType.SUCCEEDED
-    """:const:`telegram.constants.RevenueWithdrawalStateType.SUCCEEDED`"""
-    FAILED: Final[str] = constants.RevenueWithdrawalStateType.FAILED
-    """:const:`telegram.constants.RevenueWithdrawalStateType.FAILED`"""
-
-    def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
-        super().__init__(api_kwargs=api_kwargs)
-        self.type: str = enum.get_member(constants.RevenueWithdrawalStateType, type, type)
-
-        self._id_attrs = (self.type,)
-        self._freeze()
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["RevenueWithdrawalState"]:
-        """Converts JSON data to the appropriate :class:`RevenueWithdrawalState` object, i.e. takes
-        care of selecting the correct subclass.
-
-        Args:
-            data (dict[:obj:`str`, ...]): The JSON data.
-            bot (:class:`telegram.Bot`): The bot associated with this object.
-
-        Returns:
-            The Telegram object.
-
-        """
-        data = cls._parse_data(data)
-
-        if not data:
-            return None
-
-        _class_mapping: dict[str, type[RevenueWithdrawalState]] = {
-            cls.PENDING: RevenueWithdrawalStatePending,
-            cls.SUCCEEDED: RevenueWithdrawalStateSucceeded,
-            cls.FAILED: RevenueWithdrawalStateFailed,
-        }
-
-        if cls is RevenueWithdrawalState and data.get("type") in _class_mapping:
-            return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
-
-        return super().de_json(data=data, bot=bot)
-
-
-class RevenueWithdrawalStatePending(RevenueWithdrawalState):
-    """The withdrawal is in progress.
-
-    .. versionadded:: 21.4
-
-    Attributes:
-        type (:obj:`str`): The type of the state, always
-            :tg-const:`telegram.RevenueWithdrawalState.PENDING`.
-    """
-
-    __slots__ = ()
-
-    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
-        super().__init__(type=RevenueWithdrawalState.PENDING, api_kwargs=api_kwargs)
-        self._freeze()
-
-
-class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState):
-    """The withdrawal succeeded.
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`date` are equal.
-
-    .. versionadded:: 21.4
-
-    Args:
-        date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
-        url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
-
-    Attributes:
-        type (:obj:`str`): The type of the state, always
-            :tg-const:`telegram.RevenueWithdrawalState.SUCCEEDED`.
-        date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
-        url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
-    """
-
-    __slots__ = ("date", "url")
-
-    def __init__(
-        self,
-        date: dtm.datetime,
-        url: str,
-        *,
-        api_kwargs: Optional[JSONDict] = None,
-    ) -> None:
-        super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs)
-
-        with self._unfrozen():
-            self.date: dtm.datetime = date
-            self.url: str = url
-            self._id_attrs = (
-                self.type,
-                self.date,
-            )
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["RevenueWithdrawalStateSucceeded"]:
-        """See :meth:`telegram.RevenueWithdrawalState.de_json`."""
-        data = cls._parse_data(data)
-
-        if not data:
-            return None
-
-        # Get the local timezone from the bot if it has defaults
-        loc_tzinfo = extract_tzinfo_from_defaults(bot)
-        data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
-
-        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
-
-
-class RevenueWithdrawalStateFailed(RevenueWithdrawalState):
-    """The withdrawal failed and the transaction was refunded.
-
-    .. versionadded:: 21.4
-
-    Attributes:
-        type (:obj:`str`): The type of the state, always
-            :tg-const:`telegram.RevenueWithdrawalState.FAILED`.
-    """
-
-    __slots__ = ()
-
-    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
-        super().__init__(type=RevenueWithdrawalState.FAILED, api_kwargs=api_kwargs)
-        self._freeze()
-
-
-class AffiliateInfo(TelegramObject):
-    """Contains information about the affiliate that received a commission via this transaction.
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`affiliate_user`, :attr:`affiliate_chat`,
-    :attr:`commission_per_mille`, :attr:`amount`, and :attr:`nanostar_amount` are equal.
-
-    .. versionadded:: 21.9
-
-    Args:
-        affiliate_user (:class:`telegram.User`, optional): The bot or the user that received an
-            affiliate commission if it was received by a bot or a user
-        affiliate_chat (:class:`telegram.Chat`, optional): The chat that received an affiliate
-            commission if it was received by a chat
-        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
-            for each 1000 Telegram Stars received by the bot from referred users
-        amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
-            transaction, rounded to 0; can be negative for refunds
-        nanostar_amount (:obj:`int`, optional): The number of
-            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
-            Stars received by the affiliate; from
-            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
-            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
-            can be negative for refunds
-
-    Attributes:
-        affiliate_user (:class:`telegram.User`): Optional. The bot or the user that received an
-            affiliate commission if it was received by a bot or a user
-        affiliate_chat (:class:`telegram.Chat`): Optional. The chat that received an affiliate
-            commission if it was received by a chat
-        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
-            for each 1000 Telegram Stars received by the bot from referred users
-        amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
-            transaction, rounded to 0; can be negative for refunds
-        nanostar_amount (:obj:`int`): Optional. The number of
-            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
-            Stars received by the affiliate; from
-            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
-            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
-            can be negative for refunds
-    """
-
-    __slots__ = (
-        "affiliate_chat",
-        "affiliate_user",
-        "amount",
-        "commission_per_mille",
-        "nanostar_amount",
-    )
-
-    def __init__(
-        self,
-        commission_per_mille: int,
-        amount: int,
-        affiliate_user: Optional["User"] = None,
-        affiliate_chat: Optional["Chat"] = None,
-        nanostar_amount: Optional[int] = None,
-        *,
-        api_kwargs: Optional[JSONDict] = None,
-    ) -> None:
-        super().__init__(api_kwargs=api_kwargs)
-        self.affiliate_user: Optional[User] = affiliate_user
-        self.affiliate_chat: Optional[Chat] = affiliate_chat
-        self.commission_per_mille: int = commission_per_mille
-        self.amount: int = amount
-        self.nanostar_amount: Optional[int] = nanostar_amount
-
-        self._id_attrs = (
-            self.affiliate_user,
-            self.affiliate_chat,
-            self.commission_per_mille,
-            self.amount,
-            self.nanostar_amount,
-        )
-        self._freeze()
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["AffiliateInfo"]:
-        """See :meth:`telegram.TelegramObject.de_json`."""
-        data = cls._parse_data(data)
-
-        if not data:
-            return None
-
-        data["affiliate_user"] = User.de_json(data.get("affiliate_user"), bot)
-        data["affiliate_chat"] = Chat.de_json(data.get("affiliate_chat"), bot)
-
-        return super().de_json(data=data, bot=bot)
-
-
-class TransactionPartner(TelegramObject):
-    """This object describes the source of a transaction, or its recipient for outgoing
-    transactions. Currently, it can be one of:
-
-    * :class:`TransactionPartnerUser`
-    * :class:`TransactionPartnerAffiliateProgram`
-    * :class:`TransactionPartnerFragment`
-    * :class:`TransactionPartnerTelegramAds`
-    * :class:`TransactionPartnerTelegramApi`
-    * :class:`TransactionPartnerOther`
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`type` is equal.
-
-    .. versionadded:: 21.4
-
-    Args:
-        type (:obj:`str`): The type of the transaction partner.
-
-    Attributes:
-        type (:obj:`str`): The type of the transaction partner.
-    """
-
-    __slots__ = ("type",)
-
-    AFFILIATE_PROGRAM: Final[str] = constants.TransactionPartnerType.AFFILIATE_PROGRAM
-    """:const:`telegram.constants.TransactionPartnerType.AFFILIATE_PROGRAM`
-
-    .. versionadded:: 21.9
-    """
-    FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
-    """:const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
-    OTHER: Final[str] = constants.TransactionPartnerType.OTHER
-    """:const:`telegram.constants.TransactionPartnerType.OTHER`"""
-    TELEGRAM_ADS: Final[str] = constants.TransactionPartnerType.TELEGRAM_ADS
-    """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_ADS`"""
-    TELEGRAM_API: Final[str] = constants.TransactionPartnerType.TELEGRAM_API
-    """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_API`"""
-    USER: Final[str] = constants.TransactionPartnerType.USER
-    """:const:`telegram.constants.TransactionPartnerType.USER`"""
-
-    def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
-        super().__init__(api_kwargs=api_kwargs)
-        self.type: str = enum.get_member(constants.TransactionPartnerType, type, type)
-
-        self._id_attrs = (self.type,)
-        self._freeze()
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["TransactionPartner"]:
-        """Converts JSON data to the appropriate :class:`TransactionPartner` object, i.e. takes
-        care of selecting the correct subclass.
-
-        Args:
-            data (dict[:obj:`str`, ...]): The JSON data.
-            bot (:class:`telegram.Bot`): The bot associated with this object.
-
-        Returns:
-            The Telegram object.
-
-        """
-        data = cls._parse_data(data)
-
-        if data is None:
-            return None
-
-        if not data and cls is TransactionPartner:
-            return None
-
-        _class_mapping: dict[str, type[TransactionPartner]] = {
-            cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
-            cls.FRAGMENT: TransactionPartnerFragment,
-            cls.USER: TransactionPartnerUser,
-            cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
-            cls.TELEGRAM_API: TransactionPartnerTelegramApi,
-            cls.OTHER: TransactionPartnerOther,
-        }
-
-        if cls is TransactionPartner and data.get("type") in _class_mapping:
-            return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
-
-        return super().de_json(data=data, bot=bot)
-
-
-class TransactionPartnerAffiliateProgram(TransactionPartner):
-    """Describes the affiliate program that issued the affiliate commission received via this
-    transaction.
-
-    This object is comparable in terms of equality. Two objects of this class are considered equal,
-    if their :attr:`commission_per_mille` are equal.
-
-    .. versionadded:: 21.9
-
-    Args:
-        sponsor_user (:class:`telegram.User`, optional): Information about the bot that sponsored
-            the affiliate program
-        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
-            each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
-
-    Attributes:
-        type (:obj:`str`): The type of the transaction partner,
-            always :tg-const:`telegram.TransactionPartner.AFFILIATE_PROGRAM`.
-        sponsor_user (:class:`telegram.User`): Optional. Information about the bot that sponsored
-            the affiliate program
-        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
-            each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
-    """
-
-    __slots__ = ("commission_per_mille", "sponsor_user")
-
-    def __init__(
-        self,
-        commission_per_mille: int,
-        sponsor_user: Optional["User"] = None,
-        *,
-        api_kwargs: Optional[JSONDict] = None,
-    ) -> None:
-        super().__init__(type=TransactionPartner.AFFILIATE_PROGRAM, api_kwargs=api_kwargs)
-
-        with self._unfrozen():
-            self.sponsor_user: Optional[User] = sponsor_user
-            self.commission_per_mille: int = commission_per_mille
-            self._id_attrs = (
-                self.type,
-                self.commission_per_mille,
-            )
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["TransactionPartnerAffiliateProgram"]:
-        """See :meth:`telegram.TransactionPartner.de_json`."""
-        data = cls._parse_data(data)
-
-        if not data:
-            return None
-
-        data["sponsor_user"] = User.de_json(data.get("sponsor_user"), bot)
-
-        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
-
-
-class TransactionPartnerFragment(TransactionPartner):
-    """Describes a withdrawal transaction with Fragment.
-
-    .. versionadded:: 21.4
-
-    Args:
-        withdrawal_state (:class:`telegram.RevenueWithdrawalState`, optional): State of the
-            transaction if the transaction is outgoing.
-
-    Attributes:
-        type (:obj:`str`): The type of the transaction partner,
-            always :tg-const:`telegram.TransactionPartner.FRAGMENT`.
-        withdrawal_state (:class:`telegram.RevenueWithdrawalState`): Optional. State of the
-            transaction if the transaction is outgoing.
-    """
-
-    __slots__ = ("withdrawal_state",)
-
-    def __init__(
-        self,
-        withdrawal_state: Optional["RevenueWithdrawalState"] = None,
-        *,
-        api_kwargs: Optional[JSONDict] = None,
-    ) -> None:
-        super().__init__(type=TransactionPartner.FRAGMENT, api_kwargs=api_kwargs)
-
-        with self._unfrozen():
-            self.withdrawal_state: Optional[RevenueWithdrawalState] = withdrawal_state
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["TransactionPartnerFragment"]:
-        """See :meth:`telegram.TransactionPartner.de_json`."""
-        data = cls._parse_data(data)
-
-        if not data:
-            return None
-
-        data["withdrawal_state"] = RevenueWithdrawalState.de_json(
-            data.get("withdrawal_state"), bot
-        )
-
-        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
-
-
-class TransactionPartnerUser(TransactionPartner):
-    """Describes a transaction with a user.
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`user` are equal.
-
-    .. versionadded:: 21.4
-
-    Args:
-        user (:class:`telegram.User`): Information about the user.
-        affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that
-            received a commission via this transaction
-
-            .. versionadded:: 21.9
-        invoice_payload (:obj:`str`, optional): Bot-specified invoice payload.
-        subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid
-            subscription
-
-            .. versionadded:: 21.8
-        paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid
-            media bought by the user.
-
-            .. versionadded:: 21.5
-        paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload.
-
-            .. versionadded:: 21.6
-        gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot
-
-            .. versionadded:: 21.8
-
-    Attributes:
-        type (:obj:`str`): The type of the transaction partner,
-            always :tg-const:`telegram.TransactionPartner.USER`.
-        user (:class:`telegram.User`): Information about the user.
-        affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that
-            received a commission via this transaction
-
-            .. versionadded:: 21.9
-        invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload.
-        subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid
-            subscription
-
-            .. versionadded:: 21.8
-        paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid
-            media bought by the user.
-
-            .. versionadded:: 21.5
-        paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload.
-
-            .. versionadded:: 21.6
-        gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot
-
-            .. versionadded:: 21.8
-
-    """
-
-    __slots__ = (
-        "affiliate",
-        "gift",
-        "invoice_payload",
-        "paid_media",
-        "paid_media_payload",
-        "subscription_period",
-        "user",
-    )
-
-    def __init__(
-        self,
-        user: "User",
-        invoice_payload: Optional[str] = None,
-        paid_media: Optional[Sequence[PaidMedia]] = None,
-        paid_media_payload: Optional[str] = None,
-        subscription_period: Optional[dtm.timedelta] = None,
-        gift: Optional[Gift] = None,
-        affiliate: Optional[AffiliateInfo] = None,
-        *,
-        api_kwargs: Optional[JSONDict] = None,
-    ) -> None:
-        super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs)
-
-        with self._unfrozen():
-            self.user: User = user
-            self.affiliate: Optional[AffiliateInfo] = affiliate
-            self.invoice_payload: Optional[str] = invoice_payload
-            self.paid_media: Optional[tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media)
-            self.paid_media_payload: Optional[str] = paid_media_payload
-            self.subscription_period: Optional[dtm.timedelta] = subscription_period
-            self.gift: Optional[Gift] = gift
-
-            self._id_attrs = (
-                self.type,
-                self.user,
-            )
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["TransactionPartnerUser"]:
-        """See :meth:`telegram.TransactionPartner.de_json`."""
-        data = cls._parse_data(data)
-
-        if not data:
-            return None
-
-        data["user"] = User.de_json(data.get("user"), bot)
-        data["affiliate"] = AffiliateInfo.de_json(data.get("affiliate"), bot)
-        data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot)
-        data["subscription_period"] = (
-            dtm.timedelta(seconds=sp)
-            if (sp := data.get("subscription_period")) is not None
-            else None
-        )
-        data["gift"] = Gift.de_json(data.get("gift"), bot)
-
-        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
-
-
-class TransactionPartnerOther(TransactionPartner):
-    """Describes a transaction with an unknown partner.
-
-    .. versionadded:: 21.4
-
-    Attributes:
-        type (:obj:`str`): The type of the transaction partner,
-            always :tg-const:`telegram.TransactionPartner.OTHER`.
-    """
-
-    __slots__ = ()
-
-    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
-        super().__init__(type=TransactionPartner.OTHER, api_kwargs=api_kwargs)
-        self._freeze()
-
-
-class TransactionPartnerTelegramAds(TransactionPartner):
-    """Describes a withdrawal transaction to the Telegram Ads platform.
-
-    .. versionadded:: 21.4
-
-    Attributes:
-        type (:obj:`str`): The type of the transaction partner,
-            always :tg-const:`telegram.TransactionPartner.TELEGRAM_ADS`.
-    """
-
-    __slots__ = ()
-
-    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
-        super().__init__(type=TransactionPartner.TELEGRAM_ADS, api_kwargs=api_kwargs)
-        self._freeze()
-
-
-class TransactionPartnerTelegramApi(TransactionPartner):
-    """Describes a transaction with payment for
-    `paid broadcasting <https://core.telegram.org/bots/api#paid-broadcasts>`_.
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`request_count` is equal.
-
-    .. versionadded:: 21.7
-
-    Args:
-        request_count (:obj:`int`): The number of successful requests that exceeded regular limits
-            and were therefore billed.
-
-    Attributes:
-        type (:obj:`str`): The type of the transaction partner,
-            always :tg-const:`telegram.TransactionPartner.TELEGRAM_API`.
-        request_count (:obj:`int`): The number of successful requests that exceeded regular limits
-            and were therefore billed.
-    """
-
-    __slots__ = ("request_count",)
-
-    def __init__(self, request_count: int, *, api_kwargs: Optional[JSONDict] = None) -> None:
-        super().__init__(type=TransactionPartner.TELEGRAM_API, api_kwargs=api_kwargs)
-        with self._unfrozen():
-            self.request_count: int = request_count
-            self._id_attrs = (self.request_count,)
-
-
-class StarTransaction(TelegramObject):
-    """Describes a Telegram Star transaction.
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`id`, :attr:`source`, and :attr:`receiver` are equal.
-
-    .. versionadded:: 21.4
-
-    Args:
-        id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
-            of the original transaction for refund transactions.
-            Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
-            successful incoming payments from users.
-        amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
-        nanostar_amount (:obj:`int`, optional): The number of
-            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
-            Stars transferred by the transaction; from 0 to
-            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
-
-            .. versionadded:: 21.9
-        date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
-        source (:class:`telegram.TransactionPartner`, optional): Source of an incoming transaction
-            (e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
-            Only for incoming transactions.
-        receiver (:class:`telegram.TransactionPartner`, optional): Receiver of an outgoing
-            transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
-            outgoing transactions.
-
-    Attributes:
-        id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
-            of the original transaction for refund transactions.
-            Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
-            successful incoming payments from users.
-        amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
-        nanostar_amount (:obj:`int`): Optional. The number of
-            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
-            Stars transferred by the transaction; from 0 to
-            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
-
-            .. versionadded:: 21.9
-        date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
-        source (:class:`telegram.TransactionPartner`): Optional. Source of an incoming transaction
-            (e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
-            Only for incoming transactions.
-        receiver (:class:`telegram.TransactionPartner`): Optional. Receiver of an outgoing
-            transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
-            outgoing transactions.
-    """
-
-    __slots__ = ("amount", "date", "id", "nanostar_amount", "receiver", "source")
-
-    def __init__(
-        self,
-        id: str,
-        amount: int,
-        date: dtm.datetime,
-        source: Optional[TransactionPartner] = None,
-        receiver: Optional[TransactionPartner] = None,
-        nanostar_amount: Optional[int] = None,
-        *,
-        api_kwargs: Optional[JSONDict] = None,
-    ) -> None:
-        super().__init__(api_kwargs=api_kwargs)
-        self.id: str = id
-        self.amount: int = amount
-        self.date: dtm.datetime = date
-        self.source: Optional[TransactionPartner] = source
-        self.receiver: Optional[TransactionPartner] = receiver
-        self.nanostar_amount: Optional[int] = nanostar_amount
-
-        self._id_attrs = (
-            self.id,
-            self.source,
-            self.receiver,
-        )
-        self._freeze()
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["StarTransaction"]:
-        """See :meth:`telegram.TelegramObject.de_json`."""
-        data = cls._parse_data(data)
-
-        if not data:
-            return None
-
-        # Get the local timezone from the bot if it has defaults
-        loc_tzinfo = extract_tzinfo_from_defaults(bot)
-        data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
-
-        data["source"] = TransactionPartner.de_json(data.get("source"), bot)
-        data["receiver"] = TransactionPartner.de_json(data.get("receiver"), bot)
-
-        return super().de_json(data=data, bot=bot)
-
-
-class StarTransactions(TelegramObject):
-    """
-    Contains a list of Telegram Star transactions.
-
-    Objects of this class are comparable in terms of equality. Two objects of this class are
-    considered equal, if their :attr:`transactions` are equal.
-
-    .. versionadded:: 21.4
-
-    Args:
-        transactions (Sequence[:class:`telegram.StarTransaction`]): The list of transactions.
-
-    Attributes:
-        transactions (tuple[:class:`telegram.StarTransaction`]): The list of transactions.
-    """
-
-    __slots__ = ("transactions",)
-
-    def __init__(
-        self, transactions: Sequence[StarTransaction], *, api_kwargs: Optional[JSONDict] = None
-    ):
-        super().__init__(api_kwargs=api_kwargs)
-        self.transactions: tuple[StarTransaction, ...] = parse_sequence_arg(transactions)
-
-        self._id_attrs = (self.transactions,)
-        self._freeze()
-
-    @classmethod
-    def de_json(
-        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
-    ) -> Optional["StarTransactions"]:
-        """See :meth:`telegram.TelegramObject.de_json`."""
-        data = cls._parse_data(data)
-
-        if data is None:
-            return None
-
-        data["transactions"] = StarTransaction.de_list(data.get("transactions"), bot)
-        return super().de_json(data=data, bot=bot)
diff --git a/telegram/_payment/stars/__init__.py b/telegram/_payment/stars/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/telegram/_payment/stars/affiliateinfo.py b/telegram/_payment/stars/affiliateinfo.py
new file mode 100644
index 00000000000..795b243d2c8
--- /dev/null
+++ b/telegram/_payment/stars/affiliateinfo.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=redefined-builtin
+"""This module contains the classes for Telegram Stars affiliates."""
+from typing import TYPE_CHECKING, Optional
+
+from telegram._chat import Chat
+from telegram._telegramobject import TelegramObject
+from telegram._user import User
+from telegram._utils.types import JSONDict
+
+if TYPE_CHECKING:
+    from telegram import Bot
+
+
+class AffiliateInfo(TelegramObject):
+    """Contains information about the affiliate that received a commission via this transaction.
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`affiliate_user`, :attr:`affiliate_chat`,
+    :attr:`commission_per_mille`, :attr:`amount`, and :attr:`nanostar_amount` are equal.
+
+    .. versionadded:: 21.9
+
+    Args:
+        affiliate_user (:class:`telegram.User`, optional): The bot or the user that received an
+            affiliate commission if it was received by a bot or a user
+        affiliate_chat (:class:`telegram.Chat`, optional): The chat that received an affiliate
+            commission if it was received by a chat
+        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
+            for each 1000 Telegram Stars received by the bot from referred users
+        amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
+            transaction, rounded to 0; can be negative for refunds
+        nanostar_amount (:obj:`int`, optional): The number of
+            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
+            Stars received by the affiliate; from
+            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
+            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
+            can be negative for refunds
+
+    Attributes:
+        affiliate_user (:class:`telegram.User`): Optional. The bot or the user that received an
+            affiliate commission if it was received by a bot or a user
+        affiliate_chat (:class:`telegram.Chat`): Optional. The chat that received an affiliate
+            commission if it was received by a chat
+        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the affiliate
+            for each 1000 Telegram Stars received by the bot from referred users
+        amount (:obj:`int`): Integer amount of Telegram Stars received by the affiliate from the
+            transaction, rounded to 0; can be negative for refunds
+        nanostar_amount (:obj:`int`): Optional. The number of
+            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
+            Stars received by the affiliate; from
+            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT` to
+            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`;
+            can be negative for refunds
+    """
+
+    __slots__ = (
+        "affiliate_chat",
+        "affiliate_user",
+        "amount",
+        "commission_per_mille",
+        "nanostar_amount",
+    )
+
+    def __init__(
+        self,
+        commission_per_mille: int,
+        amount: int,
+        affiliate_user: Optional["User"] = None,
+        affiliate_chat: Optional["Chat"] = None,
+        nanostar_amount: Optional[int] = None,
+        *,
+        api_kwargs: Optional[JSONDict] = None,
+    ) -> None:
+        super().__init__(api_kwargs=api_kwargs)
+        self.affiliate_user: Optional[User] = affiliate_user
+        self.affiliate_chat: Optional[Chat] = affiliate_chat
+        self.commission_per_mille: int = commission_per_mille
+        self.amount: int = amount
+        self.nanostar_amount: Optional[int] = nanostar_amount
+
+        self._id_attrs = (
+            self.affiliate_user,
+            self.affiliate_chat,
+            self.commission_per_mille,
+            self.amount,
+            self.nanostar_amount,
+        )
+        self._freeze()
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["AffiliateInfo"]:
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+
+        if not data:
+            return None
+
+        data["affiliate_user"] = User.de_json(data.get("affiliate_user"), bot)
+        data["affiliate_chat"] = Chat.de_json(data.get("affiliate_chat"), bot)
+
+        return super().de_json(data=data, bot=bot)
diff --git a/telegram/_payment/stars/revenuewithdrawelstate.py b/telegram/_payment/stars/revenuewithdrawelstate.py
new file mode 100644
index 00000000000..f56b2d47b63
--- /dev/null
+++ b/telegram/_payment/stars/revenuewithdrawelstate.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=redefined-builtin
+"""This module contains the classes for Telegram Stars Revenue Withdrawals."""
+import datetime as dtm
+from typing import TYPE_CHECKING, Final, Optional
+
+from telegram import constants
+from telegram._telegramobject import TelegramObject
+from telegram._utils import enum
+from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
+from telegram._utils.types import JSONDict
+
+if TYPE_CHECKING:
+    from telegram import Bot
+
+
+class RevenueWithdrawalState(TelegramObject):
+    """This object escribes the state of a revenue withdrawal operation. Currently, it can be one
+    of:
+
+    * :class:`telegram.RevenueWithdrawalStatePending`
+    * :class:`telegram.RevenueWithdrawalStateSucceeded`
+    * :class:`telegram.RevenueWithdrawalStateFailed`
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`type` is equal.
+
+    .. versionadded:: 21.4
+
+    Args:
+        type (:obj:`str`): The type of the state.
+
+    Attributes:
+        type (:obj:`str`): The type of the state.
+    """
+
+    __slots__ = ("type",)
+
+    PENDING: Final[str] = constants.RevenueWithdrawalStateType.PENDING
+    """:const:`telegram.constants.RevenueWithdrawalStateType.PENDING`"""
+    SUCCEEDED: Final[str] = constants.RevenueWithdrawalStateType.SUCCEEDED
+    """:const:`telegram.constants.RevenueWithdrawalStateType.SUCCEEDED`"""
+    FAILED: Final[str] = constants.RevenueWithdrawalStateType.FAILED
+    """:const:`telegram.constants.RevenueWithdrawalStateType.FAILED`"""
+
+    def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
+        super().__init__(api_kwargs=api_kwargs)
+        self.type: str = enum.get_member(constants.RevenueWithdrawalStateType, type, type)
+
+        self._id_attrs = (self.type,)
+        self._freeze()
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["RevenueWithdrawalState"]:
+        """Converts JSON data to the appropriate :class:`RevenueWithdrawalState` object, i.e. takes
+        care of selecting the correct subclass.
+
+        Args:
+            data (dict[:obj:`str`, ...]): The JSON data.
+            bot (:class:`telegram.Bot`): The bot associated with this object.
+
+        Returns:
+            The Telegram object.
+
+        """
+        data = cls._parse_data(data)
+
+        if (cls is RevenueWithdrawalState and not data) or data is None:
+            return None
+
+        _class_mapping: dict[str, type[RevenueWithdrawalState]] = {
+            cls.PENDING: RevenueWithdrawalStatePending,
+            cls.SUCCEEDED: RevenueWithdrawalStateSucceeded,
+            cls.FAILED: RevenueWithdrawalStateFailed,
+        }
+
+        if cls is RevenueWithdrawalState and data.get("type") in _class_mapping:
+            return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
+
+        return super().de_json(data=data, bot=bot)
+
+
+class RevenueWithdrawalStatePending(RevenueWithdrawalState):
+    """The withdrawal is in progress.
+
+    .. versionadded:: 21.4
+
+    Attributes:
+        type (:obj:`str`): The type of the state, always
+            :tg-const:`telegram.RevenueWithdrawalState.PENDING`.
+    """
+
+    __slots__ = ()
+
+    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
+        super().__init__(type=RevenueWithdrawalState.PENDING, api_kwargs=api_kwargs)
+        self._freeze()
+
+
+class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState):
+    """The withdrawal succeeded.
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`date` are equal.
+
+    .. versionadded:: 21.4
+
+    Args:
+        date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
+        url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
+
+    Attributes:
+        type (:obj:`str`): The type of the state, always
+            :tg-const:`telegram.RevenueWithdrawalState.SUCCEEDED`.
+        date (:obj:`datetime.datetime`): Date the withdrawal was completed as a datetime object.
+        url (:obj:`str`): An HTTPS URL that can be used to see transaction details.
+    """
+
+    __slots__ = ("date", "url")
+
+    def __init__(
+        self,
+        date: dtm.datetime,
+        url: str,
+        *,
+        api_kwargs: Optional[JSONDict] = None,
+    ) -> None:
+        super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs)
+
+        with self._unfrozen():
+            self.date: dtm.datetime = date
+            self.url: str = url
+            self._id_attrs = (
+                self.type,
+                self.date,
+            )
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["RevenueWithdrawalStateSucceeded"]:
+        """See :meth:`telegram.RevenueWithdrawalState.de_json`."""
+        data = cls._parse_data(data)
+
+        if not data:
+            return None
+
+        # Get the local timezone from the bot if it has defaults
+        loc_tzinfo = extract_tzinfo_from_defaults(bot)
+        data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
+
+        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
+
+
+class RevenueWithdrawalStateFailed(RevenueWithdrawalState):
+    """The withdrawal failed and the transaction was refunded.
+
+    .. versionadded:: 21.4
+
+    Attributes:
+        type (:obj:`str`): The type of the state, always
+            :tg-const:`telegram.RevenueWithdrawalState.FAILED`.
+    """
+
+    __slots__ = ()
+
+    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
+        super().__init__(type=RevenueWithdrawalState.FAILED, api_kwargs=api_kwargs)
+        self._freeze()
diff --git a/telegram/_payment/stars/startransactions.py b/telegram/_payment/stars/startransactions.py
new file mode 100644
index 00000000000..3d2c4309477
--- /dev/null
+++ b/telegram/_payment/stars/startransactions.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=redefined-builtin
+"""This module contains the classes for Telegram Stars transactions."""
+
+import datetime as dtm
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
+
+from telegram._telegramobject import TelegramObject
+from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
+from telegram._utils.types import JSONDict
+
+from .transactionpartner import TransactionPartner
+
+if TYPE_CHECKING:
+    from telegram import Bot
+
+
+class StarTransaction(TelegramObject):
+    """Describes a Telegram Star transaction.
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`id`, :attr:`source`, and :attr:`receiver` are equal.
+
+    .. versionadded:: 21.4
+
+    Args:
+        id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
+            of the original transaction for refund transactions.
+            Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
+            successful incoming payments from users.
+        amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
+        nanostar_amount (:obj:`int`, optional): The number of
+            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
+            Stars transferred by the transaction; from 0 to
+            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
+
+            .. versionadded:: 21.9
+        date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
+        source (:class:`telegram.TransactionPartner`, optional): Source of an incoming transaction
+            (e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
+            Only for incoming transactions.
+        receiver (:class:`telegram.TransactionPartner`, optional): Receiver of an outgoing
+            transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
+            outgoing transactions.
+
+    Attributes:
+        id (:obj:`str`): Unique identifier of the transaction. Coincides with the identifer
+            of the original transaction for refund transactions.
+            Coincides with :attr:`SuccessfulPayment.telegram_payment_charge_id` for
+            successful incoming payments from users.
+        amount (:obj:`int`): Integer amount of Telegram Stars transferred by the transaction.
+        nanostar_amount (:obj:`int`): Optional. The number of
+            :tg-const:`~telegram.constants.StarTransactions.NANOSTAR_VALUE` shares of Telegram
+            Stars transferred by the transaction; from 0 to
+            :tg-const:`~telegram.constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT`
+
+            .. versionadded:: 21.9
+        date (:obj:`datetime.datetime`): Date the transaction was created as a datetime object.
+        source (:class:`telegram.TransactionPartner`): Optional. Source of an incoming transaction
+            (e.g., a user purchasing goods or services, Fragment refunding a failed withdrawal).
+            Only for incoming transactions.
+        receiver (:class:`telegram.TransactionPartner`): Optional. Receiver of an outgoing
+            transaction (e.g., a user for a purchase refund, Fragment for a withdrawal). Only for
+            outgoing transactions.
+    """
+
+    __slots__ = ("amount", "date", "id", "nanostar_amount", "receiver", "source")
+
+    def __init__(
+        self,
+        id: str,
+        amount: int,
+        date: dtm.datetime,
+        source: Optional[TransactionPartner] = None,
+        receiver: Optional[TransactionPartner] = None,
+        nanostar_amount: Optional[int] = None,
+        *,
+        api_kwargs: Optional[JSONDict] = None,
+    ) -> None:
+        super().__init__(api_kwargs=api_kwargs)
+        self.id: str = id
+        self.amount: int = amount
+        self.date: dtm.datetime = date
+        self.source: Optional[TransactionPartner] = source
+        self.receiver: Optional[TransactionPartner] = receiver
+        self.nanostar_amount: Optional[int] = nanostar_amount
+
+        self._id_attrs = (
+            self.id,
+            self.source,
+            self.receiver,
+        )
+        self._freeze()
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["StarTransaction"]:
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+
+        if not data:
+            return None
+
+        # Get the local timezone from the bot if it has defaults
+        loc_tzinfo = extract_tzinfo_from_defaults(bot)
+        data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
+
+        data["source"] = TransactionPartner.de_json(data.get("source"), bot)
+        data["receiver"] = TransactionPartner.de_json(data.get("receiver"), bot)
+
+        return super().de_json(data=data, bot=bot)
+
+
+class StarTransactions(TelegramObject):
+    """
+    Contains a list of Telegram Star transactions.
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`transactions` are equal.
+
+    .. versionadded:: 21.4
+
+    Args:
+        transactions (Sequence[:class:`telegram.StarTransaction`]): The list of transactions.
+
+    Attributes:
+        transactions (tuple[:class:`telegram.StarTransaction`]): The list of transactions.
+    """
+
+    __slots__ = ("transactions",)
+
+    def __init__(
+        self, transactions: Sequence[StarTransaction], *, api_kwargs: Optional[JSONDict] = None
+    ):
+        super().__init__(api_kwargs=api_kwargs)
+        self.transactions: tuple[StarTransaction, ...] = parse_sequence_arg(transactions)
+
+        self._id_attrs = (self.transactions,)
+        self._freeze()
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["StarTransactions"]:
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+
+        if data is None:
+            return None
+
+        data["transactions"] = StarTransaction.de_list(data.get("transactions"), bot)
+        return super().de_json(data=data, bot=bot)
diff --git a/telegram/_payment/stars/transactionpartner.py b/telegram/_payment/stars/transactionpartner.py
new file mode 100644
index 00000000000..082e5731830
--- /dev/null
+++ b/telegram/_payment/stars/transactionpartner.py
@@ -0,0 +1,405 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=redefined-builtin
+"""This module contains the classes for Telegram Stars transaction partners."""
+import datetime as dtm
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Final, Optional
+
+from telegram import constants
+from telegram._gifts import Gift
+from telegram._paidmedia import PaidMedia
+from telegram._telegramobject import TelegramObject
+from telegram._user import User
+from telegram._utils import enum
+from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.types import JSONDict
+
+from .affiliateinfo import AffiliateInfo
+from .revenuewithdrawelstate import RevenueWithdrawalState
+
+if TYPE_CHECKING:
+    from telegram import Bot
+
+
+class TransactionPartner(TelegramObject):
+    """This object describes the source of a transaction, or its recipient for outgoing
+    transactions. Currently, it can be one of:
+
+    * :class:`TransactionPartnerUser`
+    * :class:`TransactionPartnerAffiliateProgram`
+    * :class:`TransactionPartnerFragment`
+    * :class:`TransactionPartnerTelegramAds`
+    * :class:`TransactionPartnerTelegramApi`
+    * :class:`TransactionPartnerOther`
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`type` is equal.
+
+    .. versionadded:: 21.4
+
+    Args:
+        type (:obj:`str`): The type of the transaction partner.
+
+    Attributes:
+        type (:obj:`str`): The type of the transaction partner.
+    """
+
+    __slots__ = ("type",)
+
+    AFFILIATE_PROGRAM: Final[str] = constants.TransactionPartnerType.AFFILIATE_PROGRAM
+    """:const:`telegram.constants.TransactionPartnerType.AFFILIATE_PROGRAM`
+
+    .. versionadded:: 21.9
+    """
+    FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
+    """:const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
+    OTHER: Final[str] = constants.TransactionPartnerType.OTHER
+    """:const:`telegram.constants.TransactionPartnerType.OTHER`"""
+    TELEGRAM_ADS: Final[str] = constants.TransactionPartnerType.TELEGRAM_ADS
+    """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_ADS`"""
+    TELEGRAM_API: Final[str] = constants.TransactionPartnerType.TELEGRAM_API
+    """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_API`"""
+    USER: Final[str] = constants.TransactionPartnerType.USER
+    """:const:`telegram.constants.TransactionPartnerType.USER`"""
+
+    def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None:
+        super().__init__(api_kwargs=api_kwargs)
+        self.type: str = enum.get_member(constants.TransactionPartnerType, type, type)
+
+        self._id_attrs = (self.type,)
+        self._freeze()
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["TransactionPartner"]:
+        """Converts JSON data to the appropriate :class:`TransactionPartner` object, i.e. takes
+        care of selecting the correct subclass.
+
+        Args:
+            data (dict[:obj:`str`, ...]): The JSON data.
+            bot (:class:`telegram.Bot`): The bot associated with this object.
+
+        Returns:
+            The Telegram object.
+
+        """
+        data = cls._parse_data(data)
+
+        if (cls is TransactionPartner and not data) or data is None:
+            return None
+
+        _class_mapping: dict[str, type[TransactionPartner]] = {
+            cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
+            cls.FRAGMENT: TransactionPartnerFragment,
+            cls.USER: TransactionPartnerUser,
+            cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
+            cls.TELEGRAM_API: TransactionPartnerTelegramApi,
+            cls.OTHER: TransactionPartnerOther,
+        }
+
+        if cls is TransactionPartner and data.get("type") in _class_mapping:
+            return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
+
+        return super().de_json(data=data, bot=bot)
+
+
+class TransactionPartnerAffiliateProgram(TransactionPartner):
+    """Describes the affiliate program that issued the affiliate commission received via this
+    transaction.
+
+    This object is comparable in terms of equality. Two objects of this class are considered equal,
+    if their :attr:`commission_per_mille` are equal.
+
+    .. versionadded:: 21.9
+
+    Args:
+        sponsor_user (:class:`telegram.User`, optional): Information about the bot that sponsored
+            the affiliate program
+        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
+            each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
+
+    Attributes:
+        type (:obj:`str`): The type of the transaction partner,
+            always :tg-const:`telegram.TransactionPartner.AFFILIATE_PROGRAM`.
+        sponsor_user (:class:`telegram.User`): Optional. Information about the bot that sponsored
+            the affiliate program
+        commission_per_mille (:obj:`int`): The number of Telegram Stars received by the bot for
+            each 1000 Telegram Stars received by the affiliate program sponsor from referred users.
+    """
+
+    __slots__ = ("commission_per_mille", "sponsor_user")
+
+    def __init__(
+        self,
+        commission_per_mille: int,
+        sponsor_user: Optional["User"] = None,
+        *,
+        api_kwargs: Optional[JSONDict] = None,
+    ) -> None:
+        super().__init__(type=TransactionPartner.AFFILIATE_PROGRAM, api_kwargs=api_kwargs)
+
+        with self._unfrozen():
+            self.sponsor_user: Optional[User] = sponsor_user
+            self.commission_per_mille: int = commission_per_mille
+            self._id_attrs = (
+                self.type,
+                self.commission_per_mille,
+            )
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["TransactionPartnerAffiliateProgram"]:
+        """See :meth:`telegram.TransactionPartner.de_json`."""
+        data = cls._parse_data(data)
+
+        if not data:
+            return None
+
+        data["sponsor_user"] = User.de_json(data.get("sponsor_user"), bot)
+
+        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
+
+
+class TransactionPartnerFragment(TransactionPartner):
+    """Describes a withdrawal transaction with Fragment.
+
+    .. versionadded:: 21.4
+
+    Args:
+        withdrawal_state (:class:`telegram.RevenueWithdrawalState`, optional): State of the
+            transaction if the transaction is outgoing.
+
+    Attributes:
+        type (:obj:`str`): The type of the transaction partner,
+            always :tg-const:`telegram.TransactionPartner.FRAGMENT`.
+        withdrawal_state (:class:`telegram.RevenueWithdrawalState`): Optional. State of the
+            transaction if the transaction is outgoing.
+    """
+
+    __slots__ = ("withdrawal_state",)
+
+    def __init__(
+        self,
+        withdrawal_state: Optional["RevenueWithdrawalState"] = None,
+        *,
+        api_kwargs: Optional[JSONDict] = None,
+    ) -> None:
+        super().__init__(type=TransactionPartner.FRAGMENT, api_kwargs=api_kwargs)
+
+        with self._unfrozen():
+            self.withdrawal_state: Optional[RevenueWithdrawalState] = withdrawal_state
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["TransactionPartnerFragment"]:
+        """See :meth:`telegram.TransactionPartner.de_json`."""
+        data = cls._parse_data(data)
+
+        if data is None:
+            return None
+
+        data["withdrawal_state"] = RevenueWithdrawalState.de_json(
+            data.get("withdrawal_state"), bot
+        )
+
+        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
+
+
+class TransactionPartnerUser(TransactionPartner):
+    """Describes a transaction with a user.
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`user` are equal.
+
+    .. versionadded:: 21.4
+
+    Args:
+        user (:class:`telegram.User`): Information about the user.
+        affiliate (:class:`telegram.AffiliateInfo`, optional): Information about the affiliate that
+            received a commission via this transaction
+
+            .. versionadded:: 21.9
+        invoice_payload (:obj:`str`, optional): Bot-specified invoice payload.
+        subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid
+            subscription
+
+            .. versionadded:: 21.8
+        paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid
+            media bought by the user.
+
+            .. versionadded:: 21.5
+        paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload.
+
+            .. versionadded:: 21.6
+        gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot
+
+            .. versionadded:: 21.8
+
+    Attributes:
+        type (:obj:`str`): The type of the transaction partner,
+            always :tg-const:`telegram.TransactionPartner.USER`.
+        user (:class:`telegram.User`): Information about the user.
+        affiliate (:class:`telegram.AffiliateInfo`): Optional. Information about the affiliate that
+            received a commission via this transaction
+
+            .. versionadded:: 21.9
+        invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload.
+        subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid
+            subscription
+
+            .. versionadded:: 21.8
+        paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid
+            media bought by the user.
+
+            .. versionadded:: 21.5
+        paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload.
+
+            .. versionadded:: 21.6
+        gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot
+
+            .. versionadded:: 21.8
+
+    """
+
+    __slots__ = (
+        "affiliate",
+        "gift",
+        "invoice_payload",
+        "paid_media",
+        "paid_media_payload",
+        "subscription_period",
+        "user",
+    )
+
+    def __init__(
+        self,
+        user: "User",
+        invoice_payload: Optional[str] = None,
+        paid_media: Optional[Sequence[PaidMedia]] = None,
+        paid_media_payload: Optional[str] = None,
+        subscription_period: Optional[dtm.timedelta] = None,
+        gift: Optional[Gift] = None,
+        affiliate: Optional[AffiliateInfo] = None,
+        *,
+        api_kwargs: Optional[JSONDict] = None,
+    ) -> None:
+        super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs)
+
+        with self._unfrozen():
+            self.user: User = user
+            self.affiliate: Optional[AffiliateInfo] = affiliate
+            self.invoice_payload: Optional[str] = invoice_payload
+            self.paid_media: Optional[tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media)
+            self.paid_media_payload: Optional[str] = paid_media_payload
+            self.subscription_period: Optional[dtm.timedelta] = subscription_period
+            self.gift: Optional[Gift] = gift
+
+            self._id_attrs = (
+                self.type,
+                self.user,
+            )
+
+    @classmethod
+    def de_json(
+        cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
+    ) -> Optional["TransactionPartnerUser"]:
+        """See :meth:`telegram.TransactionPartner.de_json`."""
+        data = cls._parse_data(data)
+
+        if not data:
+            return None
+
+        data["user"] = User.de_json(data.get("user"), bot)
+        data["affiliate"] = AffiliateInfo.de_json(data.get("affiliate"), bot)
+        data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot)
+        data["subscription_period"] = (
+            dtm.timedelta(seconds=sp)
+            if (sp := data.get("subscription_period")) is not None
+            else None
+        )
+        data["gift"] = Gift.de_json(data.get("gift"), bot)
+
+        return super().de_json(data=data, bot=bot)  # type: ignore[return-value]
+
+
+class TransactionPartnerOther(TransactionPartner):
+    """Describes a transaction with an unknown partner.
+
+    .. versionadded:: 21.4
+
+    Attributes:
+        type (:obj:`str`): The type of the transaction partner,
+            always :tg-const:`telegram.TransactionPartner.OTHER`.
+    """
+
+    __slots__ = ()
+
+    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
+        super().__init__(type=TransactionPartner.OTHER, api_kwargs=api_kwargs)
+        self._freeze()
+
+
+class TransactionPartnerTelegramAds(TransactionPartner):
+    """Describes a withdrawal transaction to the Telegram Ads platform.
+
+    .. versionadded:: 21.4
+
+    Attributes:
+        type (:obj:`str`): The type of the transaction partner,
+            always :tg-const:`telegram.TransactionPartner.TELEGRAM_ADS`.
+    """
+
+    __slots__ = ()
+
+    def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None:
+        super().__init__(type=TransactionPartner.TELEGRAM_ADS, api_kwargs=api_kwargs)
+        self._freeze()
+
+
+class TransactionPartnerTelegramApi(TransactionPartner):
+    """Describes a transaction with payment for
+    `paid broadcasting <https://core.telegram.org/bots/api#paid-broadcasts>`_.
+
+    Objects of this class are comparable in terms of equality. Two objects of this class are
+    considered equal, if their :attr:`request_count` is equal.
+
+    .. versionadded:: 21.7
+
+    Args:
+        request_count (:obj:`int`): The number of successful requests that exceeded regular limits
+            and were therefore billed.
+
+    Attributes:
+        type (:obj:`str`): The type of the transaction partner,
+            always :tg-const:`telegram.TransactionPartner.TELEGRAM_API`.
+        request_count (:obj:`int`): The number of successful requests that exceeded regular limits
+            and were therefore billed.
+    """
+
+    __slots__ = ("request_count",)
+
+    def __init__(self, request_count: int, *, api_kwargs: Optional[JSONDict] = None) -> None:
+        super().__init__(type=TransactionPartner.TELEGRAM_API, api_kwargs=api_kwargs)
+        with self._unfrozen():
+            self.request_count: int = request_count
+            self._id_attrs = (self.request_count,)
diff --git a/tests/_payment/stars/__init__.py b/tests/_payment/stars/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/_payment/stars/test_affiliateinfo.py b/tests/_payment/stars/test_affiliateinfo.py
new file mode 100644
index 00000000000..9a199b69491
--- /dev/null
+++ b/tests/_payment/stars/test_affiliateinfo.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+import pytest
+
+from telegram import AffiliateInfo, Chat, Dice, User
+from tests.auxil.slots import mro_slots
+
+
+@pytest.fixture
+def affiliate_info():
+    return AffiliateInfo(
+        affiliate_user=AffiliateInfoTestBase.affiliate_user,
+        affiliate_chat=AffiliateInfoTestBase.affiliate_chat,
+        commission_per_mille=AffiliateInfoTestBase.commission_per_mille,
+        amount=AffiliateInfoTestBase.amount,
+        nanostar_amount=AffiliateInfoTestBase.nanostar_amount,
+    )
+
+
+class AffiliateInfoTestBase:
+    affiliate_user = User(id=1, is_bot=True, first_name="affiliate_user", username="username")
+    affiliate_chat = Chat(id=2, type="private", title="affiliate_chat")
+    commission_per_mille = 13
+    amount = 14
+    nanostar_amount = -42
+
+
+class TestAffiliateInfoWithoutRequest(AffiliateInfoTestBase):
+    def test_slot_behaviour(self, affiliate_info):
+        inst = affiliate_info
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {
+            "affiliate_user": self.affiliate_user.to_dict(),
+            "affiliate_chat": self.affiliate_chat.to_dict(),
+            "commission_per_mille": self.commission_per_mille,
+            "amount": self.amount,
+            "nanostar_amount": self.nanostar_amount,
+        }
+        ai = AffiliateInfo.de_json(json_dict, offline_bot)
+        assert ai.api_kwargs == {}
+        assert ai.affiliate_user == self.affiliate_user
+        assert ai.affiliate_chat == self.affiliate_chat
+        assert ai.commission_per_mille == self.commission_per_mille
+        assert ai.amount == self.amount
+        assert ai.nanostar_amount == self.nanostar_amount
+
+        assert AffiliateInfo.de_json(None, offline_bot) is None
+        assert AffiliateInfo.de_json({}, offline_bot) is None
+
+    def test_to_dict(self, affiliate_info):
+        ai_dict = affiliate_info.to_dict()
+
+        assert isinstance(ai_dict, dict)
+        assert ai_dict["affiliate_user"] == affiliate_info.affiliate_user.to_dict()
+        assert ai_dict["affiliate_chat"] == affiliate_info.affiliate_chat.to_dict()
+        assert ai_dict["commission_per_mille"] == affiliate_info.commission_per_mille
+        assert ai_dict["amount"] == affiliate_info.amount
+        assert ai_dict["nanostar_amount"] == affiliate_info.nanostar_amount
+
+    def test_equality(self, affiliate_info, offline_bot):
+        a = AffiliateInfo(
+            affiliate_user=self.affiliate_user,
+            affiliate_chat=self.affiliate_chat,
+            commission_per_mille=self.commission_per_mille,
+            amount=self.amount,
+            nanostar_amount=self.nanostar_amount,
+        )
+        b = AffiliateInfo(
+            affiliate_user=self.affiliate_user,
+            affiliate_chat=self.affiliate_chat,
+            commission_per_mille=self.commission_per_mille,
+            amount=self.amount,
+            nanostar_amount=self.nanostar_amount,
+        )
+        c = AffiliateInfo(
+            affiliate_user=User(id=3, is_bot=True, first_name="first_name", username="username"),
+            affiliate_chat=self.affiliate_chat,
+            commission_per_mille=self.commission_per_mille,
+            amount=self.amount,
+            nanostar_amount=self.nanostar_amount,
+        )
+        d = AffiliateInfo(
+            affiliate_user=self.affiliate_user,
+            affiliate_chat=Chat(id=3, type="private", title="title"),
+            commission_per_mille=self.commission_per_mille,
+            amount=self.amount,
+            nanostar_amount=self.nanostar_amount,
+        )
+        e = AffiliateInfo(
+            affiliate_user=self.affiliate_user,
+            affiliate_chat=self.affiliate_chat,
+            commission_per_mille=1,
+            amount=self.amount,
+            nanostar_amount=self.nanostar_amount,
+        )
+        f = AffiliateInfo(
+            affiliate_user=self.affiliate_user,
+            affiliate_chat=self.affiliate_chat,
+            commission_per_mille=self.commission_per_mille,
+            amount=1,
+            nanostar_amount=self.nanostar_amount,
+        )
+        g = AffiliateInfo(
+            affiliate_user=self.affiliate_user,
+            affiliate_chat=self.affiliate_chat,
+            commission_per_mille=self.commission_per_mille,
+            amount=self.amount,
+            nanostar_amount=1,
+        )
+        h = Dice(4, "emoji")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+        assert a != e
+        assert hash(a) != hash(e)
+
+        assert a != f
+        assert hash(a) != hash(f)
+
+        assert a != g
+        assert hash(a) != hash(g)
+
+        assert a != h
+        assert hash(a) != hash(h)
diff --git a/tests/_payment/stars/test_revenuewithdrawelstate.py b/tests/_payment/stars/test_revenuewithdrawelstate.py
new file mode 100644
index 00000000000..5ff8fddc2fe
--- /dev/null
+++ b/tests/_payment/stars/test_revenuewithdrawelstate.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
+import pytest
+
+from telegram import (
+    Dice,
+    RevenueWithdrawalState,
+    RevenueWithdrawalStateFailed,
+    RevenueWithdrawalStatePending,
+    RevenueWithdrawalStateSucceeded,
+)
+from telegram._utils.datetime import UTC, to_timestamp
+from telegram.constants import RevenueWithdrawalStateType
+from tests.auxil.slots import mro_slots
+
+
+@pytest.fixture
+def revenue_withdrawal_state():
+    return RevenueWithdrawalState(RevenueWithdrawalStateTestBase.state)
+
+
+@pytest.fixture
+def revenue_withdrawal_state_pending():
+    return RevenueWithdrawalStatePending()
+
+
+@pytest.fixture
+def revenue_withdrawal_state_succeeded():
+    return RevenueWithdrawalStateSucceeded(
+        date=RevenueWithdrawalStateTestBase.date, url=RevenueWithdrawalStateTestBase.url
+    )
+
+
+@pytest.fixture
+def revenue_withdrawal_state_failed():
+    return RevenueWithdrawalStateFailed()
+
+
+class RevenueWithdrawalStateTestBase:
+    state = "failed"
+    date = dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)
+    url = "url"
+
+
+class TestRevenueWithdrawalStateWithoutRequest(RevenueWithdrawalStateTestBase):
+    def test_slot_behaviour(self, revenue_withdrawal_state):
+        inst = revenue_withdrawal_state
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_type_enum_conversion(self):
+        assert type(RevenueWithdrawalState("failed").type) is RevenueWithdrawalStateType
+        assert RevenueWithdrawalState("unknown").type == "unknown"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {"type": "unknown"}
+        rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
+        assert rws.api_kwargs == {}
+        assert rws.type == "unknown"
+
+        assert RevenueWithdrawalState.de_json(None, offline_bot) is None
+        assert RevenueWithdrawalState.de_json({}, offline_bot) is None
+
+    @pytest.mark.parametrize(
+        ("state", "subclass"),
+        [
+            ("pending", RevenueWithdrawalStatePending),
+            ("succeeded", RevenueWithdrawalStateSucceeded),
+            ("failed", RevenueWithdrawalStateFailed),
+        ],
+    )
+    def test_de_json_subclass(self, offline_bot, state, subclass):
+        json_dict = {"type": state, "date": to_timestamp(self.date), "url": self.url}
+        rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
+
+        assert type(rws) is subclass
+        assert set(rws.api_kwargs.keys()) == {"date", "url"} - set(subclass.__slots__)
+        assert rws.type == state
+
+    def test_to_dict(self, revenue_withdrawal_state):
+        json_dict = revenue_withdrawal_state.to_dict()
+        assert json_dict == {"type": self.state}
+
+    def test_equality(self, revenue_withdrawal_state):
+        a = revenue_withdrawal_state
+        b = RevenueWithdrawalState(self.state)
+        c = RevenueWithdrawalState("pending")
+        d = Dice(value=1, emoji="🎲")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+class TestRevenueWithdrawalStatePendingWithoutRequest(RevenueWithdrawalStateTestBase):
+    def test_slot_behaviour(self, revenue_withdrawal_state_pending):
+        inst = revenue_withdrawal_state_pending
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {}
+        rws = RevenueWithdrawalStatePending.de_json(json_dict, offline_bot)
+        assert rws.api_kwargs == {}
+        assert rws.type == "pending"
+
+        assert RevenueWithdrawalStatePending.de_json(None, offline_bot) is None
+
+    def test_to_dict(self, revenue_withdrawal_state_pending):
+        json_dict = revenue_withdrawal_state_pending.to_dict()
+        assert json_dict == {"type": "pending"}
+
+    def test_equality(self, revenue_withdrawal_state_pending):
+        a = revenue_withdrawal_state_pending
+        b = RevenueWithdrawalStatePending()
+        c = RevenueWithdrawalState("unknown")
+        d = Dice(value=1, emoji="🎲")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+class TestRevenueWithdrawalStateSucceededWithoutRequest(RevenueWithdrawalStateTestBase):
+    state = RevenueWithdrawalStateType.SUCCEEDED
+
+    def test_slot_behaviour(self, revenue_withdrawal_state_succeeded):
+        inst = revenue_withdrawal_state_succeeded
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {"date": to_timestamp(self.date), "url": self.url}
+        rws = RevenueWithdrawalStateSucceeded.de_json(json_dict, offline_bot)
+        assert rws.api_kwargs == {}
+        assert rws.type == "succeeded"
+        assert rws.date == self.date
+        assert rws.url == self.url
+
+        assert RevenueWithdrawalStateSucceeded.de_json(None, offline_bot) is None
+
+    def test_to_dict(self, revenue_withdrawal_state_succeeded):
+        json_dict = revenue_withdrawal_state_succeeded.to_dict()
+        assert json_dict["type"] == "succeeded"
+        assert json_dict["date"] == to_timestamp(self.date)
+        assert json_dict["url"] == self.url
+
+    def test_equality(self, revenue_withdrawal_state_succeeded):
+        a = revenue_withdrawal_state_succeeded
+        b = RevenueWithdrawalStateSucceeded(date=self.date, url=self.url)
+        c = RevenueWithdrawalStateSucceeded(date=self.date, url="other_url")
+        d = RevenueWithdrawalStateSucceeded(
+            date=dtm.datetime(1900, 1, 1, 0, 0, 0, 0, tzinfo=UTC), url=self.url
+        )
+        e = Dice(value=1, emoji="🎲")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a == c
+        assert hash(a) == hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+        assert a != e
+        assert hash(a) != hash(e)
+
+
+class TestRevenueWithdrawalStateFailedWithoutRequest(RevenueWithdrawalStateTestBase):
+    state = RevenueWithdrawalStateType.FAILED
+
+    def test_slot_behaviour(self, revenue_withdrawal_state_failed):
+        inst = revenue_withdrawal_state_failed
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {}
+        rws = RevenueWithdrawalStateFailed.de_json(json_dict, offline_bot)
+        assert rws.api_kwargs == {}
+        assert rws.type == "failed"
+
+        assert RevenueWithdrawalStateFailed.de_json(None, offline_bot) is None
+
+    def test_to_dict(self, revenue_withdrawal_state_failed):
+        json_dict = revenue_withdrawal_state_failed.to_dict()
+        assert json_dict == {"type": "failed"}
+
+    def test_equality(self, revenue_withdrawal_state_failed):
+        a = revenue_withdrawal_state_failed
+        b = RevenueWithdrawalStateFailed()
+        c = RevenueWithdrawalState("unknown")
+        d = Dice(value=1, emoji="🎲")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
diff --git a/tests/_payment/stars/test_startransactions.py b/tests/_payment/stars/test_startransactions.py
new file mode 100644
index 00000000000..71d706b8636
--- /dev/null
+++ b/tests/_payment/stars/test_startransactions.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
+import pytest
+
+from telegram import (
+    StarTransaction,
+    StarTransactions,
+    TransactionPartnerOther,
+    TransactionPartnerUser,
+    User,
+)
+from telegram._utils.datetime import UTC, from_timestamp, to_timestamp
+from tests.auxil.slots import mro_slots
+
+
+def star_transaction_factory():
+    return StarTransaction(
+        id=StarTransactionTestBase.id,
+        amount=StarTransactionTestBase.amount,
+        nanostar_amount=StarTransactionTestBase.nanostar_amount,
+        date=from_timestamp(StarTransactionTestBase.date),
+        source=StarTransactionTestBase.source,
+        receiver=StarTransactionTestBase.receiver,
+    )
+
+
+@pytest.fixture
+def star_transaction():
+    return star_transaction_factory()
+
+
+@pytest.fixture
+def star_transactions():
+    return StarTransactions(
+        transactions=[
+            star_transaction_factory(),
+            star_transaction_factory(),
+        ]
+    )
+
+
+class StarTransactionTestBase:
+    id = "2"
+    amount = 2
+    nanostar_amount = 365
+    date = to_timestamp(dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC))
+    source = TransactionPartnerUser(
+        user=User(
+            id=2,
+            is_bot=False,
+            first_name="first_name",
+        ),
+    )
+    receiver = TransactionPartnerOther()
+
+
+class TestStarTransactionWithoutRequest(StarTransactionTestBase):
+    def test_slot_behaviour(self, star_transaction):
+        inst = star_transaction
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {
+            "id": self.id,
+            "amount": self.amount,
+            "nanostar_amount": self.nanostar_amount,
+            "date": self.date,
+            "source": self.source.to_dict(),
+            "receiver": self.receiver.to_dict(),
+        }
+        st = StarTransaction.de_json(json_dict, offline_bot)
+        st_none = StarTransaction.de_json(None, offline_bot)
+        assert st.api_kwargs == {}
+        assert st.id == self.id
+        assert st.amount == self.amount
+        assert st.nanostar_amount == self.nanostar_amount
+        assert st.date == from_timestamp(self.date)
+        assert st.source == self.source
+        assert st.receiver == self.receiver
+        assert st_none is None
+
+    def test_de_json_star_transaction_localization(
+        self, tz_bot, offline_bot, raw_bot, star_transaction
+    ):
+        json_dict = star_transaction.to_dict()
+        st_raw = StarTransaction.de_json(json_dict, raw_bot)
+        st_bot = StarTransaction.de_json(json_dict, offline_bot)
+        st_tz = StarTransaction.de_json(json_dict, tz_bot)
+
+        # comparing utcoffsets because comparing timezones is unpredicatable
+        st_offset = st_tz.date.utcoffset()
+        tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(st_tz.date.replace(tzinfo=None))
+
+        assert st_raw.date.tzinfo == UTC
+        assert st_bot.date.tzinfo == UTC
+        assert st_offset == tz_bot_offset
+
+    def test_to_dict(self, star_transaction):
+        expected_dict = {
+            "id": self.id,
+            "amount": self.amount,
+            "nanostar_amount": self.nanostar_amount,
+            "date": self.date,
+            "source": self.source.to_dict(),
+            "receiver": self.receiver.to_dict(),
+        }
+        assert star_transaction.to_dict() == expected_dict
+
+    def test_equality(self):
+        a = StarTransaction(
+            id=self.id,
+            amount=self.amount,
+            date=self.date,
+            source=self.source,
+            receiver=self.receiver,
+        )
+        b = StarTransaction(
+            id=self.id,
+            amount=self.amount,
+            date=None,
+            source=self.source,
+            receiver=self.receiver,
+        )
+        c = StarTransaction(
+            id="3",
+            amount=3,
+            date=to_timestamp(dtm.datetime.utcnow()),
+            source=TransactionPartnerUser(
+                user=User(
+                    id=3,
+                    is_bot=False,
+                    first_name="first_name",
+                ),
+            ),
+            receiver=TransactionPartnerOther(),
+        )
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+
+class StarTransactionsTestBase:
+    transactions = [star_transaction_factory(), star_transaction_factory()]
+
+
+class TestStarTransactionsWithoutRequest(StarTransactionsTestBase):
+    def test_slot_behaviour(self, star_transactions):
+        inst = star_transactions
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {
+            "transactions": [t.to_dict() for t in self.transactions],
+        }
+        st = StarTransactions.de_json(json_dict, offline_bot)
+        st_none = StarTransactions.de_json(None, offline_bot)
+        assert st.api_kwargs == {}
+        assert st.transactions == tuple(self.transactions)
+        assert st_none is None
+
+    def test_to_dict(self, star_transactions):
+        expected_dict = {
+            "transactions": [t.to_dict() for t in self.transactions],
+        }
+        assert star_transactions.to_dict() == expected_dict
+
+    def test_equality(self, star_transaction):
+        a = StarTransactions(
+            transactions=self.transactions,
+        )
+        b = StarTransactions(
+            transactions=self.transactions,
+        )
+        c = StarTransactions(
+            transactions=[star_transaction],
+        )
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
diff --git a/tests/_payment/stars/test_transactionpartner.py b/tests/_payment/stars/test_transactionpartner.py
new file mode 100644
index 00000000000..0982700cfdd
--- /dev/null
+++ b/tests/_payment/stars/test_transactionpartner.py
@@ -0,0 +1,469 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
+import pytest
+
+from telegram import (
+    AffiliateInfo,
+    Gift,
+    PaidMediaVideo,
+    RevenueWithdrawalStatePending,
+    Sticker,
+    TransactionPartner,
+    TransactionPartnerAffiliateProgram,
+    TransactionPartnerFragment,
+    TransactionPartnerOther,
+    TransactionPartnerTelegramAds,
+    TransactionPartnerTelegramApi,
+    TransactionPartnerUser,
+    User,
+    Video,
+)
+from telegram.constants import TransactionPartnerType
+from tests.auxil.slots import mro_slots
+
+
+@pytest.fixture
+def transaction_partner():
+    return TransactionPartner(TransactionPartnerTestBase.type)
+
+
+class TransactionPartnerTestBase:
+    type = TransactionPartnerType.AFFILIATE_PROGRAM
+    commission_per_mille = 42
+    sponsor_user = User(
+        id=1,
+        is_bot=False,
+        first_name="sponsor",
+        last_name="user",
+    )
+    withdrawal_state = RevenueWithdrawalStatePending()
+    user = User(
+        id=2,
+        is_bot=False,
+        first_name="user",
+        last_name="user",
+    )
+    invoice_payload = "invoice_payload"
+    paid_media = (
+        PaidMediaVideo(
+            video=Video(
+                file_id="file_id",
+                file_unique_id="file_unique_id",
+                width=10,
+                height=10,
+                duration=10,
+            )
+        ),
+    )
+    paid_media_payload = "paid_media_payload"
+    subscription_period = dtm.timedelta(days=1)
+    gift = Gift(
+        id="gift_id",
+        sticker=Sticker(
+            file_id="file_id",
+            file_unique_id="file_unique_id",
+            width=10,
+            height=10,
+            is_animated=False,
+            is_video=False,
+            type="type",
+        ),
+        total_count=42,
+        remaining_count=42,
+        star_count=42,
+    )
+    affiliate = AffiliateInfo(
+        commission_per_mille=42,
+        amount=42,
+    )
+    request_count = 42
+
+
+class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
+    def test_slot_behaviour(self, transaction_partner):
+        inst = transaction_partner
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_type_enum_conversion(self, transaction_partner):
+        assert type(TransactionPartner("affiliate_program").type) is TransactionPartnerType
+        assert TransactionPartner("unknown").type == "unknown"
+
+    def test_de_json(self, offline_bot):
+        data = {"type": "unknown"}
+        transaction_partner = TransactionPartner.de_json(data, offline_bot)
+        assert transaction_partner.api_kwargs == {}
+        assert transaction_partner.type == "unknown"
+
+        assert TransactionPartner.de_json(None, offline_bot) is None
+        assert TransactionPartner.de_json({}, offline_bot) is None
+
+    @pytest.mark.parametrize(
+        ("tp_type", "subclass"),
+        [
+            ("affiliate_program", TransactionPartnerAffiliateProgram),
+            ("fragment", TransactionPartnerFragment),
+            ("user", TransactionPartnerUser),
+            ("telegram_ads", TransactionPartnerTelegramAds),
+            ("telegram_api", TransactionPartnerTelegramApi),
+            ("other", TransactionPartnerOther),
+        ],
+    )
+    def test_subclass(self, offline_bot, tp_type, subclass):
+        json_dict = {
+            "type": tp_type,
+            "commission_per_mille": self.commission_per_mille,
+            "user": self.user.to_dict(),
+            "request_count": self.request_count,
+        }
+        tp = TransactionPartner.de_json(json_dict, offline_bot)
+
+        assert type(tp) is subclass
+        assert set(tp.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
+            "type"
+        }
+        assert tp.type == tp_type
+
+    def test_to_dict(self, transaction_partner):
+        data = transaction_partner.to_dict()
+        assert data == {"type": self.type}
+
+    def test_equality(self, transaction_partner):
+        a = transaction_partner
+        b = TransactionPartner(self.type)
+        c = TransactionPartner("unknown")
+        d = User(id=1, is_bot=False, first_name="user", last_name="user")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+@pytest.fixture
+def transaction_partner_affiliate_program():
+    return TransactionPartnerAffiliateProgram(
+        commission_per_mille=TransactionPartnerTestBase.commission_per_mille,
+        sponsor_user=TransactionPartnerTestBase.sponsor_user,
+    )
+
+
+class TestTransactionPartnerAffiliateProgramWithoutRequest(TransactionPartnerTestBase):
+    type = TransactionPartnerType.AFFILIATE_PROGRAM
+
+    def test_slot_behaviour(self, transaction_partner_affiliate_program):
+        inst = transaction_partner_affiliate_program
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {
+            "commission_per_mille": self.commission_per_mille,
+            "sponsor_user": self.sponsor_user.to_dict(),
+        }
+        tp = TransactionPartnerAffiliateProgram.de_json(json_dict, offline_bot)
+        assert tp.api_kwargs == {}
+        assert tp.type == "affiliate_program"
+        assert tp.commission_per_mille == self.commission_per_mille
+        assert tp.sponsor_user == self.sponsor_user
+
+        assert TransactionPartnerAffiliateProgram.de_json(None, offline_bot) is None
+        assert TransactionPartnerAffiliateProgram.de_json({}, offline_bot) is None
+
+    def test_to_dict(self, transaction_partner_affiliate_program):
+        json_dict = transaction_partner_affiliate_program.to_dict()
+        assert json_dict["type"] == self.type
+        assert json_dict["commission_per_mille"] == self.commission_per_mille
+        assert json_dict["sponsor_user"] == self.sponsor_user.to_dict()
+
+    def test_equality(self, transaction_partner_affiliate_program):
+        a = transaction_partner_affiliate_program
+        b = TransactionPartnerAffiliateProgram(
+            commission_per_mille=self.commission_per_mille,
+        )
+        c = TransactionPartnerAffiliateProgram(
+            commission_per_mille=0,
+        )
+        d = User(id=1, is_bot=False, first_name="user", last_name="user")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+@pytest.fixture
+def transaction_partner_fragment():
+    return TransactionPartnerFragment(
+        withdrawal_state=TransactionPartnerTestBase.withdrawal_state,
+    )
+
+
+class TestTransactionPartnerFragmentWithoutRequest(TransactionPartnerTestBase):
+    type = TransactionPartnerType.FRAGMENT
+
+    def test_slot_behaviour(self, transaction_partner_fragment):
+        inst = transaction_partner_fragment
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {"withdrawal_state": self.withdrawal_state.to_dict()}
+        tp = TransactionPartnerFragment.de_json(json_dict, offline_bot)
+        assert tp.api_kwargs == {}
+        assert tp.type == "fragment"
+        assert tp.withdrawal_state == self.withdrawal_state
+
+        assert TransactionPartnerFragment.de_json(None, offline_bot) is None
+
+    def test_to_dict(self, transaction_partner_fragment):
+        json_dict = transaction_partner_fragment.to_dict()
+        assert json_dict["type"] == self.type
+        assert json_dict["withdrawal_state"] == self.withdrawal_state.to_dict()
+
+    def test_equality(self, transaction_partner_fragment):
+        a = transaction_partner_fragment
+        b = TransactionPartnerFragment(withdrawal_state=self.withdrawal_state)
+        c = TransactionPartnerFragment(withdrawal_state=None)
+        d = User(id=1, is_bot=False, first_name="user", last_name="user")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a == c
+        assert hash(a) == hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+@pytest.fixture
+def transaction_partner_user():
+    return TransactionPartnerUser(
+        user=TransactionPartnerTestBase.user,
+        invoice_payload=TransactionPartnerTestBase.invoice_payload,
+        paid_media=TransactionPartnerTestBase.paid_media,
+        paid_media_payload=TransactionPartnerTestBase.paid_media_payload,
+        subscription_period=TransactionPartnerTestBase.subscription_period,
+    )
+
+
+class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase):
+    type = TransactionPartnerType.USER
+
+    def test_slot_behaviour(self, transaction_partner_user):
+        inst = transaction_partner_user
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {
+            "user": self.user.to_dict(),
+            "invoice_payload": self.invoice_payload,
+            "paid_media": [pm.to_dict() for pm in self.paid_media],
+            "paid_media_payload": self.paid_media_payload,
+            "subscription_period": self.subscription_period.total_seconds(),
+        }
+        tp = TransactionPartnerUser.de_json(json_dict, offline_bot)
+        assert tp.api_kwargs == {}
+        assert tp.type == "user"
+        assert tp.user == self.user
+        assert tp.invoice_payload == self.invoice_payload
+        assert tp.paid_media == self.paid_media
+        assert tp.paid_media_payload == self.paid_media_payload
+        assert tp.subscription_period == self.subscription_period
+
+        assert TransactionPartnerUser.de_json(None, offline_bot) is None
+        assert TransactionPartnerUser.de_json({}, offline_bot) is None
+
+    def test_to_dict(self, transaction_partner_user):
+        json_dict = transaction_partner_user.to_dict()
+        assert json_dict["type"] == self.type
+        assert json_dict["user"] == self.user.to_dict()
+        assert json_dict["invoice_payload"] == self.invoice_payload
+        assert json_dict["paid_media"] == [pm.to_dict() for pm in self.paid_media]
+        assert json_dict["paid_media_payload"] == self.paid_media_payload
+        assert json_dict["subscription_period"] == self.subscription_period.total_seconds()
+
+    def test_equality(self, transaction_partner_user):
+        a = transaction_partner_user
+        b = TransactionPartnerUser(
+            user=self.user,
+        )
+        c = TransactionPartnerUser(
+            user=User(id=1, is_bot=False, first_name="user", last_name="user"),
+        )
+        d = User(id=1, is_bot=False, first_name="user", last_name="user")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+@pytest.fixture
+def transaction_partner_other():
+    return TransactionPartnerOther()
+
+
+class TestTransactionPartnerOtherWithoutRequest(TransactionPartnerTestBase):
+    type = TransactionPartnerType.OTHER
+
+    def test_slot_behaviour(self, transaction_partner_other):
+        inst = transaction_partner_other
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {}
+        tp = TransactionPartnerOther.de_json(json_dict, offline_bot)
+        assert tp.api_kwargs == {}
+        assert tp.type == "other"
+
+        assert TransactionPartnerOther.de_json(None, offline_bot) is None
+
+    def test_to_dict(self, transaction_partner_other):
+        json_dict = transaction_partner_other.to_dict()
+        assert json_dict == {"type": self.type}
+
+    def test_equality(self, transaction_partner_other):
+        a = transaction_partner_other
+        b = TransactionPartnerOther()
+        c = TransactionPartnerOther()
+        d = User(id=1, is_bot=False, first_name="user", last_name="user")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a == c
+        assert hash(a) == hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+@pytest.fixture
+def transaction_partner_telegram_ads():
+    return TransactionPartnerTelegramAds()
+
+
+class TestTransactionPartnerTelegramAdsWithoutRequest(TransactionPartnerTestBase):
+    type = TransactionPartnerType.TELEGRAM_ADS
+
+    def test_slot_behaviour(self, transaction_partner_telegram_ads):
+        inst = transaction_partner_telegram_ads
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {}
+        tp = TransactionPartnerTelegramAds.de_json(json_dict, offline_bot)
+        assert tp.api_kwargs == {}
+        assert tp.type == "telegram_ads"
+
+        assert TransactionPartnerTelegramAds.de_json(None, offline_bot) is None
+
+    def test_to_dict(self, transaction_partner_telegram_ads):
+        json_dict = transaction_partner_telegram_ads.to_dict()
+        assert json_dict == {"type": self.type}
+
+    def test_equality(self, transaction_partner_telegram_ads):
+        a = transaction_partner_telegram_ads
+        b = TransactionPartnerTelegramAds()
+        c = TransactionPartnerTelegramAds()
+        d = User(id=1, is_bot=False, first_name="user", last_name="user")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a == c
+        assert hash(a) == hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
+
+
+@pytest.fixture
+def transaction_partner_telegram_api():
+    return TransactionPartnerTelegramApi(
+        request_count=TransactionPartnerTestBase.request_count,
+    )
+
+
+class TestTransactionPartnerTelegramApiWithoutRequest(TransactionPartnerTestBase):
+    type = TransactionPartnerType.TELEGRAM_API
+
+    def test_slot_behaviour(self, transaction_partner_telegram_api):
+        inst = transaction_partner_telegram_api
+        for attr in inst.__slots__:
+            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
+        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
+
+    def test_de_json(self, offline_bot):
+        json_dict = {"request_count": self.request_count}
+        tp = TransactionPartnerTelegramApi.de_json(json_dict, offline_bot)
+        assert tp.api_kwargs == {}
+        assert tp.type == "telegram_api"
+        assert tp.request_count == self.request_count
+
+        assert TransactionPartnerTelegramApi.de_json(None, offline_bot) is None
+
+    def test_to_dict(self, transaction_partner_telegram_api):
+        json_dict = transaction_partner_telegram_api.to_dict()
+        assert json_dict["type"] == self.type
+        assert json_dict["request_count"] == self.request_count
+
+    def test_equality(self, transaction_partner_telegram_api):
+        a = transaction_partner_telegram_api
+        b = TransactionPartnerTelegramApi(
+            request_count=self.request_count,
+        )
+        c = TransactionPartnerTelegramApi(
+            request_count=0,
+        )
+        d = User(id=1, is_bot=False, first_name="user", last_name="user")
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+        assert a != c
+        assert hash(a) != hash(c)
+
+        assert a != d
+        assert hash(a) != hash(d)
diff --git a/tests/test_stars.py b/tests/test_stars.py
deleted file mode 100644
index 4a02c52b94e..00000000000
--- a/tests/test_stars.py
+++ /dev/null
@@ -1,849 +0,0 @@
-#!/usr/bin/env python
-#
-# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
-# Leandro Toledo de Souza <devs@python-telegram-bot.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Lesser Public License for more details.
-#
-# You should have received a copy of the GNU Lesser Public License
-# along with this program. If not, see [http://www.gnu.org/licenses/].
-
-import datetime as dtm
-from collections.abc import Sequence
-from copy import deepcopy
-
-import pytest
-
-from telegram import (
-    Chat,
-    Dice,
-    Gift,
-    PaidMediaPhoto,
-    PhotoSize,
-    RevenueWithdrawalState,
-    RevenueWithdrawalStateFailed,
-    RevenueWithdrawalStatePending,
-    RevenueWithdrawalStateSucceeded,
-    StarTransaction,
-    StarTransactions,
-    Sticker,
-    TelegramObject,
-    TransactionPartner,
-    TransactionPartnerFragment,
-    TransactionPartnerOther,
-    TransactionPartnerTelegramAds,
-    TransactionPartnerTelegramApi,
-    TransactionPartnerUser,
-    User,
-)
-from telegram._payment.stars import AffiliateInfo, TransactionPartnerAffiliateProgram
-from telegram._utils.datetime import UTC, from_timestamp, to_timestamp
-from telegram.constants import RevenueWithdrawalStateType, TransactionPartnerType
-from tests.auxil.slots import mro_slots
-
-
-def withdrawal_state_succeeded():
-    return RevenueWithdrawalStateSucceeded(
-        date=dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC),
-        url="url",
-    )
-
-
-@pytest.fixture
-def withdrawal_state_failed():
-    return RevenueWithdrawalStateFailed()
-
-
-@pytest.fixture
-def withdrawal_state_pending():
-    return RevenueWithdrawalStatePending()
-
-
-def transaction_partner_user():
-    return TransactionPartnerUser(
-        user=User(id=1, is_bot=False, first_name="first_name", username="username"),
-        affiliate=AffiliateInfo(
-            affiliate_user=User(id=2, is_bot=True, first_name="first_name", username="username"),
-            affiliate_chat=Chat(id=3, type="private", title="title"),
-            commission_per_mille=1,
-            amount=2,
-            nanostar_amount=3,
-        ),
-        invoice_payload="payload",
-        paid_media=[
-            PaidMediaPhoto(
-                photo=[
-                    PhotoSize(
-                        file_id="file_id", width=1, height=1, file_unique_id="file_unique_id"
-                    )
-                ]
-            )
-        ],
-        paid_media_payload="payload",
-        subscription_period=dtm.timedelta(days=1),
-        gift=Gift(
-            id="some_id",
-            sticker=Sticker(
-                file_id="file_id",
-                file_unique_id="file_unique_id",
-                width=512,
-                height=512,
-                is_animated=False,
-                is_video=False,
-                type="regular",
-            ),
-            star_count=5,
-            total_count=10,
-            remaining_count=5,
-        ),
-    )
-
-
-def transaction_partner_affiliate_program():
-    return TransactionPartnerAffiliateProgram(
-        sponsor_user=User(id=1, is_bot=True, first_name="first_name", username="username"),
-        commission_per_mille=42,
-    )
-
-
-def transaction_partner_fragment():
-    return TransactionPartnerFragment(
-        withdrawal_state=withdrawal_state_succeeded(),
-    )
-
-
-def star_transaction():
-    return StarTransaction(
-        id="1",
-        amount=1,
-        nanostar_amount=365,
-        date=to_timestamp(dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)),
-        source=transaction_partner_user(),
-        receiver=transaction_partner_fragment(),
-    )
-
-
-@pytest.fixture
-def star_transactions():
-    return StarTransactions(
-        transactions=[
-            star_transaction(),
-            star_transaction(),
-        ]
-    )
-
-
-@pytest.fixture(
-    scope="module",
-    params=[
-        TransactionPartner.AFFILIATE_PROGRAM,
-        TransactionPartner.FRAGMENT,
-        TransactionPartner.OTHER,
-        TransactionPartner.TELEGRAM_ADS,
-        TransactionPartner.TELEGRAM_API,
-        TransactionPartner.USER,
-    ],
-)
-def tp_scope_type(request):
-    return request.param
-
-
-@pytest.fixture(
-    scope="module",
-    params=[
-        TransactionPartnerAffiliateProgram,
-        TransactionPartnerFragment,
-        TransactionPartnerOther,
-        TransactionPartnerTelegramAds,
-        TransactionPartnerTelegramApi,
-        TransactionPartnerUser,
-    ],
-    ids=[
-        TransactionPartner.AFFILIATE_PROGRAM,
-        TransactionPartner.FRAGMENT,
-        TransactionPartner.OTHER,
-        TransactionPartner.TELEGRAM_ADS,
-        TransactionPartner.TELEGRAM_API,
-        TransactionPartner.USER,
-    ],
-)
-def tp_scope_class(request):
-    return request.param
-
-
-@pytest.fixture(
-    scope="module",
-    params=[
-        (TransactionPartnerAffiliateProgram, TransactionPartner.AFFILIATE_PROGRAM),
-        (TransactionPartnerFragment, TransactionPartner.FRAGMENT),
-        (TransactionPartnerOther, TransactionPartner.OTHER),
-        (TransactionPartnerTelegramAds, TransactionPartner.TELEGRAM_ADS),
-        (TransactionPartnerTelegramApi, TransactionPartner.TELEGRAM_API),
-        (TransactionPartnerUser, TransactionPartner.USER),
-    ],
-    ids=[
-        TransactionPartner.AFFILIATE_PROGRAM,
-        TransactionPartner.FRAGMENT,
-        TransactionPartner.OTHER,
-        TransactionPartner.TELEGRAM_ADS,
-        TransactionPartner.TELEGRAM_API,
-        TransactionPartner.USER,
-    ],
-)
-def tp_scope_class_and_type(request):
-    return request.param
-
-
-@pytest.fixture(scope="module")
-def transaction_partner(tp_scope_class_and_type):
-    # We use de_json here so that we don't have to worry about which class gets which arguments
-    return tp_scope_class_and_type[0].de_json(
-        {
-            "type": tp_scope_class_and_type[1],
-            "invoice_payload": TransactionPartnerTestBase.invoice_payload,
-            "withdrawal_state": TransactionPartnerTestBase.withdrawal_state.to_dict(),
-            "user": TransactionPartnerTestBase.user.to_dict(),
-            "affiliate": TransactionPartnerTestBase.affiliate.to_dict(),
-            "request_count": TransactionPartnerTestBase.request_count,
-            "sponsor_user": TransactionPartnerTestBase.sponsor_user.to_dict(),
-            "commission_per_mille": TransactionPartnerTestBase.commission_per_mille,
-            "gift": TransactionPartnerTestBase.gift.to_dict(),
-            "paid_media": [m.to_dict() for m in TransactionPartnerTestBase.paid_media],
-            "paid_media_payload": TransactionPartnerTestBase.paid_media_payload,
-            "subscription_period": TransactionPartnerTestBase.subscription_period.total_seconds(),
-        },
-        bot=None,
-    )
-
-
-@pytest.fixture(
-    scope="module",
-    params=[
-        RevenueWithdrawalState.FAILED,
-        RevenueWithdrawalState.SUCCEEDED,
-        RevenueWithdrawalState.PENDING,
-    ],
-)
-def rws_scope_type(request):
-    return request.param
-
-
-@pytest.fixture(
-    scope="module",
-    params=[
-        RevenueWithdrawalStateFailed,
-        RevenueWithdrawalStateSucceeded,
-        RevenueWithdrawalStatePending,
-    ],
-    ids=[
-        RevenueWithdrawalState.FAILED,
-        RevenueWithdrawalState.SUCCEEDED,
-        RevenueWithdrawalState.PENDING,
-    ],
-)
-def rws_scope_class(request):
-    return request.param
-
-
-@pytest.fixture(
-    scope="module",
-    params=[
-        (RevenueWithdrawalStateFailed, RevenueWithdrawalState.FAILED),
-        (RevenueWithdrawalStateSucceeded, RevenueWithdrawalState.SUCCEEDED),
-        (RevenueWithdrawalStatePending, RevenueWithdrawalState.PENDING),
-    ],
-    ids=[
-        RevenueWithdrawalState.FAILED,
-        RevenueWithdrawalState.SUCCEEDED,
-        RevenueWithdrawalState.PENDING,
-    ],
-)
-def rws_scope_class_and_type(request):
-    return request.param
-
-
-@pytest.fixture(scope="module")
-def revenue_withdrawal_state(rws_scope_class_and_type):
-    # We use de_json here so that we don't have to worry about which class gets which arguments
-    return rws_scope_class_and_type[0].de_json(
-        {
-            "type": rws_scope_class_and_type[1],
-            "date": to_timestamp(RevenueWithdrawalStateTestBase.date),
-            "url": RevenueWithdrawalStateTestBase.url,
-        },
-        bot=None,
-    )
-
-
-class StarTransactionTestBase:
-    id = "2"
-    amount = 2
-    nanostar_amount = 365
-    date = to_timestamp(dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC))
-    source = TransactionPartnerUser(
-        user=User(
-            id=2,
-            is_bot=False,
-            first_name="first_name",
-        ),
-    )
-    receiver = TransactionPartnerOther()
-
-
-class TestStarTransactionWithoutRequest(StarTransactionTestBase):
-    def test_slot_behaviour(self):
-        inst = star_transaction()
-        for attr in inst.__slots__:
-            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
-        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
-
-    def test_de_json(self, offline_bot):
-        json_dict = {
-            "id": self.id,
-            "amount": self.amount,
-            "nanostar_amount": self.nanostar_amount,
-            "date": self.date,
-            "source": self.source.to_dict(),
-            "receiver": self.receiver.to_dict(),
-        }
-        st = StarTransaction.de_json(json_dict, offline_bot)
-        st_none = StarTransaction.de_json(None, offline_bot)
-        assert st.api_kwargs == {}
-        assert st.id == self.id
-        assert st.amount == self.amount
-        assert st.nanostar_amount == self.nanostar_amount
-        assert st.date == from_timestamp(self.date)
-        assert st.source == self.source
-        assert st.receiver == self.receiver
-        assert st_none is None
-
-    def test_de_json_star_transaction_localization(self, tz_bot, offline_bot, raw_bot):
-        json_dict = star_transaction().to_dict()
-        st_raw = StarTransaction.de_json(json_dict, raw_bot)
-        st_bot = StarTransaction.de_json(json_dict, offline_bot)
-        st_tz = StarTransaction.de_json(json_dict, tz_bot)
-
-        # comparing utcoffsets because comparing timezones is unpredicatable
-        st_offset = st_tz.date.utcoffset()
-        tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(st_tz.date.replace(tzinfo=None))
-
-        assert st_raw.date.tzinfo == UTC
-        assert st_bot.date.tzinfo == UTC
-        assert st_offset == tz_bot_offset
-
-    def test_to_dict(self):
-        st = star_transaction()
-        expected_dict = {
-            "id": "1",
-            "amount": 1,
-            "nanostar_amount": 365,
-            "date": st.date,
-            "source": st.source.to_dict(),
-            "receiver": st.receiver.to_dict(),
-        }
-        assert st.to_dict() == expected_dict
-
-    def test_equality(self):
-        a = StarTransaction(
-            id=self.id,
-            amount=self.amount,
-            date=self.date,
-            source=self.source,
-            receiver=self.receiver,
-        )
-        b = StarTransaction(
-            id=self.id,
-            amount=self.amount,
-            date=None,
-            source=self.source,
-            receiver=self.receiver,
-        )
-        c = StarTransaction(
-            id="3",
-            amount=3,
-            date=to_timestamp(dtm.datetime.utcnow()),
-            source=TransactionPartnerUser(
-                user=User(
-                    id=3,
-                    is_bot=False,
-                    first_name="first_name",
-                ),
-            ),
-            receiver=TransactionPartnerOther(),
-        )
-
-        assert a == b
-        assert hash(a) == hash(b)
-
-        assert a != c
-        assert hash(a) != hash(c)
-
-
-class StarTransactionsTestBase:
-    transactions = [star_transaction(), star_transaction()]
-
-
-class TestStarTransactionsWithoutRequest(StarTransactionsTestBase):
-    def test_slot_behaviour(self, star_transactions):
-        inst = star_transactions
-        for attr in inst.__slots__:
-            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
-        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
-
-    def test_de_json(self, offline_bot):
-        json_dict = {
-            "transactions": [t.to_dict() for t in self.transactions],
-        }
-        st = StarTransactions.de_json(json_dict, offline_bot)
-        st_none = StarTransactions.de_json(None, offline_bot)
-        assert st.api_kwargs == {}
-        assert st.transactions == tuple(self.transactions)
-        assert st_none is None
-
-    def test_to_dict(self, star_transactions):
-        expected_dict = {
-            "transactions": [t.to_dict() for t in self.transactions],
-        }
-        assert star_transactions.to_dict() == expected_dict
-
-    def test_equality(self):
-        a = StarTransactions(
-            transactions=self.transactions,
-        )
-        b = StarTransactions(
-            transactions=self.transactions,
-        )
-        c = StarTransactions(
-            transactions=[star_transaction()],
-        )
-
-        assert a == b
-        assert hash(a) == hash(b)
-
-        assert a != c
-        assert hash(a) != hash(c)
-
-
-class TransactionPartnerTestBase:
-    withdrawal_state = withdrawal_state_succeeded()
-    user = transaction_partner_user().user
-    affiliate = transaction_partner_user().affiliate
-    invoice_payload = "payload"
-    request_count = 42
-    sponsor_user = transaction_partner_affiliate_program().sponsor_user
-    commission_per_mille = transaction_partner_affiliate_program().commission_per_mille
-    gift = transaction_partner_user().gift
-    paid_media = transaction_partner_user().paid_media
-    paid_media_payload = transaction_partner_user().paid_media_payload
-    subscription_period = transaction_partner_user().subscription_period
-
-
-class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
-    def test_slot_behaviour(self, transaction_partner):
-        inst = transaction_partner
-        for attr in inst.__slots__:
-            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
-        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
-
-    def test_de_json(self, offline_bot, tp_scope_class_and_type):
-        cls = tp_scope_class_and_type[0]
-        type_ = tp_scope_class_and_type[1]
-
-        json_dict = {
-            "type": type_,
-            "invoice_payload": self.invoice_payload,
-            "withdrawal_state": self.withdrawal_state.to_dict(),
-            "user": self.user.to_dict(),
-            "affiliate": self.affiliate.to_dict(),
-            "request_count": self.request_count,
-            "sponsor_user": self.sponsor_user.to_dict(),
-            "commission_per_mille": self.commission_per_mille,
-        }
-        tp = TransactionPartner.de_json(json_dict, offline_bot)
-        assert set(tp.api_kwargs.keys()) == {
-            "user",
-            "affiliate",
-            "withdrawal_state",
-            "invoice_payload",
-            "request_count",
-            "sponsor_user",
-            "commission_per_mille",
-        } - set(cls.__slots__)
-
-        assert isinstance(tp, TransactionPartner)
-        assert type(tp) is cls
-        assert tp.type == type_
-        for key in json_dict:
-            if key in cls.__slots__:
-                assert getattr(tp, key) == getattr(self, key)
-
-        assert cls.de_json(None, offline_bot) is None
-        assert TransactionPartner.de_json({}, offline_bot) is None
-
-    def test_de_json_invalid_type(self, offline_bot):
-        json_dict = {
-            "type": "invalid",
-            "invoice_payload": self.invoice_payload,
-            "withdrawal_state": self.withdrawal_state.to_dict(),
-            "user": self.user.to_dict(),
-            "affiliate": self.affiliate.to_dict(),
-            "request_count": self.request_count,
-            "sponsor_user": self.sponsor_user.to_dict(),
-            "commission_per_mille": self.commission_per_mille,
-        }
-        tp = TransactionPartner.de_json(json_dict, offline_bot)
-        assert tp.api_kwargs == {
-            "withdrawal_state": self.withdrawal_state.to_dict(),
-            "user": self.user.to_dict(),
-            "affiliate": self.affiliate.to_dict(),
-            "invoice_payload": self.invoice_payload,
-            "request_count": self.request_count,
-            "sponsor_user": self.sponsor_user.to_dict(),
-            "commission_per_mille": self.commission_per_mille,
-        }
-
-        assert type(tp) is TransactionPartner
-        assert tp.type == "invalid"
-
-    def test_de_json_subclass(self, tp_scope_class, offline_bot):
-        """This makes sure that e.g. TransactionPartnerUser(data) never returns a
-        TransactionPartnerFragment instance."""
-        json_dict = {
-            "type": "invalid",
-            "invoice_payload": self.invoice_payload,
-            "withdrawal_state": self.withdrawal_state.to_dict(),
-            "user": self.user.to_dict(),
-            "affiliate": self.affiliate.to_dict(),
-            "request_count": self.request_count,
-            "commission_per_mille": self.commission_per_mille,
-        }
-        assert type(tp_scope_class.de_json(json_dict, offline_bot)) is tp_scope_class
-
-    def test_to_dict(self, transaction_partner):
-        tp_dict = transaction_partner.to_dict()
-
-        assert isinstance(tp_dict, dict)
-        assert tp_dict["type"] == transaction_partner.type
-        for attr in transaction_partner.__slots__:
-            attribute = getattr(transaction_partner, attr)
-            if isinstance(attribute, TelegramObject):
-                assert tp_dict[attr] == attribute.to_dict()
-            elif not isinstance(attribute, str) and isinstance(attribute, Sequence):
-                assert tp_dict[attr] == [a.to_dict() for a in attribute]
-            elif isinstance(attribute, dtm.timedelta):
-                assert tp_dict[attr] == attribute.total_seconds()
-            else:
-                assert tp_dict[attr] == attribute
-
-    def test_type_enum_conversion(self):
-        assert type(TransactionPartner("other").type) is TransactionPartnerType
-        assert TransactionPartner("unknown").type == "unknown"
-
-    def test_equality(self, transaction_partner, offline_bot):
-        a = TransactionPartner("base_type")
-        b = TransactionPartner("base_type")
-        c = transaction_partner
-        d = deepcopy(transaction_partner)
-        e = Dice(4, "emoji")
-
-        assert a == b
-        assert hash(a) == hash(b)
-
-        assert a != c
-        assert hash(a) != hash(c)
-
-        assert a != d
-        assert hash(a) != hash(d)
-
-        assert a != e
-        assert hash(a) != hash(e)
-
-        assert c == d
-        assert hash(c) == hash(d)
-
-        assert c != e
-        assert hash(c) != hash(e)
-
-        if hasattr(c, "user"):
-            json_dict = c.to_dict()
-            json_dict["user"] = User(2, "something", True).to_dict()
-            f = c.__class__.de_json(json_dict, offline_bot)
-
-            assert c != f
-            assert hash(c) != hash(f)
-
-        if hasattr(c, "request_count"):
-            json_dict = c.to_dict()
-            json_dict["request_count"] = 1
-            f = c.__class__.de_json(json_dict, offline_bot)
-
-            assert c != f
-            assert hash(c) != hash(f)
-
-
-class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase):
-    def test_de_json_required(self, offline_bot):
-        json_dict = {
-            "user": transaction_partner_user().user.to_dict(),
-        }
-        tp = TransactionPartnerUser.de_json(json_dict, offline_bot)
-        assert tp.api_kwargs == {}
-        assert tp.user == transaction_partner_user().user
-
-        # This test is here mainly to check that the below cases work
-        assert tp.subscription_period is None
-        assert tp.gift is None
-
-
-class RevenueWithdrawalStateTestBase:
-    date = dtm.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)
-    url = "url"
-
-
-class TestRevenueWithdrawalStateWithoutRequest(RevenueWithdrawalStateTestBase):
-    def test_slot_behaviour(self, revenue_withdrawal_state):
-        inst = revenue_withdrawal_state
-        for attr in inst.__slots__:
-            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
-        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
-
-    def test_de_json(self, offline_bot, rws_scope_class_and_type):
-        cls = rws_scope_class_and_type[0]
-        type_ = rws_scope_class_and_type[1]
-
-        json_dict = {
-            "type": type_,
-            "date": to_timestamp(self.date),
-            "url": self.url,
-        }
-        rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
-        assert set(rws.api_kwargs.keys()) == {"date", "url"} - set(cls.__slots__)
-
-        assert isinstance(rws, RevenueWithdrawalState)
-        assert type(rws) is cls
-        assert rws.type == type_
-        if "date" in cls.__slots__:
-            assert rws.date == self.date
-        if "url" in cls.__slots__:
-            assert rws.url == self.url
-
-        assert cls.de_json(None, offline_bot) is None
-        assert RevenueWithdrawalState.de_json({}, offline_bot) is None
-
-    def test_de_json_invalid_type(self, offline_bot):
-        json_dict = {
-            "type": "invalid",
-            "date": to_timestamp(self.date),
-            "url": self.url,
-        }
-        rws = RevenueWithdrawalState.de_json(json_dict, offline_bot)
-        assert rws.api_kwargs == {
-            "date": to_timestamp(self.date),
-            "url": self.url,
-        }
-
-        assert type(rws) is RevenueWithdrawalState
-        assert rws.type == "invalid"
-
-    def test_de_json_subclass(self, rws_scope_class, offline_bot):
-        """This makes sure that e.g. RevenueWithdrawalState(data) never returns a
-        RevenueWithdrawalStateFailed instance."""
-        json_dict = {
-            "type": "invalid",
-            "date": to_timestamp(self.date),
-            "url": self.url,
-        }
-        assert type(rws_scope_class.de_json(json_dict, offline_bot)) is rws_scope_class
-
-    def test_to_dict(self, revenue_withdrawal_state):
-        rws_dict = revenue_withdrawal_state.to_dict()
-
-        assert isinstance(rws_dict, dict)
-        assert rws_dict["type"] == revenue_withdrawal_state.type
-        if hasattr(revenue_withdrawal_state, "date"):
-            assert rws_dict["date"] == to_timestamp(revenue_withdrawal_state.date)
-        if hasattr(revenue_withdrawal_state, "url"):
-            assert rws_dict["url"] == revenue_withdrawal_state.url
-
-    def test_type_enum_conversion(self):
-        assert type(RevenueWithdrawalState("failed").type) is RevenueWithdrawalStateType
-        assert RevenueWithdrawalState("unknown").type == "unknown"
-
-    def test_equality(self, revenue_withdrawal_state, offline_bot):
-        a = RevenueWithdrawalState("base_type")
-        b = RevenueWithdrawalState("base_type")
-        c = revenue_withdrawal_state
-        d = deepcopy(revenue_withdrawal_state)
-        e = Dice(4, "emoji")
-
-        assert a == b
-        assert hash(a) == hash(b)
-
-        assert a != c
-        assert hash(a) != hash(c)
-
-        assert a != d
-        assert hash(a) != hash(d)
-
-        assert a != e
-        assert hash(a) != hash(e)
-
-        assert c == d
-        assert hash(c) == hash(d)
-
-        assert c != e
-        assert hash(c) != hash(e)
-
-        if hasattr(c, "url"):
-            json_dict = c.to_dict()
-            json_dict["url"] = "something"
-            f = c.__class__.de_json(json_dict, offline_bot)
-
-            assert c == f
-            assert hash(c) == hash(f)
-
-        if hasattr(c, "date"):
-            json_dict = c.to_dict()
-            json_dict["date"] = to_timestamp(dtm.datetime.utcnow())
-            f = c.__class__.de_json(json_dict, offline_bot)
-
-            assert c != f
-            assert hash(c) != hash(f)
-
-
-@pytest.fixture
-def affiliate_info():
-    return AffiliateInfo(
-        affiliate_user=AffiliateInfoTestBase.affiliate_user,
-        affiliate_chat=AffiliateInfoTestBase.affiliate_chat,
-        commission_per_mille=AffiliateInfoTestBase.commission_per_mille,
-        amount=AffiliateInfoTestBase.amount,
-        nanostar_amount=AffiliateInfoTestBase.nanostar_amount,
-    )
-
-
-class AffiliateInfoTestBase:
-    affiliate_user = User(id=1, is_bot=True, first_name="affiliate_user", username="username")
-    affiliate_chat = Chat(id=2, type="private", title="affiliate_chat")
-    commission_per_mille = 13
-    amount = 14
-    nanostar_amount = -42
-
-
-class TestAffiliateInfoWithoutRequest(AffiliateInfoTestBase):
-    def test_slot_behaviour(self, affiliate_info):
-        inst = affiliate_info
-        for attr in inst.__slots__:
-            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
-        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
-
-    def test_de_json(self, offline_bot):
-        json_dict = {
-            "affiliate_user": self.affiliate_user.to_dict(),
-            "affiliate_chat": self.affiliate_chat.to_dict(),
-            "commission_per_mille": self.commission_per_mille,
-            "amount": self.amount,
-            "nanostar_amount": self.nanostar_amount,
-        }
-        ai = AffiliateInfo.de_json(json_dict, offline_bot)
-        assert ai.api_kwargs == {}
-        assert ai.affiliate_user == self.affiliate_user
-        assert ai.affiliate_chat == self.affiliate_chat
-        assert ai.commission_per_mille == self.commission_per_mille
-        assert ai.amount == self.amount
-        assert ai.nanostar_amount == self.nanostar_amount
-
-        assert AffiliateInfo.de_json(None, offline_bot) is None
-        assert AffiliateInfo.de_json({}, offline_bot) is None
-
-    def test_to_dict(self, affiliate_info):
-        ai_dict = affiliate_info.to_dict()
-
-        assert isinstance(ai_dict, dict)
-        assert ai_dict["affiliate_user"] == affiliate_info.affiliate_user.to_dict()
-        assert ai_dict["affiliate_chat"] == affiliate_info.affiliate_chat.to_dict()
-        assert ai_dict["commission_per_mille"] == affiliate_info.commission_per_mille
-        assert ai_dict["amount"] == affiliate_info.amount
-        assert ai_dict["nanostar_amount"] == affiliate_info.nanostar_amount
-
-    def test_equality(self, affiliate_info, offline_bot):
-        a = AffiliateInfo(
-            affiliate_user=self.affiliate_user,
-            affiliate_chat=self.affiliate_chat,
-            commission_per_mille=self.commission_per_mille,
-            amount=self.amount,
-            nanostar_amount=self.nanostar_amount,
-        )
-        b = AffiliateInfo(
-            affiliate_user=self.affiliate_user,
-            affiliate_chat=self.affiliate_chat,
-            commission_per_mille=self.commission_per_mille,
-            amount=self.amount,
-            nanostar_amount=self.nanostar_amount,
-        )
-        c = AffiliateInfo(
-            affiliate_user=User(id=3, is_bot=True, first_name="first_name", username="username"),
-            affiliate_chat=self.affiliate_chat,
-            commission_per_mille=self.commission_per_mille,
-            amount=self.amount,
-            nanostar_amount=self.nanostar_amount,
-        )
-        d = AffiliateInfo(
-            affiliate_user=self.affiliate_user,
-            affiliate_chat=Chat(id=3, type="private", title="title"),
-            commission_per_mille=self.commission_per_mille,
-            amount=self.amount,
-            nanostar_amount=self.nanostar_amount,
-        )
-        e = AffiliateInfo(
-            affiliate_user=self.affiliate_user,
-            affiliate_chat=self.affiliate_chat,
-            commission_per_mille=1,
-            amount=self.amount,
-            nanostar_amount=self.nanostar_amount,
-        )
-        f = AffiliateInfo(
-            affiliate_user=self.affiliate_user,
-            affiliate_chat=self.affiliate_chat,
-            commission_per_mille=self.commission_per_mille,
-            amount=1,
-            nanostar_amount=self.nanostar_amount,
-        )
-        g = AffiliateInfo(
-            affiliate_user=self.affiliate_user,
-            affiliate_chat=self.affiliate_chat,
-            commission_per_mille=self.commission_per_mille,
-            amount=self.amount,
-            nanostar_amount=1,
-        )
-        h = Dice(4, "emoji")
-
-        assert a == b
-        assert hash(a) == hash(b)
-
-        assert a != c
-        assert hash(a) != hash(c)
-
-        assert a != d
-        assert hash(a) != hash(d)
-
-        assert a != e
-        assert hash(a) != hash(e)
-
-        assert a != f
-        assert hash(a) != hash(f)
-
-        assert a != g
-        assert hash(a) != hash(g)
-
-        assert a != h
-        assert hash(a) != hash(h)


Code review comments:
path: telegram/_payment/stars/revenuewithdrawelstate.py
text: @user1:
file is spelled wrong, should be `revenuewithdrawalstate`

diff_hunk: @@ -0,0 +1,188 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2024
+# Leandro Toledo de Souza <devs@python-telegram-bot.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+# pylint: disable=redefined-builtin
+"""This module contains the classes for Telegram Stars Revenue Withdrawals."""
+import datetime as dtm
+from typing import TYPE_CHECKING, Final, Optional
+
+from telegram import constants
+from telegram._telegramobject import TelegramObject
+from telegram._utils import enum
+from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
+from telegram._utils.types import JSONDict
+
+if TYPE_CHECKING:
+    from telegram import Bot
+
+
+class RevenueWithdrawalState(TelegramObject):
+    """This object escribes the state of a revenue withdrawal operation. Currently, it can be one
path: telegram/_payment/stars/revenuewithdrawelstate.py
text: @user1:
```suggestion
    """This object describes the state of a revenue withdrawal operation. Currently, it can be one
```