Browse Source

New billing features

pull/10109/head
Dolfies 4 months ago
parent
commit
f6380cf142
  1. 76
      discord/client.py
  2. 5
      discord/enums.py
  3. 27
      discord/http.py
  4. 8
      discord/payments.py
  5. 162
      discord/promotions.py
  6. 82
      discord/subscriptions.py
  7. 4
      discord/types/billing.py
  8. 7
      discord/types/payments.py
  9. 15
      discord/types/promotions.py
  10. 23
      discord/types/subscriptions.py
  11. 16
      docs/api.rst

76
discord/client.py

@ -82,7 +82,7 @@ from .team import Team
from .billing import PaymentSource, PremiumUsage from .billing import PaymentSource, PremiumUsage
from .subscriptions import Subscription, SubscriptionItem, SubscriptionInvoice from .subscriptions import Subscription, SubscriptionItem, SubscriptionInvoice
from .payments import Payment from .payments import Payment
from .promotions import PricingPromotion, Promotion, TrialOffer from .promotions import PricingPromotion, Promotion, TrialOffer, UserOffer
from .entitlements import Entitlement, Gift from .entitlements import Entitlement, Gift
from .store import SKU, StoreListing, SubscriptionPlan from .store import SKU, StoreListing, SubscriptionPlan
from .guild_premium import * from .guild_premium import *
@ -4129,6 +4129,37 @@ class Client:
) )
return [Promotion(state=state, data=d) for d in data] return [Promotion(state=state, data=d) for d in data]
async def user_offer(self, *, payment_gateway: Optional[PaymentGateway] = None) -> UserOffer:
"""|coro|
Retrieves the current user offer for your account.
This includes the trial offer and discount offer.
.. versionadded:: 2.1
Parameters
-----------
payment_gateway: Optional[:class:`.PaymentGateway`]
The payment gateway to fetch the user offer for.
Used to fetch user offers for :attr:`.PaymentGateway.apple`
and :attr:`.PaymentGateway.google` mobile platforms.
Raises
-------
NotFound
You do not have a user offer.
HTTPException
Retrieving the user offer failed.
Returns
-------
:class:`.UserOffer`
The user offer for your account.
"""
state = self._connection
data = await state.http.get_user_offer(payment_gateway=int(payment_gateway) if payment_gateway else None)
return UserOffer(data=data, state=state)
async def trial_offer(self) -> TrialOffer: async def trial_offer(self) -> TrialOffer:
"""|coro| """|coro|
@ -4955,6 +4986,27 @@ class Client:
data = await self._connection.http.get_premium_usage() data = await self._connection.http.get_premium_usage()
return PremiumUsage(data=data) return PremiumUsage(data=data)
async def checkout_recovery(self) -> bool:
"""|coro|
Checks whether the client should prompt the user to
continue their premium purchase.
.. versionadded:: 2.1
Raises
------
HTTPException
Retrieving the checkout recovery eligibility failed.
Returns
-------
:class:`bool`
Whether the client should prompt the user for checkout recovery.
"""
data = await self._connection.http.checkout_recovery_eligibility()
return data.get('is_eligible', False)
async def recent_mentions( async def recent_mentions(
self, self,
*, *,
@ -5137,6 +5189,28 @@ class Client:
data = await state.http.get_channel_affinities() data = await state.http.get_channel_affinities()
return [ChannelAffinity(data=d, state=state) for d in data['channel_affinities']] return [ChannelAffinity(data=d, state=state) for d in data['channel_affinities']]
async def premium_affinities(self) -> List[User]:
"""|coro|
Retrieves a list of friends who have a premium subscription,
used to incentivize the user to purchase one as well.
.. versionadded:: 2.1
Raises
------
HTTPException
Retrieving the premium affinities failed.
Returns
-------
List[:class:`.User`]
The users who share a premium subscription with the current user.
"""
state = self._connection
data = await state.http.get_premium_affinity()
return [state.store_user(d) for d in data]
async def join_active_developer_program(self, *, application: Snowflake, channel: Snowflake) -> int: async def join_active_developer_program(self, *, application: Snowflake, channel: Snowflake) -> int:
"""|coro| """|coro|

5
discord/enums.py

@ -1234,8 +1234,11 @@ class SubscriptionStatus(Enum):
canceled = 3 canceled = 3
cancelled = 3 cancelled = 3
ended = 4 ended = 4
inactive = 5 inactive = 5 # Guess, probably incorrect
account_hold = 6 account_hold = 6
billing_retry = 7
paused = 8
pause_pending = 9
def __int__(self) -> int: def __int__(self) -> int:
return self.value return self.value

27
discord/http.py

@ -4253,12 +4253,36 @@ class HTTPClient:
def ack_trial_offer(self, trial_id: Snowflake) -> Response[promotions.TrialOffer]: def ack_trial_offer(self, trial_id: Snowflake) -> Response[promotions.TrialOffer]:
return self.request(Route('POST', '/users/@me/billing/user-trial-offer/{trial_id}/ack', trial_id=trial_id)) return self.request(Route('POST', '/users/@me/billing/user-trial-offer/{trial_id}/ack', trial_id=trial_id))
def get_user_offer(self, payment_gateway: Optional[int] = None) -> Response[promotions.UserOffer]:
payload = {}
if payment_gateway:
payload['payment_gateway'] = payment_gateway
return self.request(Route('POST', '/users/@me/billing/user-offer'), json=payload)
def ack_user_offer(
self, trial_offer_id: Optional[Snowflake] = None, discount_offer_id: Optional[Snowflake] = None
) -> Response[Optional[promotions.UserOffer]]:
payload = {}
if trial_offer_id:
payload['user_trial_offer_id'] = trial_offer_id
if discount_offer_id:
payload['user_discount_offer_id'] = discount_offer_id
return self.request(Route('POST', '/users/@me/billing/user-offer/ack'), json=payload)
def redeem_user_offer(self, discount_offer_id: Snowflake) -> Response[None]: # TODO: Unknown responses
return self.request(
Route('POST', '/users/@me/billing/user-offer/redeem'), json={'user_discount_offer_id': discount_offer_id}
)
def get_pricing_promotion(self) -> Response[promotions.WrappedPricingPromotion]: def get_pricing_promotion(self) -> Response[promotions.WrappedPricingPromotion]:
return self.request(Route('GET', '/users/@me/billing/localized-pricing-promo')) return self.request(Route('GET', '/users/@me/billing/localized-pricing-promo'))
def get_premium_usage(self) -> Response[billing.PremiumUsage]: def get_premium_usage(self) -> Response[billing.PremiumUsage]:
return self.request(Route('GET', '/users/@me/premium-usage')) return self.request(Route('GET', '/users/@me/premium-usage'))
def checkout_recovery_eligibility(self) -> Response[billing.CheckoutRecovery]:
return self.request(Route('GET', '/users/@me/billing/checkout-recovery'))
# OAuth2 # OAuth2
def get_oauth2_tokens(self) -> Response[List[oauth2.OAuth2Token]]: def get_oauth2_tokens(self) -> Response[List[oauth2.OAuth2Token]]:
@ -4573,6 +4597,9 @@ class HTTPClient:
def get_channel_affinities(self) -> Response[user.ChannelAffinities]: def get_channel_affinities(self) -> Response[user.ChannelAffinities]:
return self.request(Route('GET', '/users/@me/affinities/channels')) return self.request(Route('GET', '/users/@me/affinities/channels'))
def get_premium_affinity(self) -> Response[List[user.PartialUser]]:
return self.request(Route('GET', '/users/@me/billing/nitro-affinity'))
def get_country_code(self) -> Response[subscriptions.CountryCode]: def get_country_code(self) -> Response[subscriptions.CountryCode]:
return self.request(Route('GET', '/users/@me/billing/country-code')) return self.request(Route('GET', '/users/@me/billing/country-code'))

8
discord/payments.py

@ -121,6 +121,10 @@ class Payment(Hashable):
A list of URLs to download VAT credit notices for refunds on this payment, if available. A list of URLs to download VAT credit notices for refunds on this payment, if available.
refund_disqualification_reasons: List[:class:`RefundDisqualificationReason`] refund_disqualification_reasons: List[:class:`RefundDisqualificationReason`]
A list of reasons why the payment cannot be refunded, if any. A list of reasons why the payment cannot be refunded, if any.
error_code: Optional[:class:`int`]
The JSON error code that occurred during the payment, if any.
.. versionadded:: 2.1
""" """
__slots__ = ( __slots__ = (
@ -144,6 +148,7 @@ class Payment(Hashable):
'invoice_url', 'invoice_url',
'refund_invoices_urls', 'refund_invoices_urls',
'refund_disqualification_reasons', 'refund_disqualification_reasons',
'error_code',
'_refundable', '_refundable',
'_flags', '_flags',
'_state', '_state',
@ -191,6 +196,9 @@ class Payment(Hashable):
Subscription(data=data['subscription'], state=state) if 'subscription' in data else None Subscription(data=data['subscription'], state=state) if 'subscription' in data else None
) )
metadata = data.get('metadata') or {}
self.error_code: Optional[int] = metadata.get('billing_error_code')
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<Payment id={self.id} amount={self.amount} currency={self.currency} status={self.status}>' return f'<Payment id={self.id} amount={self.amount} currency={self.currency} status={self.status}>'

162
discord/promotions.py

@ -39,11 +39,14 @@ if TYPE_CHECKING:
ClaimedPromotion as ClaimedPromotionPayload, ClaimedPromotion as ClaimedPromotionPayload,
Promotion as PromotionPayload, Promotion as PromotionPayload,
TrialOffer as TrialOfferPayload, TrialOffer as TrialOfferPayload,
DiscountOffer as DiscountOfferPayload,
UserOffer as UserOfferPayload,
PricingPromotion as PricingPromotionPayload, PricingPromotion as PricingPromotionPayload,
) )
__all__ = ( __all__ = (
'Promotion', 'Promotion',
'UserOffer',
'TrialOffer', 'TrialOffer',
'PricingPromotion', 'PricingPromotion',
) )
@ -201,6 +204,72 @@ class Promotion(Hashable):
return data['code'] return data['code']
class UserOffer:
"""Represents a Discord user offer.
.. versionadded:: 2.1
Attributes
----------
trial_offer: Optional[:class:`TrialOffer`]
The trial offer.
discount_offer: Optional[:class:`DiscountOffer`]
The discount offer.
discount: Optional[:class:`DiscountOffer`]
The discount applied.
"""
__slots__ = (
'trial_offer',
'discount_offer',
'discount',
'_state',
)
def __init__(self, *, data: UserOfferPayload, state: ConnectionState) -> None:
self._state = state
self._update(data)
def _update(self, data: UserOfferPayload) -> None:
state = self._state
self.trial_offer: Optional[TrialOffer] = None
trial_offer = data.get('user_trial_offer')
if trial_offer is not None:
self.trial_offer = TrialOffer(data=trial_offer, state=state)
self.discount_offer: Optional[DiscountOffer] = None
discount_offer = data.get('user_discount_offer')
if discount_offer is not None:
self.discount_offer = DiscountOffer(data=discount_offer, state=state)
self.discount: Optional[DiscountOffer] = None
discount = data.get('user_discount')
if discount is not None:
self.discount = DiscountOffer(data=discount, state=state)
async def ack(self) -> None:
"""|coro|
Acknowledges both the trial and discount offers.
Raises
------
NotFound
The offers were not found.
HTTPException
Acknowledging the offers failed.
"""
# The client sets all offers to null if this 404s
# Unsure if I want to do that here
data = await self._state.http.ack_user_offer(
trial_offer_id=self.trial_offer.id if self.trial_offer else None,
discount_offer_id=self.discount_offer.id if self.discount_offer else None,
)
if data:
self._update(data)
class TrialOffer(Hashable): class TrialOffer(Hashable):
"""Represents a Discord user trial offer. """Represents a Discord user trial offer.
@ -267,6 +336,8 @@ class TrialOffer(Hashable):
Raises Raises
------ ------
NotFound
The trial offer was not found.
HTTPException HTTPException
Acknowledging the trial offer failed. Acknowledging the trial offer failed.
""" """
@ -274,6 +345,97 @@ class TrialOffer(Hashable):
self._update(data) self._update(data)
class DiscountOffer(Hashable):
"""Represents a Discord user discount offer.
.. container:: operations
.. describe:: x == y
Checks if two discount offers are equal.
.. describe:: x != y
Checks if two discount offers are not equal.
.. describe:: hash(x)
Returns the discount offer's hash.
.. versionadded:: 2.1
Attributes
----------
id: :class:`int`
The ID of the discount offer.
expires_at: Optional[:class:`datetime.datetime`]
When the discount offer expires, if it has been acknowledged.
applied_at: Optional[:class:`datetime.datetime`]
When the discount offer was applied.
discount_id: :class:`int`
The ID of the discount.
"""
__slots__ = (
'id',
'expires_at',
'applied_at',
'discount_id',
'_state',
)
def __init__(self, *, data: DiscountOfferPayload, state: ConnectionState) -> None:
self._state = state
self._update(data)
def _update(self, data: DiscountOfferPayload) -> None:
self.id: int = int(data['id'])
self.expires_at: Optional[datetime] = parse_time(data.get('expires_at'))
self.applied_at: Optional[datetime] = parse_time(data.get('applied_at'))
self.discount_id: int = int(data['discount_id'])
def __repr__(self) -> str:
return f'<DiscountOffer id={self.id} discount_id={self.discount_id}>'
def is_acked(self) -> bool:
""":class:`bool`: Checks if the discount offer has been acknowledged."""
return self.expires_at is not None
async def ack(self) -> None:
"""|coro|
Acknowledges the discount offer.
Raises
------
HTTPException
Acknowledging the discount offer failed.
"""
data = await self._state.http.ack_user_offer(discount_offer_id=self.id)
if not data:
return
# The type checker has no idea what is going on here for some reason
if data.get('user_discount_offer') and int(data['user_discount_offer']['id']) == self.id: # type: ignore
self._update(data['user_discount_offer']) # type: ignore
elif data.get('user_discount') and int(data['user_discount']['id']) == self.id: # type: ignore
self._update(data['user_discount']) # type: ignore
async def redeem(self) -> None:
"""|coro|
Applies the discount on the user's existing subscription.
Raises
------
NotFound
The discount offer was not found.
HTTPException
Redeeming the discount offer failed.
"""
await self._state.http.redeem_user_offer(self.id)
class PricingPromotion: class PricingPromotion:
"""Represents a Discord localized pricing promotion. """Represents a Discord localized pricing promotion.

82
discord/subscriptions.py

@ -207,13 +207,14 @@ class SubscriptionInvoiceItem(Hashable):
.. versionadded:: 2.1 .. versionadded:: 2.1
""" """
__slots__ = ('id', 'quantity', 'amount', 'proration', 'plan_id', 'plan_price', 'discounts', 'metadata') __slots__ = ('id', 'quantity', 'amount', 'proration', 'sku_id', 'plan_id', 'plan_price', 'discounts', 'metadata')
def __init__(self, data: SubscriptionInvoiceItemPayload) -> None: def __init__(self, data: SubscriptionInvoiceItemPayload) -> None:
self.id: int = int(data['id']) self.id: int = int(data['id'])
self.quantity: int = data['quantity'] self.quantity: int = data['quantity']
self.amount: int = data['amount'] self.amount: int = data['amount']
self.proration: bool = data.get('proration', False) self.proration: bool = data.get('proration', False)
self.sku_id: Optional[int] = _get_as_snowflake(data, 'sku_id') # Seems to always be null
self.plan_id: int = int(data['subscription_plan_id']) self.plan_id: int = int(data['subscription_plan_id'])
self.plan_price: int = data['subscription_plan_price'] self.plan_price: int = data['subscription_plan_price']
self.discounts: List[SubscriptionDiscount] = [SubscriptionDiscount(d) for d in data['discounts']] self.discounts: List[SubscriptionDiscount] = [SubscriptionDiscount(d) for d in data['discounts']]
@ -282,10 +283,6 @@ class SubscriptionInvoice(Hashable):
When the current billing period started. When the current billing period started.
current_period_end: :class:`datetime.datetime` current_period_end: :class:`datetime.datetime`
When the current billing period ends. When the current billing period ends.
applied_discount_ids: List[:class:`int`]
The IDs of the discounts applied to the invoice.
.. versionadded:: 2.1
""" """
__slots__ = ( __slots__ = (
@ -301,8 +298,6 @@ class SubscriptionInvoice(Hashable):
'items', 'items',
'current_period_start', 'current_period_start',
'current_period_end', 'current_period_end',
'applied_discount_ids',
'applied_user_discounts',
) )
def __init__( def __init__(
@ -327,12 +322,6 @@ class SubscriptionInvoice(Hashable):
self.current_period_start: datetime = parse_time(data['subscription_period_start']) # type: ignore # Should always be a datetime self.current_period_start: datetime = parse_time(data['subscription_period_start']) # type: ignore # Should always be a datetime
self.current_period_end: datetime = parse_time(data['subscription_period_end']) # type: ignore # Should always be a datetime self.current_period_end: datetime = parse_time(data['subscription_period_end']) # type: ignore # Should always be a datetime
# These fields are unknown
self.applied_discount_ids: List[int] = [int(id) for id in data.get('applied_discount_ids', [])]
self.applied_user_discounts: Dict[int, Optional[Any]] = {
int(k): v for k, v in data.get('applied_user_discounts', {}).items()
}
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<SubscriptionInvoice id={self.id} status={self.status!r} total={self.total}>' return f'<SubscriptionInvoice id={self.id} status={self.status!r} total={self.total}>'
@ -489,6 +478,14 @@ class Subscription(Hashable):
The mutations to the subscription that will occur after renewal. The mutations to the subscription that will occur after renewal.
trial_id: Optional[:class:`int`] trial_id: Optional[:class:`int`]
The ID of the trial the subscription is from, if applicable. The ID of the trial the subscription is from, if applicable.
discount_id: Optional[:class:`int`]
The ID of the discount the subscription has active.
.. versionadded:: 2.1
discount_expires_at: Optional[:class:`datetime.datetime`]
When the discount expires, if applicable.
.. versionadded:: 2.1
payment_source_id: Optional[:class:`int`] payment_source_id: Optional[:class:`int`]
The ID of the payment source the subscription is paid with, if applicable. The ID of the payment source the subscription is paid with, if applicable.
payment_gateway_plan_id: Optional[:class:`str`] payment_gateway_plan_id: Optional[:class:`str`]
@ -546,9 +543,11 @@ class Subscription(Hashable):
'current_period_end', 'current_period_end',
'trial_ends_at', 'trial_ends_at',
'streak_started_at', 'streak_started_at',
'ended_at',
'use_storekit_resubscribe', 'use_storekit_resubscribe',
'metadata', 'metadata',
'ended_at',
'discount_id',
'discount_expires_at',
'latest_invoice', 'latest_invoice',
) )
@ -596,9 +595,12 @@ class Subscription(Hashable):
self.streak_started_at: Optional[datetime] = parse_time(data.get('streak_started_at')) self.streak_started_at: Optional[datetime] = parse_time(data.get('streak_started_at'))
self.use_storekit_resubscribe: bool = data.get('use_storekit_resubscribe', False) self.use_storekit_resubscribe: bool = data.get('use_storekit_resubscribe', False)
# Some metadata is exposed; I don't love this implementation
metadata = data.get('metadata') or {} metadata = data.get('metadata') or {}
self.ended_at: Optional[datetime] = parse_time(metadata.get('ended_at', None))
self.metadata: Metadata = Metadata(metadata) self.metadata: Metadata = Metadata(metadata)
self.ended_at: Optional[datetime] = parse_time(metadata.get('ended_at', None))
self.discount_id = _get_as_snowflake(metadata, 'active_discount_id')
self.discount_expires_at = parse_time(metadata.get('active_discount_expires_at', None))
self.latest_invoice: Optional[SubscriptionInvoice] = ( self.latest_invoice: Optional[SubscriptionInvoice] = (
SubscriptionInvoice(self, data=data['latest_invoice'], state=self._state) if 'latest_invoice' in data else None # type: ignore # ??? SubscriptionInvoice(self, data=data['latest_invoice'], state=self._state) if 'latest_invoice' in data else None # type: ignore # ???
@ -616,22 +618,35 @@ class Subscription(Hashable):
@property @property
def guild(self) -> Optional[Guild]: def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild the subscription's entitlements apply to, if applicable.""" """Optional[:class:`Guild`]: The guild the subscription's entitlements apply to, if applicable."""
return self._state._get_guild(self.metadata.guild_id) # I think guild_id is deprecated
return self._state._get_guild(self.metadata.guild_id or self.metadata.application_subscription_guild_id)
@property @property
def grace_period(self) -> timedelta: def grace_period(self) -> Optional[timedelta]:
""":class:`datetime.timedelta`: How many days past the renewal date the user has available to pay outstanding invoices. """Optional[:class:`datetime.timedelta`]: How many days past the renewal date the user has available to pay outstanding invoices.
.. note:: .. note::
This is a static value and does not change based on the subscription's status. This is only available for past-due subscriptions.
For that, see :attr:`remaining`.
.. versionchanged:: 2.1 .. versionchanged:: 2.1
This is now a :class:`datetime.timedelta` instead of an :class:`int`. This is now a Optional[:class:`datetime.timedelta`] instead of an :class:`int`.
"""
if self.grace_period_expires_at:
return self.grace_period_expires_at - utcnow()
@property
def grace_period_expires_at(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: When the grace period expires.
.. versionadded:: 2.1
""" """
return timedelta(days=7 if self.payment_source_id else 3) if self.payment_gateway == PaymentGateway.apple:
return self.metadata.apple_grace_period_expires_date or self.metadata.grace_period_expires_date
if self.payment_gateway == PaymentGateway.google:
return self.metadata.google_grace_period_expires_date or self.metadata.grace_period_expires_date
return self.metadata.grace_period_expires_date
@property @property
def remaining(self) -> timedelta: def remaining(self) -> timedelta:
@ -639,12 +654,14 @@ class Subscription(Hashable):
if self.status in (SubscriptionStatus.active, SubscriptionStatus.cancelled): if self.status in (SubscriptionStatus.active, SubscriptionStatus.cancelled):
return self.current_period_end - utcnow() return self.current_period_end - utcnow()
elif self.status == SubscriptionStatus.past_due: elif self.status == SubscriptionStatus.past_due:
if self.payment_gateway == PaymentGateway.google and self.metadata.google_grace_period_expires_date: # Grace expiry is now always provided
return self.metadata.google_grace_period_expires_date - utcnow() return self.grace_period_expires_at - utcnow() # type: ignore
return (self.current_period_start + self.grace_period) - utcnow()
elif self.status == SubscriptionStatus.account_hold: elif self.status == SubscriptionStatus.account_hold:
# Max hold time is 30 days # Max hold time is 30 days
return (self.current_period_start + timedelta(days=30)) - utcnow() return (self.current_period_start + timedelta(days=30)) - utcnow()
elif self.status == SubscriptionStatus.billing_retry:
# Max retry time is 7 days
return (self.current_period_start + timedelta(days=7)) - utcnow()
return timedelta() return timedelta()
@property @property
@ -657,6 +674,16 @@ class Subscription(Hashable):
return self.remaining return self.remaining
return self.trial_ends_at - utcnow() return self.trial_ends_at - utcnow()
@property
def discount_remaining(self) -> timedelta:
""":class:`datetime.timedelta`: The remaining time until the active discount on the subscription ends."""
if not self.discount_id:
return timedelta()
if not self.discount_expires_at:
# Infinite discount?
return self.remaining
return self.discount_expires_at - utcnow()
def is_active(self) -> bool: def is_active(self) -> bool:
""":class:`bool`: Indicates if the subscription is currently active and providing perks.""" """:class:`bool`: Indicates if the subscription is currently active and providing perks."""
return self.remaining > timedelta() return self.remaining > timedelta()
@ -750,6 +777,7 @@ class Subscription(Hashable):
currency: str = MISSING, currency: str = MISSING,
apply_entitlements: bool = MISSING, apply_entitlements: bool = MISSING,
renewal: bool = MISSING, renewal: bool = MISSING,
discount_offer: Snowflake = MISSING,
) -> SubscriptionInvoice: ) -> SubscriptionInvoice:
"""|coro| """|coro|
@ -769,6 +797,8 @@ class Subscription(Hashable):
Whether to apply entitlements (credits) to the previewed invoice. Whether to apply entitlements (credits) to the previewed invoice.
renewal: :class:`bool` renewal: :class:`bool`
Whether the previewed invoice should be a renewal. Whether the previewed invoice should be a renewal.
discount_offer: :class:`.DiscountOffer`
The discount offer to apply to the previewed invoice.
Raises Raises
------ ------
@ -791,6 +821,8 @@ class Subscription(Hashable):
payload['apply_entitlements'] = apply_entitlements payload['apply_entitlements'] = apply_entitlements
if renewal is not MISSING: if renewal is not MISSING:
payload['renewal'] = renewal payload['renewal'] = renewal
if discount_offer is not MISSING:
payload['user_discount_offer_id'] = discount_offer.id
if payload: if payload:
data = await self._state.http.preview_subscription_update(self.id, **payload) data = await self._state.http.preview_subscription_update(self.id, **payload)

4
discord/types/billing.py

@ -76,3 +76,7 @@ class PremiumUsage(TypedDict):
total_large_uploads: PremiumUsageValue total_large_uploads: PremiumUsageValue
total_hd_streams: PremiumUsageValue total_hd_streams: PremiumUsageValue
hd_hours_streamed: PremiumUsageValue hd_hours_streamed: PremiumUsageValue
class CheckoutRecovery(TypedDict):
is_eligible: bool

7
discord/types/payments.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import List, Literal, TypedDict from typing import List, Literal, Optional, TypedDict
from typing_extensions import NotRequired from typing_extensions import NotRequired
from .billing import PartialPaymentSource from .billing import PartialPaymentSource
@ -59,3 +59,8 @@ class Payment(PartialPayment):
sku: NotRequired[PublicSKU] sku: NotRequired[PublicSKU]
payment_source: NotRequired[PartialPaymentSource] payment_source: NotRequired[PartialPaymentSource]
subscription: NotRequired[PartialSubscription] subscription: NotRequired[PartialSubscription]
metadata: PaymentMetadata
class PaymentMetadata(TypedDict):
billing_error_code: Optional[int]

15
discord/types/promotions.py

@ -55,11 +55,26 @@ class ClaimedPromotion(TypedDict):
claimed_at: str claimed_at: str
class UserOffer(TypedDict):
user_trial_offer: Optional[TrialOffer]
user_discount_offer: Optional[DiscountOffer]
user_discount: Optional[DiscountOffer]
class TrialOffer(TypedDict): class TrialOffer(TypedDict):
id: Snowflake id: Snowflake
expires_at: Optional[str] expires_at: Optional[str]
trial_id: Snowflake trial_id: Snowflake
subscription_trial: SubscriptionTrial subscription_trial: SubscriptionTrial
user_id: Snowflake
class DiscountOffer(TypedDict):
id: Snowflake
expires_at: Optional[str]
applied_at: Optional[str]
discount_id: Snowflake
user_id: Snowflake
class PromotionalPrice(TypedDict): class PromotionalPrice(TypedDict):

23
discord/types/subscriptions.py

@ -73,6 +73,7 @@ class SubscriptionInvoiceItem(TypedDict):
subscription_plan_id: Snowflake subscription_plan_id: Snowflake
subscription_plan_price: int subscription_plan_price: int
discounts: List[SubscriptionDiscount] discounts: List[SubscriptionDiscount]
sku_id: Optional[Snowflake]
tenant_metadata: NotRequired[Dict[str, Any]] tenant_metadata: NotRequired[Dict[str, Any]]
@ -87,8 +88,8 @@ class SubscriptionInvoice(TypedDict):
items: List[SubscriptionInvoiceItem] items: List[SubscriptionInvoiceItem]
current_period_start: str current_period_start: str
current_period_end: str current_period_end: str
applied_discount_ids: NotRequired[List[Snowflake]] # applied_discount_ids: NotRequired[List[Snowflake]]
applied_user_discounts: NotRequired[Dict[Snowflake, Optional[Any]]] # applied_user_discounts: NotRequired[Dict[Snowflake, Optional[Any]]]
class SubscriptionRenewalMutations(TypedDict, total=False): class SubscriptionRenewalMutations(TypedDict, total=False):
@ -118,12 +119,28 @@ class Subscription(PartialSubscription):
canceled_at: NotRequired[str] canceled_at: NotRequired[str]
country_code: Optional[str] country_code: Optional[str]
trial_ends_at: NotRequired[str] trial_ends_at: NotRequired[str]
metadata: NotRequired[Dict[str, Any]] metadata: NotRequired[SubscriptionMetadata]
latest_invoice: NotRequired[SubscriptionInvoice] latest_invoice: NotRequired[SubscriptionInvoice]
use_storekit_resubscribe: bool use_storekit_resubscribe: bool
price: Optional[int] price: Optional[int]
class SubscriptionMetadata(TypedDict, total=False):
is_egs: bool
is_holiday_promotion_2021: bool
ended_at: str
guild_id: Snowflake
application_subscription_guild_id: Snowflake
grace_period_expires_date: str
apple_grace_period_expires_date: str
google_grace_period_expires_date: str
google_original_expires_date: str
user_trial_offer_id: Snowflake
user_discount_offer_id: Snowflake # guess
active_discount_id: Snowflake
active_discount_expires_at: str
class SubscriptionTrial(TypedDict): class SubscriptionTrial(TypedDict):
id: Snowflake id: Snowflake
interval: Literal[1, 2, 3] interval: Literal[1, 2, 3]

16
docs/api.rst

@ -4166,6 +4166,12 @@ of :class:`enum.Enum`.
The subscription is on account hold. The subscription is on account hold.
.. attribute:: billing_retry
The subscription failed to bill and will retry.
.. versionadded:: 2.1
.. class:: SubscriptionInvoiceStatus .. class:: SubscriptionInvoiceStatus
Represents the status of a subscription invoice. Represents the status of a subscription invoice.
@ -7334,11 +7340,21 @@ Promotion
.. autoclass:: PricingPromotion() .. autoclass:: PricingPromotion()
:members: :members:
.. attributetable:: UserOffer
.. autoclass:: UserOffer()
:members:
.. attributetable:: TrialOffer .. attributetable:: TrialOffer
.. autoclass:: TrialOffer() .. autoclass:: TrialOffer()
:members: :members:
.. attributetable:: DiscountOffer
.. autoclass:: DiscountOffer()
:members:
Subscription Subscription
~~~~~~~~~~~~ ~~~~~~~~~~~~

Loading…
Cancel
Save